WebSocket协议

  • 简介

WebSocket是一种独立的基于TCP协议的协议,它是一种全双工的通信协议,可以在支持websocket的机器之间进行双向通信。设计的目的便是为了弥补http协议的不足之处。http是严格的单向协议,任何服务器的数据传送都需要客户端先进行一次请求,http-polling是用来处理这种限制的,http-polling主要是设置长时间的超时时间,即在客户端发出请求之后,服务端在没有数据时一直hold这个请求,直到有数据时,才进行响应,但是在hold过程当中,服务器的资源是一直被占用的。而websocket更像是udp发送消息,但是其实它是基于tcp的。

  • 说明

websocket是使用建立的http协议进行转换的,并且使用的是该次http建立时候建立的tcp/ip连接。websocket是一次开放的握手过程,先通过http发送请求建立以及协议转换内容,服务端收到后进行响应以及协议的转换,并返回结果。

  • 建立连接的过程(即Handshake过程)

  1. 客户端发出请求

         客户端发出请求、格式类似下面的内容

     GET /chat HTTP/1.1
     Host: server.example.com
     Upgrade: websocket
     Connection: Upgrade
     Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
     Sec-WebSocket-Origin: http://example.com
     Sec-WebSocket-Protocol: chat, superchat
     Sec-WebSocket-Version: 8

        可以看到请求的内容与http大致相同,不同之处便是链接和部分特殊的请求头

        其中:

              ws:是websocket使用的资源请求符号,http协议使用的是http://和https://,而websocket使用的是ws://和wss://,websocket请求资源的url和http协议不同之处只有协议部分,其他ip和端口参数等都一致。

             Connection: Upgrade: 一般的通用值是keep-alive ,是判断服务器在这一次事务完成之后是否保持连接打开状态,保证连接是长久的以便后续请求请求同一台服务器。在websocket握手的过程当中,我们设置的是Upgrade,保证请求处于活跃状态,并且用于非http请求。

             Upgrade:wobsocket:此处是标志请求过去之后,客户端请求服务器建立websocket,即转换该处列出的协议(按列出的顺序)。

             Sec-WebSocket-Key:**: 这是一个客户端生成的一次性随机值,是一个进行Base64加密的16位值。此值到达服务端后会拼接GUID:“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”字符串在后面,再进行SHA-1哈希运算,在经过Base64加密,得到的值会在握手中返回。

            Sec-WebSocket-Version: 8:接收的websocket版本号,其他版本不适用不接受。

            Sec-WebSocket-Protocol:chat :指示客户端可以接受的子协议,服务器收到之后,可以选择一种可以接受的协议,并在握手当中返回,标识其接受的子协议。

            Sec-WebSocket-Origin::标志请求来源,用于鉴别未经授权的跨域请求api,告知服务器请求的来源,并由服务器判定是否接受该来源。如不接受,则返回tppt的错误代码拒绝接受。(js应用程序XHR无法设置该请求头内容,防止伪造握手来源)

     2 服务端返回内容

           返回的内容如下:

     HTTP/1.1 101 Switching Protocols
     Upgrade: websocket
     Connection: Upgrade
     Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

          其中

                101:101代表服务器进行了协议转换,任何意外的状态都可以视为握手失败

                Sec-WebSocket-Accept:由请求的key进行运算之后,返回的值,如果返回的值与预期值不同,则可以视为服务端不接受websocket连接。

                Sec-WebSocket-Protocal:chat :此类可选的字段,如果在请求的时候携带了,则返回时,服务端必须在其中选择一个子协议

        连接过程当中,部分必须请求头需要进行验证,并且如果传递了可选请求头,也需要进行校验,如果与预期不同,则可以视为握手失败。成功后才可以进行websocket数据帧的传输。

       websocket兼容http,支持http的请求头,比如cookit之类的。

 

  • 数据格式-数据帧

    •  websocket发送数据使用的是分段传输,采用数据帧的形式进行数据的传输。它将客户端发送到服务端的数据帧进行屏蔽,以免混淆网络中间设备,如拦截代理等。而服务器到客户端的帧则不会进行屏蔽。
      • 数据分段作用:可以发送大小量未知的数据时,不用缓冲该数据。如果消息不分段,那必须缓冲整个消息,那么,就必须在第一个字节发送前计算长度,陷入死循环。分段的话,设立合理大小缓冲区,缓冲区满是进行网络传输。分段还可以多路复用,如果有很大的消息的时候,不分段可能造成通道垄断,而分段很小的片段可以很好的进行通道共享。
    • 数据帧之间定义了操作码,标志该数据帧的作用,如关闭帧等。
    • 数据帧可以在握手完成之后,关闭帧发送之前的时候进行发送。
    • 数据帧格式:
      •    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/63)           |
          |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(1bit):是数据帧的第一位,用来标志一段数据,标识最后一段数据帧。

                     RSV1,RSV2,RSV3(1 bit each): 这几个参数需要协商定义值含义,否则必须为0,如果是非零值又没有进行定义,则必将使websocket连接失败。

                    Opcode(4bit): 定义的负载数据操作码 ,如果给出的操作码没在定义列表当中,那么服务器会忽略该帧。

