概述
WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。
http协议是无状态的,每一个新的http请求request,只能通过client发起,server端收到后,返回一个response,然后连接断开。http1.1版本增加了keep-alive请求头,可以通过一条通道请求多次.且server端不能主动被client端发送数据,只能被动地相应请求.
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输
websocket是基于tcp的独立的协议,与http有良好的兼容性,使用80,443端口,在握手阶段采用http协议.
优点:
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。 - 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
握手协议:
-
WebSocket 是独立的、创建在TCP上的协议。
-
Websocket 通过 HTTP/1.1 协议的101状态码进行握手。
-
为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)。
建立连接过程
1.首先由客户端client发起协议升级请求.
采用http报文,用get方法,websocket连接地址为ws://host:port/path
connection:update
:表示要升级协议upgrade:websocket
:表示要升级的协议是websocketSec-WebSocket-Version: 13
:表示wbsocket的版本Sec-WebSocket-Key: x7qdyJEvQAUvGSweHB4N6g==
:一段随机的base64编码的字符串,与响应头的Sec-WebSocket-Accept
配套,提供基本的防护.Sec-WebSocket-Extensions
指定一个或多个协议级WebSocket扩展以要求服务器使用。允许在一个请求中使用多个Sec-WebSocket-Extension标头;
- 服务端收到请求后,相应协议升级
- 状态代码101表示协议切换
Sec-WebSocket-Accept: yNdP+JNEFr+tEVUzCYTdSGpZy0Y=
Sec-WebSocket-Accept计算流程去如下:
- 1 将请求头的
Sec-WebSocket-Key
字段与魔串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11
)拼接 - 2 通过sha1计算摘要并将其编码为base64
代码如下:
// golang
var magicString = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func main() {
SecWebSocketKey := "x7qdyJEvQAUvGSweHB4N6g=="
// 将 Sec-WebSocket-Key与magicString拼接
str := append([]byte(SecWebSocketKey), magicString...)
// 创建sha1对象
h := sha1.New()
h.Write(str)
SecWebSocketAccept := base64.StdEncoding.EncodeToString(h.Sum(nil))
fmt.Println(SecWebSocketAccept) // 输出yNdP+JNEFr+tEVUzCYTdSGpZy0Y=
}
# python
import hashlib
import base64
Sec_WebSocket_Key="x7qdyJEvQAUvGSweHB4N6g=="
magic_string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
value =Sec_WebSocket_Key + magic_string
Sec_WebSocket_Accept= base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
print(Sec_WebSocket_Accept)# 输出 b'yNdP+JNEFr+tEVUzCYTdSGpZy0Y='
在客户端接收到响应后,将计算的结果与Sec_WebSocket_Accept
比较,用于判断是否有效.
数据传输
websocketke客户端和服务端传输的数据为帧(frame),下面为一帧的格式,以bit为单位
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: 1 bit
表示这是一个消息的最后的一帧。第一个帧也可能是最后一个。 %x0 : 还有后续帧 %x1 : 最后一帧
-
RSV1、2、3: 1 bit each
除非一个扩展经过协商赋予了非零值以某种含义,否则必须为0 如果没有定义非零值,并且收到了非零的RSV,则websocket链接会失败
-
Opcode: 4 bit
解释说明 “Payload data” 的用途/功能 如果收到了未知的opcode,最后会断开链接 定义了以下几个opcode值: %x0 : 代表连续的帧 %x1 : text帧 %x2 : binary帧 %x3-7 : 为非控制帧而预留的 %x8 : 关闭握手帧 %x9 : ping帧 %xA : pong帧 %xB-F : 为非控制帧而预留的
-
Mask: 1 bit
定义“payload data”是否被添加掩码 如果置1, “Masking-key”就会被赋值 所有从客户端发往服务器的帧都会被置1从服务端向客户端发送数据时,不需要对数据进行掩码操作
-
Payload length: 7 bit | 7+16 bit | 7+64 bit
“payload data” 的长度如果在0~125 bytes范围内,它就是“payload length”, 如果是126 bytes, 紧随其后的被表示为16 bits的2 bytes无符号整型就是“payload length”, 如果是127 bytes, 紧随其后的被表示为64 bits的8 bytes无符号整型就是“payload length”
-
Masking-key: 0 or 4 bytes
所有从客户端发送到服务器的帧都包含一个32 bits的掩码(如果“mask bit”被设置成1),否则为0 bit。一旦掩码被设置,所有接收到的payload data都必须与该值以一种算法做异或运算来获取真实值.如果Mask为0,则没有Masking-key
-
Payload data: (x+y) bytes
它是"Extension data"和"Application data"的总和,一般扩展数据为空。
-
Extension data: x bytes
扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。 除非扩展被定义,否则就是0 任何扩展必须指定其Extension data的长度
-
Application data: y bytes
任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。"Extension data"之后的剩余帧的空间
WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN
的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。
此外,opcode
在数据交换的场景下,表示的是数据的类型。0x01
表示文本,0x02
表示二进制。而0x00
比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完
心跳机制
如果长时间没有数据往来,防止造成资源的浪费,需要主动断开连接.但是某些情况下,即使长时间没有数据也需要保持连接,即心跳机制.
一般是服务端给客户端发送Ping,然后客户端发送Pong来回应.
ping/pong操作对应的是websocket的ping/pong 控制帧
参考:
https://tonybai.com/2019/09/28/how-to-build-websockets-in-go/