Go 1.21是否会包含通过WebAssembly托管HTTP的功能?如何实现?

huangapple go评论107阅读模式
英文:

Will Go 1.21 include ability to host http via WebAssembly? How?

问题

我想尝试在Go语言中使用WebAssembly来创建一个HTTP服务器。我认为在Go 1.20中不支持在浏览器之外编译WebAssembly,而且tinygo中也没有包含net/http库。

我在阅读了https://stackoverflow.com/a/76091829之后尝试使用gotip来实现(感谢@TachyonicBytes),但是每当我尝试启动服务器(或任何阻塞/等待函数)时,都会出现错误: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还在开发中,还是因为我没有正确理解启动阻塞函数的方法,或者因为在Go 1.21中不支持这种方式?

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

英文:

I'd like to try an http server via WebAssembly on Go. I think that compiling go for webassembly outside the browser is not supported in go 1.20, and that the net/http libraries aren't included in tinygo.

I tried to do it with gotip after reading https://stackoverflow.com/a/76091829 (thanks @TachyonicBytes), but whenever I tried to start the server (or any blocking/waiting function), I got an error: fatal error: all goroutines are asleep - deadlock!. I tried moving things to a goroutine with wait functions and that either simply ended the function, or gave the same error.
Here's how I ran it:

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

Here's the example 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")
}

So, is this just because go 1.21 is a WIP, because I'm failing to understand the proper way to start a blocking function, or because this sort of thing won't be supported in go 1.21?

I tried to start a go server in a server side webassembly runner wasm3 on an Intel Mac. I expected it to serve http, but found it either threw an error, or exited immediately.

答案1

得分: 3

很高兴能帮上忙!

不幸的是,看起来 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 仅对套接字提供了部分支持,所以目前还没有完整的 Berkeley 套接字支持。

好消息是你实际上可以在 tinygo 中使用 http,tinygo 对 go 的 net/http 包有部分支持,具体可以参考它的驱动程序

如果你想看到一些实际使用的例子,我目前正在尝试将 这个 项目使用 tinygo 移植到 wasm,如果我记得正确,我已经让它工作了,但是已经有一段时间了,我确定我还没有完成转换。也许目前来说这是不可能的。

另外,尽管 wasm3 部分实现了 WASI,但可能还没有实现套接字部分。我建议你也尝试一些其他的运行时,比如 wasmtime、wasmer、wasmedge 或 wazero,@Gedw99 提到的。Wasmedge 对套接字有很好的支持,但在你的情况下,编译器实际上是个问题。

英文:

Glad to have been of help!

Unfortunately no, it seems that wasm networking will not be a part of go 1.21. It's a bit complicated to implement networking in wasm. Running your code, I got this line:

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

Upon inspection, it has this disclaimer:

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

The hard part of doing this is that WASI has only partial support for sockets, so no full blown Berkeley sockets for WASI, yet.

The good news is that you can actually do http, but in tinygo. Tinygo has partial support for the go net/http package, with it's drivers.

If you want to see some real-life usage of this, I am currently trying to port this project to wasm, using tinygo. If I recall correctly, I got it to work, but it has been a while, and I know for sure that I did not complete the conversion yet. Maybe it was impossible for the time being.

Another thing is that wasm3, despite having partial wasi implementation, may not have implemented the sockets part. I would suggest also playing with some other runtimes, like wasmtime, wasmer, wasmedge, or wazero, which @Gedw99 suggested. Wasmedge has great support for sockets, but in your case, the compiler is actually the problem.

答案2

得分: 1

我已经成功地通过将TCP套接字的打开文件描述符传递给guest模块,并调用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() {
	// 通过上下文将网络的可用性传递给guest模块。
	// 据我所知,目前还没有专门的API来确定
	// a) 动态端口分配,
	// b) 文件描述符。
	// 但是,我们可以对后者进行合理的猜测:由于标准输入、
	// 标准输出和标准错误输出的文件描述符是0-2,我们的套接字应该是3。
	// 请注意,这个猜测是从guest模块的角度来看的。
	ctx := sock.WithConfig(
		context.Background(),
		sock.NewConfig().WithTCPListener("127.0.0.1", 8080),
	)

	// 运行时和WASI准备。
	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)
	wasi_snapshot_preview1.MustInstantiate(ctx, r)

	// 模块配置。
	cfg := wazero.NewModuleConfig().WithStdout(os.Stdout).WithStderr(os.Stderr)
	// stdout/stderr 用于简单的调试:这会破坏沙盒。

	// 导出一个函数供guest模块获取(猜测的)文件描述符。
	if _, err := r.NewHostModuleBuilder("env").NewFunctionBuilder().
		WithFunc(func() uint32 {
			return socketDescriptor
		}).Export("getSocketDescriptor").Instantiate(ctx); err != nil {
		panic(err)
	}
	// 我们也可以通过环境变量提供文件描述符,
	// 但是使用字符串可能会很麻烦:
	// cfg = cfg.WithEnv("socketDescriptor", fmt.Sprint(socketDescriptor))

	// 编译步骤
	compiled, err := r.CompileModule(ctx, moduleData)
	if err != nil {
		panic(err)
	}

	// 运行模块
	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() {
	// 从主机接收打开的TCP套接字的文件描述符。
	sd := getSocketDescriptor()

	// 由于缺少线程,阻塞I/O是有问题的。
	if err := syscall.SetNonblock(int(sd), true); err != nil {
		panic(err)
	}

	// 当上下文结束时,主机应该关闭文件描述符。
	// 文件名是任意的。
	ln, err := net.FileListener(os.NewFile(uintptr(sd), "[socket]"))
	if err != nil {
		panic(err)
	}

	// HTTP服务器
	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上成功测试。从模块内部找到套接字文件描述符的另一种方法是迭代正整数,直到出现"bad file number"错误或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
英文:

I have managed to get this to work with 1.21 by passing an open file descriptor of a TCP socket to the guest module, and calling net.FileListener.

First part is achieved with the github.com/tetratelabs/wazero runtime using the submodule experimental/sock. Below is a simple demo.

host.go, run with 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, compiled with 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)
}
}

Successfully tested with Ubuntu/WSL. Another way to find the socket fd from within the module is to iterate over positive integers until "bad file number" error or a syscall.Fstat() -> *syscall.Stat_t that implies a socket.

Update for clarity: After the two files are in place in the same directory run the following commands (people from the future should be able to replace gotip with just go) and visit http://127.0.0.1:8080 with your browser:

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

答案3

得分: 0

“所有的goroutine都处于休眠状态 - 死锁!” - 当使用Go语言和WebAssembly时,这是一个常见的问题。

解决方法是需要在一个单独的goroutine中执行http.Get操作,而不是使用WaitGroup。

--

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

英文:

"all goroutines are asleep - deadlock!" - its a common surprise when using golang with wasm.

The solution is that you would need to perform the http.Get in a separate goroutine. Don't use WaitGroup.

--

Also for running wasm outside the browser you can use wazero. https://github.com/tetratelabs/wazero

huangapple
  • 本文由 发表于 2023年7月16日 05:52:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/76695907.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定