%x0 denotes a continuation frame    连续帧
%x1 denotes a text frame       文本帧
%x2 denotes a binary frame   二进制帧
%x3-7 are reserved for further non-control frames   保留用于其他的非控制帧
%x8 denotes a connection close      连接关闭
%x9 denotes a ping      ping
%xA denotes a pong     pong
%xB-F are reserved for further control frames   保留用于其他的控制帧

       

                Mask(1bit):是否屏蔽负载数据,1是0否,如果为1,则在masking-key中使用随机选择的32位值,用于掩盖数据。(屏蔽帧的屏蔽密钥的不可预测性对于websocket的安全性直观重要)。客户端发送到服务器的帧Mask必须为1,如果服务端收到Mask为0的帧,则必须断开连接,返回Http 1002(protocol error)错误码。

               Payload length(7bits, 7+16bits, or 7+64bits):负载数据的长度(以字节为单位),如果是0-125,则为负载数据长度,如果为126,那么之后的2个字节(16位无符号整数)为负载数据的长度。如果是127,那么之后的8个字节(64位无符号整数)便是负载数据的长度。payload length = extension data + 应用数据长度,如果extension data 长度为0,那么payload length可以视为应用数据长度。

              Masking-key(0 or 4 bytes):当Mask值为1时,进行设置

             Payload data(x+y)bytes:extension data 拼接 application data

             Extension data(x bytes):协定的扩展数据,扩展数据必须指定长度,或者给出长度的计算方式,并且需要在握手的时候必须给出扩展的使用定义。否则便是为定义,则此处内容为空

            Application data(y bytes):应用程序数据,在扩展数据之后占用剩余的帧空间,长度为负载数据长度减去扩展数据长度。

       数据帧定义(ABFN):

  ws-frame                = frame-fin
                             frame-rsv1
                             frame-rsv2
                             frame-rsv3
                             frame-opcode
                             frame-masked
                             frame-payload-length
                             [ frame-masking-key ]
                             frame-payload-data

   frame-fin               = %x0 ; more frames of this message follow
                           / %x1 ; final frame of this message

   frame-rsv1              = %x0 ; 1 bit, MUST be 0

   frame-rsv2              = %x0 ; 1 bit, MUST be 0

   frame-rsv3              = %x0 ; 1 bit, MUST be 0

   frame-opcode            = %x0 ; continuation frame
                           / %x1 ; text frame
                           / %x2 ; binary frame
                           / %x3-7 ; reserved for further non-control frames
                           / %x8 ; connection close
                           / %x9 ; ping
                           / %xA ; pong
                           / %xB-F ; reserved for further control frames

   frame-masked            = %x0 ; frame is not masked, no frame-masking-key
                           / %x1 ; frame is masked, frame-masking-key present

   frame-payload-length    = %x00-7D
                           / %x7E frame-payload-length-16
                           / %x7F frame-payload-length-63

   frame-payload-length-16 = %x0000-FFFF

   frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF

   frame-masking-key       = 4( %0x00-FF ) ; present only if frame-masked is 1

   frame-payload-data      = (frame-masked-extension-data
                              frame-masked-application-data)   ; frame-masked 1
                           / (frame-unmasked-extension-data
                              frame-unmasked-application-data) ; frame-masked 0

   frame-masked-extension-data     = *( %x00-FF ) ; to be defined later

   frame-masked-application-data   = *( %x00-FF )

   frame-unmasked-extension-data   = *( %x00-FF ) ; to be defined later

   frame-unmasked-application-data = *( %x00-FF )

 

  • 数据传输

    • 发送数据 

      • 1.确保服务端和客户端都都是Open状态,如果有任何一个端点状态改变了,则端点将会终止发送消息步骤。

                           2.将消息封装在websocket数据帧当中

                           3.在第一帧中设置解释的数据类型

                           4.最后一帧的FIN值设置为1

                           5.如果消息是由客户端进行发送的,则需要设置Mask的相关信息

                           6.如果为websocket的连接定义了任何扩展名,则需要注意扩展名的相关信息设置

                           7.将设置好的数据帧通过网络进行发送

           接收数据

                          端点监听基础网络连接,进行数据的接收,将接收到的数据进行解析,解析为帧的格式,如果是收到的控制帧,则按照控制帧定义的操作进行操作。接收到数据帧后,记录给定的数据类型。如果帧中包含未分段的消息,则说明已经收到了包含数据类型和数据的websocket消息。如果该帧是消息的一部分,则将后续的数据帧应用程序数据连接起来形成data(服务器端会将数据帧按照特定算法进行解密,扩展数据按照定义进行处理),读到fin位为1的数据时,则表示接收了完整的websocket消息。那么后续的数据帧,则被认为是下一个websocket消息。

 

  • 关闭websocket连接(正常关闭)

    • 使用code和可选的关闭原因来进行关闭的操作。端点发送关闭的控制帧,另一端收到关闭帧之后。大多数情况下,基础tCP连接应该有服务器先关闭,即:如果是客户端发送关闭帧,则客户端等待服务端关闭TCP之后,再收到关闭信息后进行TCP关闭。如果是服务端发送关闭帧,则客户端立即进行TCP关闭。

 

 

