我想在 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 服务,但发现它要么抛出错误,要么立即退出。
很高兴能提供帮助!
不幸的是,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 对套接字有很好的支持,但在你的情况下,编译器实际上是问题所在。
我通过将 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
“所有 goroutine 都在睡觉 - 僵局!” - 当使用 golang 和 wasm 时,这是一个常见的惊喜。
解决方案是您需要在单独的 goroutine 中执行 http.Get。不要使用 WaitGroup。
--
如果要在浏览器外运行 wasm,您可以使用 wazero。 https://github.com/tetratelabs/wazero