Go 1.21 会包含通过 WebAssembly 托管 http 的功能吗?怎么办?

问题描述 投票:0回答:3

我想在 Go 上通过 WebAssembly 尝试 http 服务器。我认为 go 1.20 不支持在浏览器外编译 go for web assembly,并且tinygo 中不包含 net/http 库。

在阅读

https://stackoverflow.com/a/76091829
(感谢@TachyonicBytes)后,我尝试用gotip来做到这一点,但是每当我尝试启动服务器(或任何阻塞/等待功能)时,我得到了错误:
fatal error: all goroutines are asleep - deadlock!
。我尝试将事情转移到带有等待函数的 goroutine 中,但要么简单地结束该函数,要么给出相同的错误。 这是我的运行方式:

go install golang.org/dl/gotip@latest
gotip download
GOOS=wasip1 GOARCH=wasm gotip build -o server.wasm server.go && wasm3 server.wasm

这是示例

server.go

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
    s := http.Server{
        Addr: ":8080",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("Hello, World!"))
        }),
    }

    fmt.Println("about to serve")
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        err := s.ListenAndServe()
        if err != nil {
            fmt.Printf("Unable to serve: %v\n", err)
        }
        wg.Done()
        fmt.Println("serving stopped")
    }()
    wg.Wait()
    fmt.Println("started up server")
}

那么,这仅仅是因为 go 1.21 是一个 WIP,因为我无法理解启动阻塞函数的正确方法,或者因为 go 1.21 不支持这种事情?

我尝试在 Intel Mac 上的服务器端 WebAssembly 运行器 wasm3 中启动 Go 服务器。我期望它提供 http 服务,但发现它要么抛出错误,要么立即退出。

go webassembly wasi go-wasm
3个回答
1
投票

很高兴能提供帮助!

不幸的是,wasm 网络似乎不会成为 go 1.21 的一部分。在 wasm 中实现网络有点复杂。运行你的代码,我得到了这一行:

    sdk/gotip/src/net/net_fake.go:229

检查,有此免责声明:

// Fake networking for js/wasm and wasip1/wasm.
// This file only exists to make the compiler happy.

这样做的难点在于 WASI 仅对套接字提供部分支持,因此 WASI 还没有完整的 Berkeley 套接字。

好消息是你实际上可以做http,但是在tinygo中。 Tinygo 对 go

net/http
软件包提供部分支持,包括 drivers

如果你想看看它的一些实际用法,我目前正在尝试使用tinygo将this项目移植到wasm。如果我没记错的话,我已经让它工作了,但已经有一段时间了,我确信我还没有完成转换。也许暂时不可能。

另一件事是,

wasm3
,尽管有部分 wasi 实现,但可能没有实现套接字部分。我建议也使用一些其他运行时,例如 @Gedw99 建议的 wasmtime、wasmer、wasmedge 或 wazero。 Wasmedge 对套接字有很好的支持,但在你的情况下,编译器实际上是问题所在。


1
投票

我通过将 TCP 套接字的打开文件描述符传递给来宾模块并调用

net.FileListener
,成功地使其能够与 1.21 一起使用。

第一部分是通过

github.com/tetratelabs/wazero
运行时使用子模块
experimental/sock
实现的。下面是一个简单的演示。

host.go
,与
gotip

一起奔跑
package main

import (
    "context"
    _ "embed"
    "os"

    "github.com/tetratelabs/wazero"
    "github.com/tetratelabs/wazero/experimental/sock"
    "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

const socketDescriptor uint32 = 3

//go:embed module.wasm
var moduleData []byte

func main() {
    // The availability of networking is passed to the guest module via context.
    // AFAIK there is not yet any bespoke API to figure out
    // a) dynamic port allocation,
    // b) the file descriptor.
    // However, we can make an educated guess of the latter: since stdin,
    // stdout and stderr are the file descriptors 0-2, our socket SHOULD be 3.
    // Take note that this guess is for the perspective of the guest module.
    ctx := sock.WithConfig(
        context.Background(),
        sock.NewConfig().WithTCPListener("127.0.0.1", 8080),
    )

    // Runtime and WASI prep.
    r := wazero.NewRuntime(ctx)
    defer r.Close(ctx)
    wasi_snapshot_preview1.MustInstantiate(ctx, r)

    // Module configuration.
    cfg := wazero.NewModuleConfig().WithStdout(os.Stdout).WithStderr(os.Stderr)
    // stdout/stderr added for simple debugging: this breaks sandboxing.

    // Export a function for the guest to fetch the (guessed) fd.
    if _, err := r.NewHostModuleBuilder("env").NewFunctionBuilder().
        WithFunc(func() uint32 {
            return socketDescriptor
        }).Export("getSocketDescriptor").Instantiate(ctx); err != nil {
        panic(err)
    }
    // We also could provide the fd via an environment variable,
    // but working with strings can be annoying:
    // cfg = cfg.WithEnv("socketDescriptor", fmt.Sprint(socketDescriptor))

    // Compilation step
    compiled, err := r.CompileModule(ctx, moduleData)
    if err != nil {
        panic(err)
    }

    // Run the module
    if _, err := r.InstantiateModule(ctx, compiled, cfg); err != nil {
        panic(err)
    }
}

module.go
,用
GOOS="wasip1" GOARCH="wasm" gotip build -o module.wasm module.go

编译
package main

import (
    "net"
    "net/http"
    "os"
    "syscall"
)

//go:wasmimport env getSocketDescriptor
func getSocketDescriptor() uint32

func main() {
    // Receive the file descriptor of the open TCP socket from host.
    sd := getSocketDescriptor()

    // Blocking I/O is problematic due to the lack of threads.
    if err := syscall.SetNonblock(int(sd), true); err != nil {
        panic(err)
    }

    // The host SHOULD close the file descriptor when the context is done.
    // The file name is arbitrary.
    ln, err := net.FileListener(os.NewFile(uintptr(sd), "[socket]"))
    if err != nil {
        panic(err)
    }

    // HTTP server
    if err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!\n"))
    })); err != nil {
        panic(err)
    }
}

已成功使用 Ubuntu/WSL 进行测试。从模块内查找套接字 fd 的另一种方法是迭代正整数,直到出现“错误文件号”错误或 syscall.Fstat() -> *syscall.Stat_t 暗示套接字。

为清楚起见进行更新:将两个文件放在同一目录中后,运行以下命令(将来的人应该能够将

gotip
替换为
go
)并访问 http://127.0.0.1: 8080 使用浏览器:

gotip mod init go-wasm-hello-world
gotip mod tidy
GOOS="wasip1" GOARCH="wasm" gotip build -o module.wasm module.go
gotip run host.go

0
投票

“所有 goroutine 都在睡觉 - 僵局!” - 当使用 golang 和 wasm 时,这是一个常见的惊喜。

解决方案是您需要在单独的 goroutine 中执行 http.Get。不要使用 WaitGroup。

--

如果要在浏览器外运行 wasm,您可以使用 wazero。 https://github.com/tetratelabs/wazero

© www.soinside.com 2019 - 2024. All rights reserved.