注意:发送消息时,负载帧数据按照顺序串联,但是如果存在扩展,则可能会打破这个规则。

        关于分段,有几个规则:

        控制帧不可分段

        控制帧可以注入到分段的消息中间

        由于不能分段,所以中间人不得尝试更改控制帧的分段

        消息片段必须按照发送者的消息发送顺序传递给接受者

       一个消息的片段不得在另一个消息的片段之间交错,除非定义了可以解释这种交错的扩展

       端点必须能够处理消息中间的控制帧

       客户端和服务器必须支持接收分段和非分段消息

       如果使用了保留位值,中间人不知道这下帧的含义,那么中间人不得修改消息分段

       发送者可以为非控制帧消息创建任何大小的分段

        

由以上规则,可以发现,一个websocket消息,其所有片段的数据属性都和第一个片段相同,由于无法对控制帧进行分段,所有消息中所有分段的类型必须为文本或者二进制或者保留的操作码之一。

由于有时候发送大消息,需要等待很久的传输,比如ping的时间等待会非常长,所以需要在片段化消息中处理控制帧。

 

 

参考:https://tools.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-09.html

         https://sookocheff.com/post/networking/how-do-websockets-work/

         https://en.wikipedia.org/wiki/WebSocket        

posted @ 2021-08-31 21:06  sewell_画风  阅读(288)  评论(0编辑  收藏  举报