基于 Go 与 golang.org/x/net/websocket 构建实时 Web 应用:WebSocket 握手原理与双向通信实战(build-web-application-with-golang 第 8.2 节)

原创2026-10-03 18:11:59964 阅读
文章标签:文档教程

基于 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 协议原理与握手细节

数据帧结构

WebSocket 的协议本身并不复杂。在第一次握手通过后连接便建立成功,其后的通讯数据均以 \x00 开头、以 \xFF 结尾。对于客户端而言,这一前缀与后缀是透明的——WebSocket 组件会自动将原始数据“掐头去尾”,你最终拿到的就是纯净的业务数据。

握手(Handshaking)过程

WebSocket 连接由浏览器发起请求、服务器回应确认,这一过程通常被称为“握手”(handshaking)。下图展示了真实的握手请求与响应报文:请求方法为 GET,状态码为 101 Switching Protocols,请求头中包含 Upgrade: websocket、Sec-WebSocket-Key、Sec-WebSocket-Version: 13 等字段,服务器则在响应头中返回 Sec-WebSocket-Accept。

WebSocket 握手请求与响应报文示例:请求携带 Sec-WebSocket-Key,响应返回 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! 的过程。

WebSocket 服务器控制台输出:展示收到客户端消息并回发应答的日志

运行步骤

  1. 在 Go 环境中执行 go get golang.org/x/net/websocket 安装依赖;
  2. 将上述服务端代码保存为 main.go,运行 go run main.go,服务器即在 :1234 端口监听;
  3. 用浏览器打开包含客户端 HTML/JS 的页面,在输入框中输入消息并点击 Send Message;
  4. 观察浏览器控制台(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 开发中将扮演越来越重要的角色,储备这方面的知识十分必要。

继续阅读:上一节 Socket 编程|下一节 REST|返回 目录。

登录后查看全文
build-web-application-with-golang