Skip to content

WebSocket(双向通信) ​

WebSocket 插件为页面提供一个由 Go 建立连接的 WebSocket,其 API 与 浏览器的一致。与 webview 自带的那个不同,它:

  • 在打开握手时发送任何请求头,例如 Authorization、Cookie 或 Origin,以满足希望在请求头中携带令牌(server)的服务器;
  • 不受页面的 Content Security Policy 限制;
  • 当页面落后时,把快速服务器的消息保存在 Go 中,而不是页面的内存里。

设置 ​

在 Go 中使用该插件:

go
import (
	"github.com/egoist/mygo"
	"github.com/egoist/mygo/plugins/websocket"
)

func main() {
	mygo.Use(websocket.Plugin)
	// ...
}

并在前端添加它的包:

sh
bun add @mygo-plugins/websocket

在一个未使用其 Go 侧组件的应用中打开连接会失败,并且控制台会提示要 调用 mygo.Use。

建立连接 ​

从包中导入 WebSocket,像使用浏览器的那个一样使用它:

ts
import { WebSocket } from "@mygo-plugins/websocket";

const ws = new WebSocket("wss://example.com/live", ["v1"], {
  headers: { authorization: `Bearer ${token}` },
});
ws.binaryType = "arraybuffer";
ws.onopen = () => ws.send(JSON.stringify({ subscribe: "prices" }));
ws.onmessage = (e) => console.log(e.data);
ws.onclose = (e) => console.log(e.code, e.reason, e.wasClean);

前两个参数与浏览器的 WebSocket 相同:URL,ws: 或 wss:(http: 和 https: 会转换为它们,相对 URL 相对于页面的 URL 解析),以及子协议, 服务器会从中选择一个,连接打开后即为 ws.protocol。第三个是插件的: headers,打开握手的请求头,可以是 Headers、对象或键值对。

其余就是浏览器的 API:readyState 及其常量,send 字符串、 ArrayBuffer、类型化数组和 Blob,close(code, reason),binaryType (默认为 "blob",或 "arraybuffer"),以及 open、message、 error 和 close 事件,既可作为 on… 属性,也可用 addEventListener。

接受 WebSocket 类的库——GraphQL 和实时客户端常常如此——用两个参数 创建连接;给它们一个负责添加请求头的类:

ts
class AuthWebSocket extends WebSocket {
  constructor(url: string | URL, protocols?: string | string[]) {
    super(url, protocols, { headers: { authorization: `Bearer ${token}` } });
  }
}

发送与接收 ​

  • send 在连接仍在打开时抛出异常,连接关闭后发送的消息会被丢弃,与 浏览器的一致。消息按发送顺序发出。
  • bufferedAmount 统计已传给 send 但 Go 尚未写出的字节数:大量发送 的页面可以等待它下降。
  • 当页面还有超过 1 MiB 的消息尚未处理时,来自服务器的消息在 Go 中 等待,因此比页面更快的服务器不会填满页面的内存。大于 MaxMessageSize(默认 64 MiB)的消息会使连接失败。
  • Go 会响应服务器的 ping。

关闭 ​

close(code, reason) 接受 1000 或 3000 到 4999 之间的状态码,以及最多 123 字节的原因说明,与浏览器的一致。随后 Go 等待服务器完成关闭握手: 完成时,close 事件带有服务器的状态码和原因说明,wasClean 为 true; 超过 CloseTimeout(默认 5 秒)仍未完成时,Go 断开连接,事件的状态码 为 1006。

当页面导航离开或其窗口关闭时,连接会自行关闭,并告知服务器它正在离开 (状态码 1001)。

当连接失败——因为无法到达服务器、服务器应答握手时没有升级、重定向了 握手或破坏了协议——页面会收到一个 error 事件,随后是状态码为 1006 的 close 事件,并且控制台会说明原因,与浏览器的一致。

与浏览器的 WebSocket 的区别 ​

  • 构造函数接受第三个参数,即握手的请求头。
  • 页面的 Content Security Policy(connect-src)不适用,webview 的 Cookie 也不适用:适用的是 Go 侧 http.Client 的 Cookie(参见 选项)。
  • 不协商任何扩展,例如 permessage-deflate:extensions 始终为空。

选项 ​

websocket.Plugin 通过系统的代理设置向任何 URL 打开连接(参见 代理)。websocket.New 则接受选项:

go
mygo.Use(websocket.New(websocket.Options{
	Client: &http.Client{Transport: transport},
	Allow: func(r *http.Request) bool {
		return r.URL.Hostname() == "live.example.com"
	},
	MaxMessageSize: 16 << 20,        // 16 MiB; default 64 MiB
	CloseTimeout:   2 * time.Second, // default 5 seconds
}))
  • Client 发出打开握手,因此它的 Transport(代理、TLS 设置)和 Jar(Cookie)会生效。它的 Timeout 和 CheckRedirect 会被忽略: 连接会长期保持,而重定向会使连接失败,与浏览器中一样。
  • Allow 看到每个连接的握手请求,带着它的 URL 和请求头,并决定页面 是否可以打开它;被它拒绝的连接会失败。
  • MaxMessageSize 是服务器的单条消息最多可拥有的字节数;更大的消息会 使连接失败。
  • CloseTimeout 是页面关闭的连接等待服务器完成关闭握手的时长。

只有应用自己的页面可以调用该插件,与每个绑定方法一样(参见谁可以 调用)。