websocket基本使用

参考1:使用Go语言创建WebSocket服务器和客户端WebSocket 是一种新型的网络通信协议,可以在 Web 应用程序中 - 掘金

参考2:websocket的连接原理WebSocket 是一种全双工通信协议,基于 TCP 连接实现,允许客户端和服务器之间建立 - 掘金

参考3:RFC 6455: The WebSocket Protocol | RFC Editor

我们来写一个最简单的golang服务端:

Go 复制代码
package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func main() {
	http.HandleFunc("/wspath", handleWebSocket)
	log.Println("Listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	defer conn.Close()

	for {
		messageType, p, err := conn.ReadMessage()
		if err != nil {
			log.Println("ReadMessage: ", err)
			return
		}
		log.Println("Received message:", string(p))
		err = conn.WriteMessage(messageType, []byte("hello client"))
		if err != nil {
			log.Println("WriteMessage: ", err)
			return
		}
	}
}

再写一个最简单的网页作为客户端:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebSocket Client</title>
</head>
<body>
<div id="root">
    <input id="input" type="text" value="hello, server" />
    <button id="send">Send</button>
    <div id="output"></div>
</div>
<script>
    const ws = new WebSocket("ws://127.0.0.1:8080/wspath"); // 新建对象等于连接
    ws.onopen = () => {
        console.log("Connected!");
    }
    ws.onmessage = (e) => {
        console.log("Received: " + e.data);
        document.querySelector("#output").innerHTML = e.data
    }
    ws.onclose = (e) => {
        console.log("Closed: " + e.data);
    }
    ws.onerror = (e) => {
        console.log("Error: " + e.data);
    }
    document.querySelector("#send").addEventListener("click", (e) => {
        const data = JSON.stringify({
            'type': 'text',
            'text': document.getElementById('input').value
        })
        console.log("To Send: " + data);
        ws.send(data)
    })
</script>
</body>
</html>

测试方式:go run main.go 运行服务端,再用浏览器打开网页客户端(文件方式打开就能工作),点击网页客户端按钮发送消息到服务端。

服务端的显示如下:(步骤4:服务器在接收客户消息后发送消息给客户)

bash 复制代码
d:\GoPath\src\websocketTest>go run main.go
2026/10/08 16:06:28 Listening on :8080     步骤0:服务器监听指定端口
2026/10/08 16:06:52 Received message: {"type":"text","text":"hello, server"} 步骤3:接收到客户消息
2026/10/08 16:21:17 websocket: close 1001 (going away) 步骤7:关闭连接

网页客户端控制台的显示如下:(步骤6:关闭网页=客户主动关闭连接)

bash 复制代码
client.html:16 Connected!  步骤1:打开网页连接上服务端
client.html:33 To Send: {"type":"text","text":"hello, server"}  步骤2:发送消息到服务端
client.html:19 Received: hello client 步骤5:收到服务端消息

如果服务端强制退出,网页客户端控制台显示:Closed: undefined

上面的流程中,服务端表现得还是比较被动的,即它只是在收到客户端消息后对应回复一条消息。但实际websocket是完全双向的长连接。我们插入以下代码来观察服务端主动给客户端发送消息(每间隔几秒就把时间发送给客户端):

Go 复制代码
//.........................................
	defer conn.Close()

	go func() {
		for {
			time.Sleep(5 * time.Second)
			err := conn.WriteMessage(websocket.TextMessage, []byte(time.Now().Format(time.RFC3339)))
			if err != nil {
				log.Println("WriteMessage time:", err)
				return
			}
		}
	}()

	for {
//.........................................

这样,服务端会每隔几秒就把时间发给客户端(只要客户端连接了),无论客户端有没有主动发送消息。我们的处理是简化的,所以客户端接收到服务端的hello, client还是当前时间,都是显示在div#output 中的。真实的处理中,应该自己设计消息格式,如JSON中用 msgtype 之类的字段区分消息类别,然后 ws.onmessage 中解析JSON并分门别类处理消息,如显示到不同的位置。

另外要注意的一点是,如果刷新页面,服务端会输出错误信息

bash 复制代码
2026/10/09 08:48:31 ReadMessage:  websocket: close 1001 (going away)
2026/10/09 08:48:33 WriteMessage time: websocket: close sent

也就是服务端认为客户断开连接了,所以关闭了对应连接。这时,会发现页面上仍然能收到时间戳消息,这是因为刷新页面和新打开页面一样,都会new一个WebSocket对象,本质上就是新建了一个连接,对服务端来说,http.HandleFunc 为每个连接创建了 go routine,客户端收到的时间戳是新的go routine 中的 handleWebSocket (其中的 go func)发送给它的。(可以回顾 golang web学习随便记1-快速入门_golang 背记手册-CSDN博客 和 golang学习随便记11-goroutine和channel(3)_golang os.stdin.read(make(\[\]byte, 1))-CSDN博客)

我们用WireShark来学习以下WebSocket的原理:因为我们是发到本机,所以,适配器选择loopback,过滤器设定为 tcp.port==8080 (如果设置为 websocket and tcp.port==8080 那就只能看到upgrade之后的通信了)

先是TCP握手过程:客户端端口61006 to 服务端端口8080 SYN;8080 to 61006 SYN,ACK; 61006 to 8080 ACK。

然后是 HTTP GET /wspath HTTP/1.1:和一般的HTTP GET请求相比,值得关注的头部字段是Connection 、Upgrade 、Sec-WebSocket-Version 、Sec-WebSocket-Key 、Sec-WebSocket-Extensions。

bash 复制代码
    GET /wspath HTTP/1.1\r\n
    Host: 127.0.0.1:8080\r\n
    Connection: Upgrade\r\n
    Pragma: no-cache\r\n
    Cache-Control: no-cache\r\n
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0\r\n
    Upgrade: websocket\r\n
    Origin: null\r\n
    Sec-WebSocket-Version: 13\r\n
    Accept-Encoding: gzip, deflate, br, zstd\r\n
    Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\r\n
    Sec-WebSocket-Key: xyKtSMPj/xsIpMarwN7eEg==\r\n
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n
    \r\n

服务端响应 TCP 8080 to 61006 ACK 后是 HTTP响应101 Switching Protocols 切换协议 :其中 Sec-WebSocket-Accept 的值是根据客户端发送的 Sec-WebSocket-Key 值计算得到,计算方式是后者拼接固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11后计算SHA1散列的二进制值(20字节),再用BASE64编码得到前者。

bash 复制代码
    HTTP/1.1 101 Switching Protocols\r\n
    Upgrade: websocket\r\n
    Connection: Upgrade\r\n
    Sec-WebSocket-Accept: hIb4DHYsBmjOKIAnwJ2sh5kXtxY=\r\n
    \r\n

客户端 TCP 61006 to 8080 ACK 响应。服务端发送 WebSocket 帧(下图中81 19开始的部分):1bit Finish标记,3bit 扩展保留,4bit Opcode操作码类型;1bit 掩码指示,7bit 负载长度(如果是126,就是7+16bit 负载长度,是127就是7+64bit 负载长度);掩码key,没有或者4字节;有效负载数据(应用数据,之前可能有扩展数据)。81 19 = 1/000/ 0001/ 0/001 1001/ = 这是消息帧最后片段,操作码0x1表示这是文本帧,无掩码key,负载长度25字节(2026-10-09T10:09:53+08:00的长度)

bash 复制代码
0000   02 00 00 00 45 00 00 43 1e 25 40 00 80 06 00 00   ....E..C.%@.....
0010   7f 00 00 01 7f 00 00 01 1f 90 ee 4e fb 5a 9a 47   ...........N.Z.G
0020   17 e8 45 46 50 18 27 f9 59 81 00 00 81 19 32 30   ..EFP.'.Y.....20
0030   32 36 2d 31 30 2d 30 39 54 31 30 3a 30 39 3a 35   26-10-09T10:09:5
0040   33 2b 30 38 3a 30 30                              3+08:00

客户端发送的 WebSocket 帧(下图中 81 a6开始的部分),带掩码:81 a6 = 1/000/ 0001/ 1/010 0110 = FIN最后片段,0x1文本帧,有掩码,负载长度 0b10 0110=0x26=38字节。掩码key=df 13 39 e1。负载数据 a4 31 4d 98 ... 5c 93 fd 6e 共38字节。我们把负载数据按字节和掩码key进行异或操作就可以得到实际负载了(例如 df XOR a4=7b, 13 XOR 31=22 分别是{和"的ASCII码)。

bash 复制代码
0000   02 00 00 00 45 00 00 54 1e 45 40 00 80 06 00 00   ....E..T.E@.....
0010   7f 00 00 01 7f 00 00 01 ee 4e 1f 90 17 e8 45 46   .........N....EF
0020   fb 5a 9a 62 50 18 27 f8 bb 40 00 00 81 a6 df 13   .Z.bP.'..@......
0030   39 e1 a4 31 4d 98 af 76 1b db fd 67 5c 99 ab 31   9..1M..v...g\..1
0040   15 c3 ab 76 41 95 fd 29 1b 89 ba 7f 55 8e f3 33   ...vA..)....U..3
0050   4a 84 ad 65 5c 93 fd 6e                           J..e\..n

上面2段都是数据帧,除了数据帧还有控制帧(用操作码区分),如关闭0x8,心跳ping 0x9,心跳pong 0xa。

从HTTP到WebSocket,协议升级请求是客户端主动发起的(用HTTP GET请求),服务端用 gorilla/websocket 的 websocket.Upgrader.Upgrade()方法处理这个请求时,会对 GET 请求头部中的信息进行必要检查,之后先接管 w 这个 http.ResponseWriter 对应的连接,再根据其创建("改造成")新的连接。(具体可以参考 gorilla/websocket 库中 server.go、conn.go)

Vue项目中刷新页面导致的 WebSocket 断开连接,是否需要重新连接可以参考 关于在vue项目中,刷新页面时websocket断开连接的解决办法_websocket页面刷新会断开连接吗-CSDN博客

采用了类似认定已经登录,那么刷新页面就重新连接的思路。对于服务端来说,要认定消息来源,还是需要采用类似会话控制的方式才能在新的连接中继续会话。

相关推荐
梦帮科技3 小时前
端侧编译原理:TVM / MLIR 计算图模式匹配、算子融合与显存生命周期复用
网络·人工智能·深度学习·神经网络·自然语言处理·cnn·mlir
dadaobusi4 小时前
学习: libfabric 2.0
网络
xiaoye-duck4 小时前
《Linux 网络编程》深入理解 epoll(下):epoll 实战开发与 LT/ET 触发模式深度剖析
linux·网络
小鹿的周先生5 小时前
第18章-MCP
服务器·网络·人工智能
Da Da 泓5 小时前
谈谈HTTP
网络·网络协议·http
要吃这碗饭5 小时前
YouTube 数据抓取出现 429/403 错误?2026 反爬机制拆解与爬虫选型指南
网络·爬虫·网络协议
YumiProxy5 小时前
代理IP自动切换实战:会话保持与请求头配置避坑指南
服务器·网络·网络协议·tcp/ip·ip
honsor5 小时前
以太网温湿度传感器:RJ45直连机房的环境监控新方案
运维·网络·数据库·物联网·安全·云计算·github
花间相见5 小时前
【计算基础|网络02】HTTP 原理:报文结构、方法语义与状态码
网络·网络协议·http