说明
本文主要描述WebSocket协议及请求响应交互过程,最后通过go实现一个WebSocket服务端的例子
WebSocket消息格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
标志位说明
-
FIN: 1 bit
0-表示后续还有帧;1-表示这是最后一帧。第一帧也可能是最后一帧。 -
RSV1、RSV2、RSV3:1 bit each
预留位,通常设为 0,除非扩展定义了使用 -
Opcode: 4 bits
表示帧的类型,目前定义类型有:-
%x0 denotes a continuation frame 继续帧用于延续之前的帧。当一条消息被分割成多个帧时,除了第一个帧之外的所有帧都应该使用 Opcode = 0。
-
%x1 denotes a text frame 文本帧,用于传输文本数据(UTF-8编码)
-
%x2 denotes a binary frame 二进制帧,用于传输二进制数据
-
%x3-7 are reserved for further non-control frames 保留帧,用于非控制帧
-
%x8 denotes a connection close 关闭帧,用于表示关闭连接
-
%x9 denotes a ping Ping帧 用于心跳检测 收到一个ping帧,必须发送一个Pong帧作为响应,除非已经接收了一个Close帧。 Ping帧可以用来做服务探测保活
-
%xA denotes a pong Pong帧 用于对ping帧的响应
-
%xB-F are reserved for further control frames 保留帧,用于控制帧
-
-
Mask: 1 bit
掩码标志,如果设置为1,表示需要对数据做掩码处理,出于安全考虑,WebSocket协议规定所有从client到server的数据都要进行掩码处理。server端往client端则不做要求。 -
Payload length:7 bits 7位表示, 7+16 bits 7+16位表示, or 7+64 bits 7+64位表示 负载长度
frame-payload-length = ( %x00-7D )
/ ( %x7E frame-payload-length-16 )
/ ( %x7F frame-payload-length-63 )
; 7, 7+16, or 7+64 bits in length,
; respectively
frame-payload-length-16 = %x0000-FFFF ; 16 bits in length
frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
; 64 bits in length
-
Masking-key: 0 or 4 bytes 所有client端发往server的数据都会进行掩码处理,掩码是通过与32为掩码键(Masking-key)异或(XOR)运算完成的 。Masking-key由客户端按照一定规则随机生成并发送服务端,服务端通过掩码键进行解码处理
-
Payload data: (x+y) bytes
包含 Extension data和Application data -
Extension data: x bytes 扩展数据通常为0字节,除非协商指定长度
-
Application data: y bytes 应用程序数据
分段传输
WebSocket支持消息的分段传输,分片的主要目的是允许在消息开始时发送大小未知的消息,而无需缓冲该消息。如果消息不能分片,那么端点将不得不缓冲整个消息,以便在发送第一个字节之前计算其长度。通过分片,服务器或中间件可以选择一个合理大小的缓冲区,并在缓冲区满时,将片段写入网络。分片的第二个场景是多路复用扩展需要。
一个分片消息由单一帧组成。通过Opcode和Fin标志位来表示连续的分片。示例:对于作为一个文本消息分成三个片段发送的情况,第一个片段的Opcode将是0x1,FIN位被清除;第二个片段的Opcode将是0x0,FIN位被清除;第三个片段的Opcode将是0x0,并且FIN位被设置。
- 控制帧 可以在插入在任何两个分片消息的中间。控制帧通常用于传输重要的控制信息,比如心跳检测(Ping/Pong帧)、关闭连接(Close帧)等。这些信息对维持连接的稳定性和正确性至关重要,因此它们可以打断正在进行的数据传输。需要注意,控制帧本身不能被分片。所有Control Frames的payload长度必须是小于等于125字节
- 分片消息 必须按顺序发送。一段连续的分片消息中间不能插入另外的分片消息,除非事先协商约定好了这么做。
连接说明
WebSocket建立连接是基于Http的握手过程,客户端通过发送一个HTTP GET请求到服务器来发起WebSocket连接。这个请求包含特定的HTTP头字段,表明这是一个WebSocket升级请求。
Http请求头字段
- Connection:upgrade
- Upgrade:websocket
- Sec-Websocket-Version:13
- Sec-Websocket-Extensions: 这个头字段用于客户端声明它支持的WebSocket扩展。扩展可以改变WebSocket协议的行为,例如通过压缩消息或添加自定义规则
- Sec-Websocket-Key:这是一个由客户端生成的随机值,用于安全地验证WebSocket握手。服务器接收这个密钥后,会使用它来生成一个"Sec-WebSocket-Accept"响应头的值。
- Sec-Websocket-Protocol:这个头字段允许客户端指定它希望使用的子协议。WebSocket协议可以支持多个子协议,客户端可以通过这个头字段声明它支持的子协议列表。
服务器响应
服务器接收到HTTP请求后,会检查请求头字段,确认它是一个WebSocket升级请求
如果服务器同意升级连接,它会发送一个HTTP 101 Switching Protocols响应。
响应头字段
- Upgrade: websocket:确认服务器同意升级到WebSocket协议。
- Connection: Upgrade:确认连接类型将从HTTP升级。
- Sec-WebSocket-Accept:服务器根据客户端发送的Sec-WebSocket-Key生成的值,通过SHA-1哈希然后Base64编码得到。
使用Go gorilla实现Websocket服务端
启动文件
package main
import (
"goLearning/gin/example/router"
)
func main() {
r := router.InitRouter()
r.Run(":18080") // 监听并在 0.0.0.0:8080 上启动服务
}
使用gin http路由功能
package router
import (
"github.com/gin-gonic/gin"
"goLearning/gin/example/controller/ws"
)
func InitRouter() *gin.Engine {
r := gin.Default()
r.GET("/echo", ws.Echo)
gin.Recovery()
return r
}
使用gorilla实现websocket功能
package ws
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"log"
)
var upgrader = websocket.Upgrader{} // use default options
func Echo(context *gin.Context) {
//http协议升级为websocket协议
conn, err := upgrader.Upgrade(context.Writer, context.Request, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer conn.Close()
for {
//默认情况下,接收一个ping事件,会自动回复一个pong帧。也可以通过监听ping事件,自定义回复内容
conn.SetPingHandler(func(appData string) error {
log.Printf("Received ping: %s", appData)
return conn.WriteControl(websocket.PongMessage, []byte("hello..."), time.Now().Add(time.Second))
})
//这里可以在关闭事件添加处理逻辑
conn.SetCloseHandler(func(code int, text string) error {
log.Println("code : %s", code, " text: %s", text)
return nil
})
if err != nil {
return
}
//接受消息
mt, message, err := conn.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
//发送消息
err = conn.WriteMessage(mt, append([]byte("hello "), message...))
if err != nil {
log.Println("write:", err)
break
}
}
}
更加详细例子及使用说明可以查看gorilla官网
https://pkg.go.dev/github.com/gorilla/websocket#section-readme
抓包分析
后续补充…