使用 Go 和 WebAssembly 创建实时 WebSocket

用 Go 编写 WebAssembly 一直是我想做的事,但很难找到示例或教程,所以我决定自己翻翻书。

在这篇文章中,我将与大家分享我的心得和一些代码示例。

这个项目是对实时网络通信世界的一次探索。我们正在深入研究直接从 WebAssembly (Wasm) 代码连接 WebSocket,从而在 Web 中实现动态交互。通过利用 WebSocket 事件,我们可以根据实时数据操作文档对象模型(DOM),从而创建交互式和响应式的用户体验。

项目概要

在本项目中,我们将创建一个 WebSocket 服务(使用 Gorilla WebSocket)、一个 WASM 服务和一个 index.html(仅用于查看我们所做的工作)。

文件夹结构

在这个基本项目中,我们将保持文件夹结构的简单明了,以避免复杂性。我们只需要一个存放 WebAssembly 文件的 wasm/ 目录、一个存放套接字服务逻辑的 pkg/socket/ 目录,以及一个存放前端资产的 public/ 目录。

项目看起来像这样:

main.go 
wasm/ 
pkg/socket/ 
public/ 
Makefile

public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Go WASM Button Example</title>
 <script src="wasm_exec.js"></script>
 <style>
 #btn {
 position: absolute;
 top: 50%;
 left: 50%;
 transform: translate(-50%, -50%);
 }
 </style>
</head>
<body>
 <p id="text"></p>
 <button id="btn">Click me</button>
 <script>
 const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
 go.run(result.instance);
 });
document.getElementById("btn").addEventListener("click", () => {
 onButtonClick();
 });
 </script>
</body>
</html>

我们的 HTML 布局有意采用简约风格,只在屏幕中央放置一个按钮。点击后,该按钮会激活 “click “事件,进而调用 “buttonClick() “函数。

此外,屏幕顶部还有一个 id 为 text<p> 标签,我们将通过 WebAssembly 代码对其进行动态处理。

pkg/socket/manage.go

package socket
import (
 "log"
 "math/rand"
 "net/http"
 "sync"
"github.com/gorilla/websocket"
)
// Pre-configure the upgrader, which is responsible for upgrading
// an HTTP connection to a WebSocket connection.
var (
 websocketUpgrader = websocket.Upgrader{
 ReadBufferSize: 1024,
 WriteBufferSize: 1024,
 }
)
// NotifyEvent represents an event that contains a reference
// to the client who initiated the event and the message to be notified.
type NotifyEvent struct {
 client *Client
 message string
}
// Client represents a single WebSocket connection.
// It holds the client's ID, the WebSocket connection itself, and
// the manager that controls all clients.
type Client struct {
 id uint32
 connection *websocket.Conn
 manager *Manager
writeChan chan string
}
// Manager keeps track of all active clients and broadcasts messages.
type Manager struct {
 clients ClientList
sync.RWMutex
notifyChan chan NotifyEvent
}
// ClientList is a map of clients to keep track of their presence.
type ClientList map[*Client]bool
// NewClient creates a new Client instance with a unique ID, its connection,
// and a reference to the Manager.
func NewClient(conn *websocket.Conn, manager *Manager) *Client {
 return &Client{
 id: rand.Uint32(),
 connection: conn,
 manager: manager,
 writeChan: make(chan string),
 }
}
// readMessages continuously reads messages from the WebSocket connection.
// It will send any received messages to the manager's notification channel.
func (c *Client) readMessages() {
 defer func() {
 c.manager.removeClient(c)
 }()
for {
 messageType, payload, err := c.connection.ReadMessage()
c.manager.notifyChan <- NotifyEvent{client: c, message: string(payload)}
if err != nil {
 if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
 log.Printf("error reading message: %v", err)
 }
 break
 }
 log.Println("MessageType: ", messageType)
 log.Println("Payload: ", string(payload))
 }
}
// writeMessages listens on the client's write channel for messages
// and writes any received messages to the WebSocket connection.
func (c *Client) writeMessages() {
 defer func() {
 c.manager.removeClient(c)
 }()
for {
 select {
 case data := <-c.writeChan:
 c.connection.WriteMessage(websocket.TextMessage, []byte(data))
 }
 }
}
// NewManager creates a new Manager instance, initializes the client list,
// and starts the goroutine responsible for notifying other clients.
func NewManager() *Manager {
 m := &Manager{
 clients: make(ClientList),
 notifyChan: make(chan NotifyEvent),
 }
go m.notifyOtherClients()
return m
}
// otherClients returns a slice of clients excluding the provided client.
func (m *Manager) otherClients(client *Client) []*Client {
 clientList := make([]*Client, 0)
for c := range m.clients {
 if c.id != client.id {
 clientList = append(clientList, c)
 }
 }
return clientList
}
// notifyOtherClients waits for notify events and broadcasts the message
// to all clients except the one who sent the message.
func (m *Manager) notifyOtherClients() {
 for {
 select {
 case e := <-m.notifyChan:
 otherClients := m.otherClients(e.client)
for _, c := range otherClients {
 c.writeChan <- e.message
 }
 }
 }
}
// addClient adds a new client to the manager's client list.
func (m *Manager) addClient(client *Client) {
 m.Lock()
 defer m.Unlock()
m.clients[client] = true
}
// removeClient removes a client from the manager's client list and
// closes the WebSocket connection.
func (m *Manager) removeClient(client *Client) {
 m.Lock()
 defer m.Unlock()
if _, ok := m.clients[client]; ok {
 client.connection.Close()
 delete(m.clients, client)
 }
}
// ServeWS is an HTTP handler that upgrades the HTTP connection to a
// WebSocket connection and registers the new client with the manager.
func (m *Manager) ServeWS(w http.ResponseWriter, r *http.Request) {
 log.Println("New Connection")
conn, err := websocketUpgrader.Upgrade(w, r, nil)
 if err != nil {
 log.Println(err)
 return
 }
client := NewClient(conn, m)
 m.addClient(client)
go client.readMessages()
 go client.writeMessages()
}
Our WebSocket service is powered by a Go file that relies on the `gorilla/websocket` library. It begins with setting up an `Upgrader` from the library, which transitions an HTTP connection to the WebSocket protocol, allowing for real-time communication.

