基于 Go 与 golang.org/x/net/websocket 构建实时 Web 应用:WebSocket 握手原理与双向通信实战(build-web-application-with-golang 第 8.2 节)
基于 Go 与 golang.org/x/net/websocket 构建实时 Web 应用:WebSocket 握手原理与双向通信实战(build-web-application-with-golang 第 8.2 节)
WebSocket 是 HTML5 引入的全双工通信协议,它让浏览器与服务器只需一次握手即可建立长连接,从而摆脱传统 HTTP 轮询带来的带宽浪费,是即时消息、在线游戏等实时 Web 应用的核心基础设施。本文以《build-web-application-with-golang》第 8.2 节(es/08.2.md)为主线,完整讲解 WebSocket 的握手原理、Sec-WebSocket-Key 签名计算过程,并给出基于 Go 官方 golang.org/x/net/websocket 包的服务端与浏览器客户端实战代码,读完后你将能够独立搭建一个可运行的 WebSocket 回声(Echo)服务。
为什么需要 WebSocket:从轮询到全双工
在 WebSocket 诞生之前,实现即时通信的主流技术是“轮询”(polling):浏览器以固定的时间间隔向服务器发送 HTTP 请求,服务器在收到请求后返回最新数据供浏览器刷新。这种方案的缺点是浏览器必须持续不断地向服务器发起请求,每个请求都带有完整的 HTTP 头部开销,会占用大量带宽,且实时性受轮询间隔限制。
WebSocket 采用特殊的握手报头,使浏览器与服务器只需完成一次握手即可建立一条连接通道,且该连接在生命周期内始终保持活动状态。之后你可以像使用常规 TCP Socket 一样,用 JavaScript 向这条连接写入数据或从中读取数据。相比传统 HTTP,它带来三个直接优势:
- 一个 Web 客户端只建立一个 TCP 连接,避免反复建连的开销;
- WebSocket 服务端可以主动推送(push)数据到 Web 客户端,服务端不再只能被动响应;
- 更轻量级的头部,减少每次传输的数据量。
WebSocket 的 URL 以 ws:// 开头,加密版本使用 wss://(基于 SSL)。下图展示了 WebSocket 的通信过程:浏览器发出携带特定报头的 HTTP 握手请求,服务器回应后连接建立,之后两端便可通过 JavaScript 使用该套接口,并由事件处理器异步接收数据。
WebSocket 协议原理与握手细节
数据帧结构
WebSocket 的协议本身并不复杂。在第一次握手通过后连接便建立成功,其后的通讯数据均以 \x00 开头、以 \xFF 结尾。对于客户端而言,这一前缀与后缀是透明的——WebSocket 组件会自动将原始数据“掐头去尾”,你最终拿到的就是纯净的业务数据。
握手(Handshaking)过程
WebSocket 连接由浏览器发起请求、服务器回应确认,这一过程通常被称为“握手”(handshaking)。下图展示了真实的握手请求与响应报文:请求方法为 GET,状态码为 101 Switching Protocols,请求头中包含 Upgrade: websocket、Sec-WebSocket-Key、Sec-WebSocket-Version: 13 等字段,服务器则在响应头中返回 Sec-WebSocket-Accept。
Sec-WebSocket-Accept 的计算过程
握手的关键校验字段是 Sec-WebSocket-Key。它是客户端随机生成、经 base64 编码的一串数据。服务器端接收到该请求后,需要把它与一个固定字符串拼接,这个固定字符串是协议规定的 GUID:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
假设客户端发来的 Sec-WebSocket-Key 是 f7cb4ezEAl6C3wRaU6JORA==,那么服务器将其与固定字符串拼接,得到:
f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
随后对该字符串先用 SHA1 安全散列算法计算出二进制值,再用 base64 编码,即得到握手确认字符串:
rE91AJhfC+6JdVcVXOGJEADEJdQ=
服务器把该值作为响应头 Sec-WebSocket-Accept 的值反馈给客户端,客户端校验通过后,双向连接即宣告建立。这一整套校验逻辑如今已由各类 WebSocket 库内置实现,但在理解协议层面,掌握这个计算链路仍然很有价值——它是 WebSocket 握手的“签名”本质。
Go 实现 WebSocket:客户端与服务端实战
安装依赖
Go 语言标准库本身没有提供对 WebSocket 的支持,但由官方维护的 go.net 子包中提供了 websocket 包。可以通过 go get 安装:
go get golang.org/x/net/websocket
说明:本文基于该书中介绍的
golang.org/x/net/websocket包进行讲解;这也是当前仓库各语言版本第 8.2 节(es/08.2.md、zh/08.2.md、en/08.2.md 等)共同采用的实现方案。该包提供的websocket.Conn、websocket.Handler与websocket.Message.Send/Receive是下面示例的核心 API。
客户端:基于 JavaScript 的浏览器端
WebSocket 分为客户端与服务端。下面的示例实现了一个简单的回声测试:用户在客户端输入信息,通过 WebSocket 发送给服务器端;服务器端收到信息后主动将信息推送回客户端,客户端再把收到的信息输出到控制台。
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
window.onload = function() {
console.log("onload");
sock = new WebSocket(wsuri);
sock.onopen = function() {
console.log("connected to " + wsuri);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}
sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};
function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>
可以看到,客户端仅用原生 JavaScript 的 new WebSocket(wsuri) 就建立了一个与服务器的连接 sock。当握手成功后,会触发 WebSocket 对象的 onopen 事件,告知客户端连接已成功建立。打开连接的客户端通常会绑定以下四个事件:
- 1)onopen:建立连接后触发;
- 2)onmessage:收到消息后触发;
- 3)onerror:发生错误时触发;
- 4)onclose:关闭连接时触发。
服务端:Go 回声服务器
服务器端的实现同样简洁:将 Echo 函数包装为 websocket.Handler 并注册到根路径,然后在 :1234 端口启动 HTTP 服务。Echo 内部是一个无限循环——通过 websocket.Message.Receive 接收客户端发来的消息,再通过 websocket.Message.Send 把拼接后的应答发回客户端;一旦接收或发送出错便跳出循环:
package main
import (
"golang.org/x/net/websocket"
"fmt"
"log"
"net/http"
)
func Echo(ws *websocket.Conn) {
var err error
for {
var reply string
if err = websocket.Message.Receive(ws, &reply); err != nil {
fmt.Println("Can't receive")
break
}
fmt.Println("Received back from client: " + reply)
msg := "Received: " + reply
fmt.Println("Sending to client: " + msg)
if err = websocket.Message.Send(ws, msg); err != nil {
fmt.Println("Can't send")
break
}
}
}
func main() {
http.Handle("/", websocket.Handler(Echo))
if err := http.ListenAndServe(":1234", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
整个数据流是:客户端把用户输入的信息 Send 出去 → 服务器端通过 Receive 接收到相应信息 → 服务器端通过 Send 发送应答信息 → 客户端的 onmessage 事件被触发并输出结果。下图是服务器端控制台的实际运行日志:可以看到客户端发来的 Hello, world! 被接收、并回发了 Received: Hello, world! 的过程。
运行步骤
- 在 Go 环境中执行
go get golang.org/x/net/websocket安装依赖; - 将上述服务端代码保存为
main.go,运行go run main.go,服务器即在:1234端口监听; - 用浏览器打开包含客户端 HTML/JS 的页面,在输入框中输入消息并点击
Send Message; - 观察浏览器控制台(
message received: Received: ...)与服务器控制台日志,即可验证双向通信已建立。
仓库视角:章节定位与延伸阅读
在《build-web-application-with-golang》一书的章节规划中,第 8 章“Web 服务”(es/08.0.md)依次讲解了 Socket 编程(8.1)、WebSocket(本文)、REST(8.3)与 RPC(8.4),共同构成 Go 语言构建网络服务的完整图景。其中 WebSocket 章节的定位,是解决传统 HTTP 响应模式在实时场景下的性能瓶颈——正如第 8 章导读所述,当时许多实时服务器已借助 Socket 编程来弥补 HTTP 的慢速响应,而随着 HTML 的快速发展,WebSocket 正被众多网游公司采用。
从仓库结构看,本节属于纯文档讲解章节:es/code 目录(es/code/readme.md)中保存的是第 1、2 章的基础示例代码(如 ch.1.2/main.go),并未附带 WebSocket 的可运行源码;但 8.2 节给出的客户端与服务端代码是完整、自洽的,足以直接复制运行。与此同时,本节的讲解在英文(en/08.2.md)、中文(zh/08.2.md)、德文(de/08.2.md)、日文(ja/08.2.md)等多个语言版本中保持一致,内容均围绕同一套握手原理与 Echo 示例展开,便于对照学习。
小结
通过本节,你可以掌握:WebSocket 相对 HTTP 轮询的核心优势(单连接、服务端推送、轻量头部);握手机制中 Sec-WebSocket-Key → 拼接 GUID → SHA1 → base64 → Sec-WebSocket-Accept 的完整计算链路;以及基于 Go 官方 golang.org/x/net/websocket 包实现回声服务的客户端与服务器端代码。这套知识是后续深入 REST、RPC 以及构建实时 Web 应用的基础——书中也指出,随着 HTML5 的发展,WebSocket 在现代化 Web 开发中将扮演越来越重要的角色,储备这方面的知识十分必要。


