全双工协议 简而言之,许多应用场景需要全双工的web应用,例如:服务器主动推送和实时交互的应用。这些应用不适合在HTTP协议上实现。最好的方式是使用HTTP建立握手,之后基于TCP做长连接。
与HTTP的关系是:使用了其握手机制和端口号。
Relationship to TCP and HTTP
The WebSocket Protocol is an independent TCP-based protocol. Its only relationship to HTTP is that its handshake is interpreted by HTTP servers as an Upgrade request.
By default, the WebSocket Protocol uses port 80 for regular WebSocket connections and port 443 for WebSocket connections tunneled over Transport Layer Security (TLS) [RFC2818].
支持WebSocket的浏览器
握手 WebSocket通过HTTP/1.1协议的101状态码握手。
客户端发起握手请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 GET ws://localhost:6060/ws HTTP/1.1 Host : localhost:6060Connection : Upgrade # 连接升级Pragma : no-cacheCache-Control : no-cacheUser-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36Upgrade : websocket # 升级到WebSocketOrigin : http://localhost:6060Sec-WebSocket-Version : 13 # 表示支持的WebSocket版本Accept-Encoding : gzip, deflate, brAccept-Language : zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6,zh-TW;q=0.5Cookie : wp-settings-time-1=1566626634Sec-WebSocket-Key : Omt7iN7CaEUxYdm+wlVEaA==Sec-WebSocket-Extensions : permessage-deflate; client_max_window_bits
服务器回应
1 2 3 4 HTTP/1.1 101 Switching ProtocolsUpgrade : websocketConnection : UpgradeSec-WebSocket-Accept : ggdcU6xoySObbFCh62I2J3eRxu0=
Sec-WebSocket-Key
是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把Sec-WebSocket-Key
加上一个特殊字符串(固定):258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,然后计算SHA-1摘要,之后进行BASE-64编码,将结果做为Sec-WebSocket-Accept
头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var keyGUID = []byte ("258EAFA5-E914-47DA-95CA-C5AB0DC85B11" )func computeAcceptKey (challengeKey string ) string { h := sha1.New() h.Write([]byte (challengeKey)) h.Write(keyGUID) return base64.StdEncoding.EncodeToString(h.Sum(nil )) } func generateChallengeKey () (string , error) { p := make ([]byte , 16 ) if _, err := io.ReadFull(rand.Reader, p); err != nil { return "" , err } return base64.StdEncoding.EncodeToString(p), nil }
Origin
字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面。
协议格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
为true表示是最后一个分片。
RSV
一般情况都为0。
opcode
定义了payload data
的类型。
mask
为true表示payload是编码处理的。
masking-key
编码key。
payload length
表示payload的长度。
extend为payload过长时的扩展字段。
客户端发给服务器的包,必须masking:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 const wordSize = int (unsafe.Sizeof(uintptr (0 )))func newMaskKey () [4]byte { n := rand.Uint32() return [4 ]byte {byte (n), byte (n >> 8 ), byte (n >> 16 ), byte (n >> 24 )} } func maskBytes (key [4]byte , pos int , b []byte ) int { if len (b) < 2 *wordSize { for i := range b { b[i] ^= key[pos&3 ] pos++ } return pos & 3 } if n := int (uintptr (unsafe.Pointer(&b[0 ]))) % wordSize; n != 0 { n = wordSize - n for i := range b[:n] { b[i] ^= key[pos&3 ] pos++ } b = b[n:] } var k [wordSize]byte for i := range k { k[i] = key[(pos+i)&3 ] } kw := *(*uintptr )(unsafe.Pointer(&k)) n := (len (b) / wordSize) * wordSize for i := 0 ; i < n; i += wordSize { *(*uintptr )(unsafe.Pointer(uintptr (unsafe.Pointer(&b[0 ])) + uintptr (i))) ^= kw } b = b[n:] for i := range b { b[i] ^= key[pos&3 ] pos++ } return pos & 3 }
控制消息 WebSocket定义了3种类型的控制消息(control message):close, ping and pong。
close
opcode
为0x8
客户端发起的close,websocket.payload.close.status_code
置为1000,表示normal closure。若不设置,则默认为CloseNoStatusReceived=1005。
服务器收到关闭请求,响应包也发送1000。
客户端收到响应后,执行TCP四次挥手过长,双方关闭TCP连接。
在实际捕获websocket的close code时,应注意以下细节:
1006
属于错误码,并不是可从SetCloseHandler
中捕获到的,因为它没有真正的发送close消息。
若websocket由Nginx代理,应考虑Nginx可能会主动发送close的情况,因此应该从读取message的地方去获取断开原因,因为SetCloseHandler
有可能会接收到Nginx发来的close状态。
ping
opcode
为0x9
发送方 -> 接收方
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
如果客户端支持ping,最好由客户端发起ping,然后服务器记录时间,超时断开即可。浏览器中没有相关api发送ping给服务器,只能由服务器发ping给浏览器。
pong
opcode
为0xA
接收方 -> 发送方
参考 https://tools.ietf.org/html/rfc6455
https://zh.wikipedia.org/wiki/WebSocket
https://godoc.org/github.com/gorilla/websocket
https://github.com/gorilla/websocket
websocket ping/pong timeout