用 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