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 main
import (
"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 options
func 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").value
ws = 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")
}
// 检查请求方法是否为 GET
if 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.CheckOrigin
if 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 bool
if u.EnableCompression {
for _, ext := range parseExtensions(r.Header) {
if ext[""] != "permessage-deflate" {
continue
}
compress = true
break
}
}
// 检查响应是否实现了 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.Reader
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
br = brw.Reader
}
// 获取缓冲写入器
buf := bufioWriterBuffer(netConn, brw.Writer)
// 根据情况重用缓冲写入器作为连接缓冲
var writeBuf []byte
if 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 = compressNoContextTakeover
c.newDecompressionReader = decompressNoContextTakeover
}
// 使用较大的缓冲区写入响应头
p := buf
if 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 连接对象,并返回给调用者,以便后续的数据传输。