代码定义了一个 NotifyEvent 类型,用于封装来自客户端(激活的 WebSocket 连接)的消息,以及一个客户端类型,用于跟踪单个连接及其通过通道与服务器的通信。

Manager 类型负责管理所有客户端实例,并在 “ClientList”(一个跟踪所有活动连接的映射)的帮助下管理传入和传出的消息。

客户端创建时有一个唯一的 ID,并与其 WebSocket 连接和管理系统绑定。它们有两个主要例程: 读取信息”(readMessages)侦听传入信息并将其转发给管理器,”写入信息”(writeMessages)将传出信息从服务器发送到客户端的 WebSocket。

管理器还负责向除发送者以外的所有客户端广播消息,确保消息实时传播给所有连接的用户。

最后,”ServeWS “是一个函数,它通过将 HTTP 请求升级到 WebSocket 来处理新的 WebSocket 连接,并启动消息读写例程。

通过这种结构,我们可以高效地管理多个客户端及其交互,形成基于 WebSocket 的实时通信服务的骨干。

main.go

main.go,在这个文件中我们需要提供 index.html 和一些 websocket 的东西。为此,我将使用“net/http”包。

package main
import (
 "log"
 "net/http"
"github.com/doganarif/wasm/2/pkg/socket"
)
func main() {
 setupAPI()
// Serve on port :8080, fudge yeah hardcoded port
 log.Fatal(http.ListenAndServe(":8080", nil))
}
// setupAPI will start all Routes and their Handlers
func setupAPI() {
 manager := socket.NewManager()
// Serve the ./public directory at Route /
 http.Handle("/", http.FileServer(http.Dir("./public")))
 http.Handle("/ws", http.HandlerFunc(manager.ServeWS))
}

我们的网络服务有一个简单的起点,一个启动一切的文件。它设置了我们的网络服务器,并告诉它监听 8080 端口(Web开发测试的经典端口)上的访客。

我们调用一个 setupAPI() 函数来完成一些幕后工作。它利用我们的套接字软件包创建一个Manager ,Manager 就像一出戏的导演,负责协调我们所有的 WebSocket 连接。

我们还为 WebSocket 连接设置了一个特殊的 /ws 路由,在这里我们的 Manager 开始处理实时聊天。

wasm/wasm.go

