隔了半个月才发出来,咕咕咕
WebSocket 这玩意用的不少了,从 JavaScript 到 Go,用过各种各样的包来实现这玩意的服务器或客户端。然而,直到现在我对这玩意的原理还是模模糊糊的,趁这个机会,好好学一下 WebSocket 的原理
从 HTTP 到 Websocket
二者关系
虽然 WebSocket 区别于 HTTP,是全双工通信协议。但其并非完全独立于 HTTP,而是基于 HTTP 的升级机制
其核心原理,是通过一次 HTTP 的握手,建立持久连接,然后转变为双向二进制帧传输
升级过程
握手请求
首先,客户端应该先发送 HTTP 握手请求
1 | GET /ws |
其中,Connection: Upgrade
和Upgrade: websocket
字段,明确要求协议升级
Sec-WebSocekt-Key
用于服务端生成生成响应密钥,防止中间代理缓存 WebSocket 帧
最后那个字段用于指定协议版本
响应
成功响应的响应头如下所示
1 | 101 Switching Protocols |
这里唯一值得注意的操作,就是这里的密钥生成规则
服务端将客户端发送的Sec-WebSocket-Key
字段内容,拼接在字符串常量后,计算 SHA-1 后,转为 Base64 编码,作为Sec-WebSocket-Accept
字段的内容返回
在 Go 语言中,girilla/websocket
库自动完成此计算,我们并不需要手动处理这块内容
TCP 连接复用
握手完成后,Websocket 接管刚刚完成的 TCP 连接,后续的数据将以 WebSocket 帧格式传输
Go 语言中,存在net/http
包将原始 TCP 连接抽象为http.ResponseWriter
和http.Request
,我们用这两个对象向 Websocket 中读写消息
同时,在girilla/websocket
库中存在Upgrader.Upgrade()
方法,从http.ResponseWriter
中提取底层的 TCP 连接net.Conn
,并切换为 WebSocket 协议处理器
Go 语言实例
以下提供一个简单的 Go 实现的 WebSocket 服务器:
1 | package main |
来看代码
依赖
1 | import ( |
log
:用于记录日志(虽然但是,Go 的日志系统还是依托)net/http
:提供 HTTP 服务器功能github.com/gorilla/websocket
:提供 WebSocket 协议的实现
实例化 WebSocket 升级器
1 | var upgrader = websocket.Upgrader{ |
将升级的关键函数Upgrader
通过upgrader
实例化,并重写了CheckOrigin
字段中的函数
这玩意是用来控制跨域请求的,默认只允许同源请求,即 WebSocket 请求的 Origin 头与服务器主机完全匹配时,连接才会被允许,即
- 相同的协议(http/https)
- 相同的主机名
- 相同的端口号
所以,不管时调试还是生产环境,一般都是要重写的
处理 WebSocket 连接
1 | func handleWebSocket(w http.ResponseWriter, r *http.Request) { |
我们用实例化的upgrader.Upgrade
,拿到 HTTP 中的 TCP 读写,然后交给 WebSocket 控制,使其升级为 WebSocket 连接conn
吐槽一下 Go 的 err 机制,太繁琐了
记得使用defer conn.Close
确保函数结束时连接关闭,释放资源
消息处理循环
1 | for { |
使用conn
的ReadMessage
方法,从 WebSocket 客户端中读取消息,返回消息类型和内容
这里的WriteMessage
方法可以将消息写入 WebSocket 客户端,这里为了演示,直接将接收到的消息原样返回了
启动 HTTP 服务器
1 | func main() { |
这里使用http.HandleFunc
方法,将/ws
路径的请求交给handleWebSocket
处理,并使用http.ListenAndServe
方法,将服务启动在本机 8080 端口