dispatchrun / net

Go package implementing WASI socket extensions
Apache License 2.0
137 stars 7 forks source link

Compatibility with Gorilla Websockets #25

Open michaelweber opened 5 months ago

michaelweber commented 5 months ago

Hi!

I've been trying to compile some of the example netcode in the gorilla/websockets project using the net extensions package. Specifically I'm going with the fairly straightforward echo server example at https://github.com/gorilla/websocket/tree/main/examples/echo.

I have no issues when I try to replace the server code (in server.go) with the wasip1 bindings, I just swap a single err := server.ListenAndServe() line out and replace it with a call to wasip1.Listen along with a follow-up definition of an http.Server and call to .Serve(). I can use wasirun to execute the example perfectly by building with GOOS=wasip1 GOARCH=wasm go build -o server.wasm server.go and then running wasirun server.wasm. It works perfectly!

When I try to replace the client code however, I run into some weirdness. I'm including the entire file of client.go here after my changes:

// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore
// +build ignore

package main

import (
    "flag"
    "github.com/stealthrocket/net/wasip1"
    "log"
    "net/url"
    "os"
    "os/signal"
    "time"

    "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "127.0.0.1:8080", "http service address")

func main() {
    flag.Parse()
    log.SetFlags(0)

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
    log.Printf("connecting to %s", u.String())

    dialer := websocket.DefaultDialer
    dialer.NetDialContext = wasip1.DialContext
    dialer.NetDialTLSContext = wasip1.DialContext

    c, _, err := dialer.Dial(u.String(), nil)

    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    done := make(chan struct{})

    go func() {
        defer close(done)
        for {
            mt, message, err := c.ReadMessage()
            if err != nil {
                log.Println("read:", err)
                return
            }
            log.Printf("recv: %s, type: %s", message, websocket.FormatMessageType(mt))
        }
    }()

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-done:
            return
        case t := <-ticker.C:
            err := c.WriteMessage(websocket.TextMessage, []byte(t.String()))
            if err != nil {
                log.Println("write:", err)
                return
            }
        case <-interrupt:
            log.Println("interrupt")

            // Cleanly close the connection by sending a close message and then
            // waiting (with timeout) for the server to close the connection.
            err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
            if err != nil {
                log.Println("write close:", err)
                return
            }
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}

The key change I've tried to make here is that I've swapped:

c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)

into

dialer := websocket.DefaultDialer
dialer.NetDialContext = wasip1.DialContext
dialer.NetDialTLSContext = wasip1.DialContext
c, _, err := dialer.Dial(u.String(), nil)

From what I can tell in the codebase at https://github.com/gorilla/websocket/blob/main/client.go#L255-L277 - this should be the right place to be replacing the dial contexts, though I'm not certain.

I compile the code with GOOS=wasip1 GOARCH=wasm go build -o client.wasm client.go and then try running it with wasirun --trace --dial 127.0.0.1:8080 --sockets=auto --non-blocking-stdio client.wasm. It hangs for a while and then the connection times out. What's interesting is that it only hangs if the server is listening, otherwise I get an immediate dial:dial tcp 127.0.0.1:8080: connect: Connection refused error. And after it times out (dial:read tcp 127.0.0.1:60849->127.0.0.1:8080: i/o timeout) I can see the server warn me that a connection to it has been reset, read: read tcp 127.0.0.1:8080->127.0.0.1:60849: fd_read: Connection reset by peer. So it looks like this is at least partially working, but I'm not quite sure why it's hanging and timing out. Maybe there's an issue with threading I need to figure out how to avoid?

Any tips on how to proceed further / am I using the package incorrectly?

Cheers!