package main
import (
 "context"
 "fmt"
 "log"
 "syscall/js"
"nhooyr.io/websocket"
)
// Conn wraps a WebSocket connection.
type Conn struct {
 wsConn *websocket.Conn
}
// NewConn establishes a new WebSocket connection to a specified URL.
func NewConn() *Conn {
 c, _, err := websocket.Dial(context.Background(), "ws://localhost:8080/ws", nil)
 if err != nil {
 fmt.Println(err, "ERROR")
 }
return &Conn{
 wsConn: c,
 }
}
func main() {
 // Channel to keep the main function running until it's closed.
 c := make(chan struct{}, 0)
println("WASM Go Initialized")
 // Establish a new WebSocket connection.
 conn := NewConn()
// Register the onButtonClick function in the global JavaScript context.
 js.Global().Set("onButtonClick", onButtonClickFunc(conn))
// Start reading messages in a new goroutine.
 go conn.readMessage()
// Wait indefinitely.
 <-c
}
// onButtonClickFunc returns a js.Func that sends a "HELLO" message over WebSocket when invoked.
func onButtonClickFunc(conn *Conn) js.Func {
 return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 println("Button Clicked!")
 // Send a message through the WebSocket connection.
 err := conn.wsConn.Write(context.Background(), websocket.MessageText, []byte("HELLO"))
 if err != nil {
 log.Println("Error writing to WebSocket:", err)
 }
 return nil
 })
}
// readMessage handles incoming WebSocket messages and updates the DOM accordingly.
func (c *Conn) readMessage() {
 defer func() {
 // Close the WebSocket connection when the function returns.
 c.wsConn.Close(websocket.StatusGoingAway, "BYE")
 }()
for {
 // Read a message from the WebSocket connection.
 messageType, payload, err := c.wsConn.Read(context.Background())
if err != nil {
 // Log and panic if there is an error reading the message.
 log.Panicf(err.Error())
 }
// Update the DOM with the received message.
 updateDOMContent(string(payload))
// Log the message type and payload for debugging.
 log.Println("MessageType: ", messageType)
 log.Println("Payload: ", string(payload))
 }
}
// updateDOMContent updates the text content of the DOM element with the given text.
func updateDOMContent(text string) {
 // Get the document object from the global JavaScript context.
 document := js.Global().Get("document")
 // Get the DOM element by its ID.
 element := document.Call("getElementById", "text")
 // Set the innerText of the element to the provided text.
 element.Set("innerText", text)
}

我们的 wasm.go 文件在编译成 .wasm 可执行文件后,就是我们之前讨论过的 WebSocket 服务的客户端对应文件。它被设置为在网络浏览器中运行,直接与 DOM 接口。

该文件的职责首先是与我们设置的服务器端点(ws://localhost:8080/ws)建立 WebSocket 连接。这是我们的网络服务监听的通信通道,我们已经详细介绍过了。

与此同时,我们还要关注服务器的响应。从 WebSocket 服务器收到的信息会被捕获并用于动态更新网页。

这就是客户端与服务器交互的完整循环–我们的 Go 代码接收服务器消息,然后直接在用户视图(<p id=”text”)中反映变化。

Makefile(生成文件)

.PHONY: build buildwasm run clean serve
# Variables
WASM_DIR := ./wasm
PUBLIC_DIR := ./public
SERVER_FILE := main.go
WASM_SOURCE := $(WASM_DIR)/wasm.go
WASM_TARGET := $(PUBLIC_DIR)/main.wasm
GOOS := js
GOARCH := wasm
# Default rule
all: run
# Run the server
run: serve
# Serve the project
serve: buildwasm
 go run $(SERVER_FILE)
# Build the WebAssembly module
buildwasm:
 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(WASM_TARGET) $(WASM_SOURCE)
# Clean the built artifacts
clean:
 rm -f $(WASM_TARGET)

结论

我们已经了解了如何使用 Go 和 WebAssembly 创建一个实时应用程序。项目的设置很简单:一个 Go 语言的 WebSocket 服务器、一个基本的 HTML 页面和一些编译成 WebAssembly 并在浏览器中运行的 Go 代码。我们已经介绍了如何通过 WebSockets 发送和接收消息,以及如何通过更新 DOM 在网页中反映这些消息。

从设置项目结构到处理客户端和服务器之间的实时通信,所提供的代码示例旨在为你使用 Go 和 WebAssembly 构建自己的应用程序提供一个清晰的起点。这是一个简单的设置,但它为更复杂的项目奠定了基础。

希望这能帮助你开启自己的 Go WebAssembly 之旅。继续构建,继续学习,最重要的是,保持简单。

Github 仓库:https://github.com/doganarif/go-wasm-socket

作者:arif
译自:https://arifdogan.medium.com/

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/37467.html

(0)

相关推荐

发表回复

登录后才能评论