webscoket 学习笔记 原理篇

概述

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发起协议升级请求.

webscoket 学习笔记 原理篇_第1张图片

采用http报文,用get方法,websocket连接地址为ws://host:port/path

  • connection:update:表示要升级协议
  • upgrade:websocket:表示要升级的协议是websocket
  • Sec-WebSocket-Version: 13:表示wbsocket的版本
  • Sec-WebSocket-Key: x7qdyJEvQAUvGSweHB4N6g==:一段随机的base64编码的字符串,与响应头的Sec-WebSocket-Accept配套,提供基本的防护.
  • Sec-WebSocket-Extensions指定一个或多个协议级WebSocket扩展以要求服务器使用。允许在一个请求中使用多个Sec-WebSocket-Extension标头;
  1. 服务端收到请求后,相应协议升级

在这里插入图片描述

  • 状态代码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/

你可能感兴趣的:(web,websocket)