1.Websocket协议与原理
1.1 连接建立协议
1.1.1 客户端发起连接请求
客户端通过 HTTP 请求发起 WebSocket 连接。以下是一个 WebSocket 握手请求的例子:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
- GET /chat HTTP/1.1:请求使用 GET 方法。
- Host: server.example.com:请求目标主机。
- Upgrade: websocket:表明希望升级到 WebSocket 协议。
- Connection: Upgrade:指示当前的连接请求升级。
- Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==:一个 Base64 编码的随机密钥,用于服务器响应中进行确认。
- Sec-WebSocket-Version: 13:表明 WebSocket 的版本(13 是最新的版本)。
1.1.2 服务器响应
服务器接收到请求后,如果同意升级协议,将发送一个 HTTP 响应来确认连接升级:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
- HTTP/1.1 101 Switching Protocols:表示协议切换成功。
- Upgrade: websocket:确认升级到 WebSocket。
- Connection: Upgrade:确认连接升级。
- Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=:服务器根据客户端的 Sec-WebSocket-Key 生成的一个响应密钥。
1.1.3 连接建立
一旦服务器发回 101 状态码,WebSocket 连接便建立成功。之后,客户端和服务器可以在此连接上相互发送和接收数据帧,而不需要再使用 HTTP 协议的头部。
- 握手过程:WebSocket 握手基于 HTTP,并包含 Upgrade 和 Connection 头部来请求和确认协议的升级。
- Sec-WebSocket-Key:客户端生成的随机密钥,服务器用于验证连接的合法性。
- Sec-WebSocket-Accept:服务器生成的响应密钥,基于客户端提供的 Sec-WebSocket-Key 和固定 GUID 的 SHA-1 和 Base64 编码。
1.2 数据传输协议
- FIN (1 bit): 表示是否为消息的最后一个帧。1 表示是最后一个帧。
- RSV1, RSV2, RSV3 (1 bit each): 保留位,通常设置为 0,除非在扩展中有定义。
- Opcode (4 bits): 表示帧的类型:
- 0x0: 延续帧(continuation frame)
- 0x1: 文本帧(text frame)
- 0x2: 二进制帧(binary frame)
- 0x8: 连接关闭(connection close)
- 0x9: Ping
- 0xA: Pong
- Mask (1 bit): 表示是否应用掩码。客户端发送的帧必须设置为 1,服务器发送的帧必须设置为 0。
- Payload Length (7 bits or 7+16/64 bits): 数据载荷的长度:
- 如果值在 0 到 125 之间,则为数据载荷的实际长度。
- 126: 后接 16 位整数表示长度。
- 127: 后接 64 位整数表示长度。
- Masking-Key (0 or 4 bytes): 掩码键,存在于客户端发送的帧中,用于解码数据载荷。
- Payload Data (x bytes): 实际的应用数据。
1.3 Websocket原理
掩码: 确保客户端发送的数据经过掩码处理,防止恶意数据影响。
控制帧: 必须尽快处理控制帧,避免它们与数据帧混合,导致错误。
Websocket补充-Connection Header头意义
- 标记请求发起方与第一代理的状态
- 决定当前事务完成后,是否会关闭代理
- Connection:keep-alive 不关闭网络
- Connection:close 关闭网络
- Connection:Upgrade 协议升级
2.Websocket代理实战
2.1 基于go构建Websocket测试服务器和客户端
这是一个前后端一体代码:
package mainimport ("flag""html/template""log""net/http""github.com/gorilla/websocket"
)var addr = flag.String("addr", "localhost:2003", "http service address")var upgrader = websocket.Upgrader{} // use default optionsfunc echo(w http.ResponseWriter, r *http.Request) {c, err := upgrader.Upgrade(w, r, nil)if err != nil {log.Print("upgrade:", err)return}defer c.Close()for {mt, message, err := c.ReadMessage()if err != nil {log.Println("read:", err)break}log.Printf("recv: %s", message)err = c.WriteMessage(mt, message)if err != nil {log.Println("write:", err)break}}
}func home(w http.ResponseWriter, r *http.Request) {homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
}func main() {flag.Parse()log.SetFlags(0)http.HandleFunc("/echo", echo)http.HandleFunc("/", home)log.Println("Starting websocket server at " + *addr)log.Fatal(http.ListenAndServe(*addr, nil))
}var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function(evt) {var output = document.getElementById("output");var input = document.getElementById("input");var ws;var print = function(message) {var d = document.createElement("div");d.innerHTML = message;output.appendChild(d);};document.getElementById("open").onclick = function(evt) {if (ws) {return false;}var web_url=document.getElementById("web_url").valuews = new WebSocket(web_url);ws.onopen = function(evt) {print("OPEN");}ws.onclose = function(evt) {print("CLOSE");ws = null;}ws.onmessage = function(evt) {print("RESPONSE: " + evt.data);}ws.onerror = function(evt) {print("ERROR: " + evt.data);}return false;};document.getElementById("send").onclick = function(evt) {if (!ws) {return false;}print("SEND: " + input.value);ws.send(input.value);return false;};document.getElementById("close").onclick = function(evt) {if (!ws) {return false;}ws.close();return false;};});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server,
"Send" to send a message to the server and "Close" to close the connection.
You can change the message and send multiple times.
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="web_url" type="text" value="{{.}}">
<p><input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))
运行后访问:http://localhost:2003/
Open尝试与服务器建立连接
Send基于Websocket向服务器发送了链接
Close关闭连接
2.2 深入理解upgrader.Upgrade
- 获取Sec-Websocket-Key
- sha1生成Sec-WebSocket-Accept
- 向客户端发送101status
// Upgrade 升级 HTTP 服务器连接到 WebSocket 协议。
// responseHeader 包含在响应中,以回应客户端的升级请求。
// 使用 responseHeader 指定 cookies (Set-Cookie) 和应用程序协商的子协议 (Sec-WebSocket-Protocol)。
// 如果升级失败,Upgrade 会用 HTTP 错误响应来回复客户端。
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {const badHandshake = "websocket: the client is not using the websocket protocol: "// 检查 "Connection" 头部是否包含 "upgrade" 令牌if !tokenListContainsValue(r.Header, "Connection", "upgrade") {return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")}// 检查 "Upgrade" 头部是否包含 "websocket" 令牌if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")}// 检查请求方法是否为 GETif r.Method != "GET" {return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")}// 检查 "Sec-Websocket-Version" 头部是否包含版本号 "13"if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")}// 检查 responseHeader 中是否包含 "Sec-Websocket-Extensions",不支持应用程序特定的扩展头if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")}// 检查请求来源是否允许checkOrigin := u.CheckOriginif checkOrigin == nil {checkOrigin = checkSameOrigin}if !checkOrigin(r) {return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")}// 获取 "Sec-Websocket-Key" 头部的值,并检查是否为空challengeKey := r.Header.Get("Sec-Websocket-Key")if challengeKey == "" {return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank")}// 选择协商的子协议subprotocol := u.selectSubprotocol(r, responseHeader)// PMCE (Per-Message Compression Extensions) 协商var compress boolif u.EnableCompression {for _, ext := range parseExtensions(r.Header) {if ext[""] != "permessage-deflate" {continue}compress = truebreak}}// 检查响应是否实现了 http.Hijacker 接口h, ok := w.(http.Hijacker)if !ok {return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")}var brw *bufio.ReadWriter// 通过劫持获得net/IO流netConn, brw, err := h.Hijack()if err != nil {return u.returnError(w, r, http.StatusInternalServerError, err.Error())}// 检查是否有未读数据if brw.Reader.Buffered() > 0 {netConn.Close()return nil, errors.New("websocket: client sent data before handshake is complete")}// 根据情况重用缓冲读取器var br *bufio.Readerif u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {br = brw.Reader}// 获取缓冲写入器buf := bufioWriterBuffer(netConn, brw.Writer)// 根据情况重用缓冲写入器作为连接缓冲var writeBuf []byteif u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {writeBuf = buf}// 创建新的 WebSocket 连接c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)c.subprotocol = subprotocol// 设置压缩/解压缩功能if compress {c.newCompressionWriter = compressNoContextTakeoverc.newDecompressionReader = decompressNoContextTakeover}// 使用较大的缓冲区写入响应头p := bufif len(c.writeBuf) > len(p) {p = c.writeBuf}p = p[:0]// 构建 WebSocket 握手响应p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)p = append(p, computeAcceptKey(challengeKey)...)p = append(p, "\r\n"...)if c.subprotocol != "" {p = append(p, "Sec-WebSocket-Protocol: "...)p = append(p, c.subprotocol...)p = append(p, "\r\n"...)}if compress {p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)}for k, vs := range responseHeader {if k == "Sec-Websocket-Protocol" {continue}for _, v := range vs {p = append(p, k...)p = append(p, ": "...)for i := 0; i < len(v); i++ {b := v[i]if b <= 31 {// 防止响应分割。b = ' '}p = append(p, b)}p = append(p, "\r\n"...)}}p = append(p, "\r\n"...)// 清除 HTTP 服务器设置的超时时间。netConn.SetDeadline(time.Time{})// 设置握手超时if u.HandshakeTimeout > 0 {netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))}if _, err = netConn.Write(p); err != nil {netConn.Close()return nil, err}if u.HandshakeTimeout > 0 {netConn.SetWriteDeadline(time.Time{})}return c, nil
}
- 协议验证: 检查 HTTP 请求头部中的必要字段,确保请求是一个有效的 WebSocket 握手请求。
- 请求合法性检查: 验证请求的方法、版本、来源和密钥等,以确保请求的合法性和安全性。
- 协议协商: 根据客户端请求和服务器配置,选择合适的子协议和是否启用数据压缩。
- 连接劫持: 使用 http.Hijacker 接口劫持 HTTP 连接,以便直接读取和写入网络数据。
- 发送握手响应: 构建并发送 WebSocket 握手响应,确认协议升级成功。
- 返回连接对象: 创建 WebSocket 连接对象,并返回给调用者,以便后续的数据传输。