
隔了半个月才发出来,咕咕咕
WebSocket 这玩意用的不少了,从 JavaScript 到 Go,用过各种各样的包来实现这玩意的服务器或客户端。然而,直到现在我对这玩意的原理还是模模糊糊的,趁这个机会,好好学一下 WebSocket 的原理
从 HTTP 到 Websocket
二者关系
虽然 WebSocket 区别于 HTTP,是全双工通信协议。但其并非完全独立于 HTTP,而是基于 HTTP 的升级机制
其核心原理,是通过一次 HTTP 的握手,建立持久连接,然后转变为双向二进制帧传输
升级过程
握手请求
首先,客户端应该先发送 HTTP 握手请求
GET /ws HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 随机 Base64 字符串
Sec-WebSocket-Version: 13
其中,Connection: Upgrade和Upgrade: websocket字段,明确要求协议升级
Sec-WebSocekt-Key用于服务端生成生成响应密钥,防止中间代理缓存 WebSocket 帧
最后那个字段用于指定协议版本
响应
成功响应的响应头如下所示
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 基于客户端密钥生成
这里唯一值得注意的操作,就是这里的密钥生成规则
服务端将客户端发送的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 服务器:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("WebSocket 升级失败:", err)
return
}
defer conn.Close()
log.Println("客户端已连接:", conn.RemoteAddr())
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("读取消息失败:", err)
break
}
log.Printf("收到消息: %s", message)
if err := conn.WriteMessage(messageType, message); err != nil {
log.Println("发送消息失败:", err)
break
}
}
log.Println("客户端已断开:", conn.RemoteAddr())
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
log.Println("WebSocket 服务器已启动,监听端口 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("服务器启动失败:", err)
}
}
来看代码
依赖
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
log:用于记录日志(虽然但是,Go 的日志系统还是依托)net/http:提供 HTTP 服务器功能github.com/gorilla/websocket:提供 WebSocket 协议的实现
实例化 WebSocket 升级器
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
将升级的关键函数Upgrader通过upgrader实例化,并重写了CheckOrigin字段中的函数
这玩意是用来控制跨域请求的,默认只允许同源请求,即 WebSocket 请求的 Origin 头与服务器主机完全匹配时,连接才会被允许,即
- 相同的协议(http/https)
- 相同的主机名
- 相同的端口号
所以,不管时调试还是生产环境,一般都是要重写的
处理 WebSocket 连接
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("WebSocket 升级失败:", err)
return
}
defer conn.Close()
...
}
我们用实例化的upgrader.Upgrade,拿到 HTTP 中的 TCP 读写,然后交给 WebSocket 控制,使其升级为 WebSocket 连接conn
吐槽一下 Go 的 err 机制,太繁琐了
记得使用defer conn.Close确保函数结束时连接关闭,释放资源
消息处理循环
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("读取消息失败:", err)
break
}
log.Printf("收到消息: %s", message)
if err := conn.WriteMessage(messageType, message); err != nil {
log.Println("发送消息失败:", err)
break
}
}
使用conn的ReadMessage方法,从 WebSocket 客户端中读取消息,返回消息类型和内容
这里的WriteMessage方法可以将消息写入 WebSocket 客户端,这里为了演示,直接将接收到的消息原样返回了
启动 HTTP 服务器
func main() {
http.HandleFunc("/ws", handleWebSocket)
log.Println("WebSocket 服务器已启动,监听端口 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("服务器启动失败:", err)
}
}
这里使用http.HandleFunc方法,将/ws路径的请求交给handleWebSocket处理,并使用http.ListenAndServe方法,将服务启动在本机 8080 端口