关于Socket,看我这几篇就够了(三)原来你是这样的Websocket

期刊列表

  1. 关于Socket,看我这几篇就够了(一)
  2. 关于Socket,看我这几篇就够了(二)之HTTP
  3. 关于Socket,看我这几篇就够了(三)原来你是这样的Websocket

在上一篇中,咱们介绍了HTTP协议。HTTP协议是一种无状态、无链接的协议。html

在HTTP 1.1 版本以前,客户端到服务器的TCP/IP链接是使用完毕便断开的,而服务器的TCP/IP的socket层是有开销的,而客户端又极可能请求屡次链接,每次创建链接都须要进行三次握手,断开链接须要进行四次挥手,咱们即可以思考如何简化这些步骤。git

因而,HTTP 1.1的版本中,便正式增长了一系列头部字段如Connection: keep-alive等等,使得客户端到服务器的socket链接能够维持必定时间不被销毁。所以客户端到服务器的每一次请求便没必要都从新创建一次socket链接了,能够在已经创建的链接上直接发送数据了。github

HTTP协议的缺点

即使是HTTP协议已经进化到能够复用链接了,它依然是有许多部分让人不满意:web

1. HTTP请求的无关内容(协议相关内容)开销大

咱们上一篇文章中讲过 HTTP协议中 咱们操做的部分通常是body,也有一部分的header算法

这里咱们按照字节Byte来简述下:swift

这里假设咱们须要定时刷新一个GET接口获取信息(咱们只分析发送请求),则咱们请求的数据文本结构便为以下结构:浏览器

GET / HTTP/1.1\r\n
Host: www.example.com\r\n
\r\n
复制代码

可能有人会以为,这个数据并很少啊。bash

这里咱们须要注意,开销大并非一个绝对的含义,它是一种相对的。咱们能够观察一下,在这样的一个简单请求中,咱们究竟发送了多少字节,一共是42个字节。也就是说,每次咱们执行这个请求都须要发送这42个字节,其中用于格式相关的便占有14个字节(HTTP/1.1\r\n)。这些数据每次请求都须要重复发送,咱们也能够说,HTTP请求相对较重服务器

2. HTTP请求只能单向发送

HTTP请求采用的是请求-应答模式,即客户端发出请求,服务器给出回应。这样就产生了一个弊端,服务器只能被动回应数据,没法主动推送数据。websocket

咱们虽然能够主动轮询请求,可是这就又引起了问题1,HTTP请求的开销很大,服务器又是资源紧缺型的

所以这就致使了Websocket的产生:

Websocket

Websocket是一种在创建在TCP链接上进行的全双工通讯的协议

全双工 指的是通讯的两端都具备主动发送数据的能力

WebSocket使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只须要完成一次额外握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输。

协议

链接创建

咱们所说的链接创建都是已经创建在TCP/IP三次握手后。

Websocket 在链接创建后 须要额外进行一次HTTP握手,目的是肯定通讯双方均可以支持 此协议(防止误访问)。

  1. 客户端发起协议升级请求

客户端须要先发送一个HTTP头(包含Websocket指定信息,与其余头部信息如cookie等),客户端头部结构以下所示:

GET /访问路径 HTTP/1.1\r\n 
Host: www.example.com\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Version: 13\r\n
Sec-WebSocket-Key: mgZ6+kXU1+mEgOXWDPPsBg==\r\n
\r\n
复制代码

上述为websocket规定的固定的头部信息

  • Connection字段必须为Upgrade,用以标志着客户端须要链接升级
  • Upgrade字段必须为websocket,标志着客户端须要由http请求升级成websocket
  • Sec-WebSocket-Version字段为13,表明着当前协议的版本号(目前通常采用13)
  • Sec-WebSocket-Key字段为必填项,值通常为16个字节的随机数据转成base64字符串。该字段用以提供给服务器作头部返回凭证校验(用于客户端肯定服务器是否支持websocket)

Websocket的请求头字段与标准的HTTP并没有两样,可是协议规定,Websocket请求只能为GET类型,其他头部字段可由服务器与客户端双方协商增长。

Sec-WebSocket-Key主要是用于客户端肯定服务器是否支持,由于客户端有可能由于某些缘由错误的访问了一个HTTP服务器,该服务器并不支持Websocket,可是能够响应对应的GET请求,这个时候,客户端即可以经过服务器对应的返回字段肯定是否应该继续创建链接或者是关闭链接

  1. 服务器响应请求数据

当服务器收到客户端的请求头的时候,便须要做出响应,响应数据也为标准的HTTP请求头

HTTP/1.1 101 Switch Protocol\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Accept: qIs5tRK57T9vTjEtFfTLOSe3K3w=\r\n
\r\n
复制代码

服务器首先要返回状态码101,用以代表服务端切换协议了,之后的数据解析协议将再也不是HTTP超文本协议

服务器一样也要返回对应的ConnectionUpgrade 字段,同时服务器须要对客户端传入Sec-WebSocket-Key进行必定的处理,将处理结果返回至Sec-WebSocket-Accept中供客户端校验。

  • Sec-WebSocket-Key处理方法:

Sec-WebSocket-Key拼接字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 而后将其进行sha1计算hash,最后将得出的hash进行base64转码成字符串,放入至Sec-WebSocket-Accept

当客户端收到对应的Sec-WebSocket-Accept时,用本身传的Sec-WebSocket-Key进行一样的处理,并比较服务器返回结果,若是结果一致则客户端认为服务器支持请求。当比较不一致时,按照协议要求,客户端应该主动断开链接。


咱们能够看到,Websocket链接创建事实上就至关于客户端向服务器发起了一次普通的Body为空的HTTP请求,而服务器作出了一样的响应

Websocket如此作,是为了兼容标准的HTTP协议,由于对于一台服务器应用而言,它没必要同时监听多个端口,就能够同时知足充当HTTP服务器和Websocket服务器。

一样Websocket请求也能够支持Cookie等等的HTTP头部规定。

在这里咱们还看不出来Websocket如何解决HTTP的缺点的,由于这个只是Websocket的额外握手过程,并不是真正数据发送。

数据发送

这里就要讲到Websocket最重要的环节了

首先咱们须要明确两个定义ByteBit:

  • Byte:计算机存储与传输的标准单位(字节),转成非负整数能支持最大的数为(2^8 - 1) = 255,一个Byte转成二进制位的时候:0 0 0 0 0 0 0 0 由8个能够为0或1的组成,其中每一个0或1均为1个Bit
  • Bit:二进制数系统中,每一个0或1就是一个位(bit),位是数据存储的最小单位。1 Byte = 8 Bit

接下来仍是要讲Websocket的数据发送结构,咱们习惯称每一次完整的数据包为一

帧的数据结构:

Websocket帧数据结构

在上图中,咱们是以Bit为单位,可是在真实数据处理过程当中,咱们操做内存的最小单位也就是Byte,也就是8*Bit,在Swift中咱们可使用UInt8将Byte转为无整形进行处理。

咱们能够看出来,Websocket的数据包的协议相关部分只占2-10个字节,若是算上相关掩码,也最多占用14个字节,和http相比,这也就是说,Websocket的额外消耗小。

这里咱们开始按照顺序开始讲解协议相关内容:

  • FIN:

该位是整个帧的首位,用以标志该帧是否为连续帧的结束

0: 连续数据包还没有结束

1: 当前帧为数据包的最后一帧

  • RSV1-RSV3:

用于子协议,或者其余相关。官方要求这3位均为0,子协议能够对此进行拓展。当这三位中有1-3位为1的时候,若是接收端不能正确理解相关数据,则应关闭相关链接

关闭:并不是指TCP/IP层的链接关闭,而是Websocket协议层定义的关闭,接下来的全部关闭都是如此,咱们将在接下来解释关闭含义

  • 操做码(opcode):

操做码占用4个Bit,因此操做码的一共有2^4=16种可能

下面我将以16进制列举状况:

  1. 0:表明着当前帧是一个继续帧
  2. 1:表明着当前帧是一个文本帧(传输数据为UTF8编码的文本)
  3. 2:表明着当前帧是一个二进制数据流帧(Swift中为Data)
  4. 3-7:用于将来的非控制帧
  5. 8:表明着当前帧是一个关闭帧
  6. 9:表明着当前帧是一个心跳检测Ping帧
  7. A:表明着当前帧是一个心跳检测回复Pong帧
  8. B-F:用于将来的控制帧

在这里,一个有两种状况,控制帧非控制帧

控制帧

控制帧有必定的特殊要求:

  1. 控制帧不能处于一个连续的数据帧中
  2. 控制帧的真实发送数据大小不能超过125字节
  3. 控制帧的FIN(终止位)必须是1

控制帧意味着,当收到对应帧的时候,接收方应该作出必定的响应或者操做。

8:关闭帧

当接收方收到关闭帧的时候,有以下两种状况:

  1. 若接收方以前还没有发送过关闭帧

若是此时接收方正在发送连续的数据帧过程当中,则能够继续发送数据帧(此时没法肯定另外一方还会继续处理数据)。随后应该回复一个关闭帧,随后完成断开TCP/IP链接操做。

  1. 若接收方以前已经发送过关闭帧

接收方在发送关闭帧以后不该再发送任何数据帧,当收到关闭帧后,断开TCP/IP链接

  1. 关闭帧为控制帧,所以能够携带不超过125个字节的数据,该帧携带的数据前两个字节为错误码,随后的字节为对应的描述缘由(UTF8编码文本)

关闭: 若一方发起关闭,则该方主动发送关闭帧,并最终执行关闭TCP/IP链接的一整套流程被称为关闭

9:Ping

Ping为Websocket的心跳包机制帧,主要用于确认另外一方未由于异常关闭链接,当咱们接收到Ping帧时,咱们应该响应Pong帧做为回应。若长时间未收到回应,咱们应该考虑主动关闭链接

A:Pong

Pong帧为Websocket的心跳包机制帧中的响应帧。

其他控制帧

在现有协议中未作定性要求,可能在将来Websocket升级增长(或者子协议中定义)

若是接收方未定义该帧的相应处理方法,则应该关闭链接

非控制帧

非控制帧也就是咱们一般意义上的数据帧,主要是用于双方发送数据,也是咱们平时用的最多的

0:继续帧(分片)

分片

分片的主要目的是容许当消息开始但没必要缓冲该消息时发送一个未知大小的消 息。若是消息不能被分片,那么端点将不得不缓冲整个消息以便在首字节发生之 前统计出它的长度。对于分片,服务器或中间件能够选择一个合适大小的缓冲, 当缓冲满时,写一个片断到网络。

第二个分片的用例是用于多路复用,一个逻辑通道上的一个大消息独占输出通道 是不可取的,所以多路复用须要能够分割消息为更小的分段来更好的共享输出通道。

数据分片发送的要求:

  1. 数据的首帧与过程帧的FIN位为0
  2. 数据的首帧的操做码必须为对应的非控制帧操做码,且不能为继续帧
  3. 数据的过程帧与终止帧的操做码必须为继续帧
  4. 数据的终止帧的操做码必须为1

咱们能够这样理解:

首先当咱们须要发送分片数据的时候,咱们最开始确定要告诉对方,咱们的这个数据是什么类型的,同时咱们确定不能在发送过程当中告诉对方,数据发送完了。同时在发送过程当中,咱们得告诉对方,咱们的数据尚未发送完成,这个数据是其中的一部分。当发送到最后一个的时候,咱们又须要告诉对方,发送完了。

其实简化来讲,规则以下:

  1. 发送开始肯定数据类型,过程与结尾均不可更改
  2. 发送截止告诉对方数据完成

对应的接收处理方式也如上面所说,先解析首帧,肯定数据类型,而后接收中间数据,最后接收尾帧,数据处理完成。过程当中若是接收到不符合分片发送的数据要求,则应该关闭链接

1:文本帧

文本帧就是标志着,传输的数据是使用UTF8编码的文本,当咱们使用的时候,就须要将数据转换为UTF8字符串,当转换失败的时候咱们须要关闭链接

2:二进制帧

二进制帧表明着发送的数据为二进制文件

3-7: 其他非控制帧

用以在将来协议升级,或者子协议拓展

操做码算是整个协议头里很关键的部分,它定义了数据的处理方式,与一些其余的操做

掩码(MASK)

掩码占位1个Bit 用以标志着该字段发送是否使用了掩码,以及是否须要对真实数据进行解码。

若掩码位为1: 则标志着存在掩码,并须要进行转码

为何要设计掩码?

协议规定,客户端到服务器数据发送必须包含掩码,服务器返回数据不能携带掩码

数据长度(Payload Len)

数据长度占用7个Bit(可能更多),因此该段最大有可能2^7 - 1 = 127,可是真实的发送数据可能远远超过这个值,应该怎么处理呢?

因此协议制定者在这里规定了:

  1. 当该值小于等于125时表示真正的数据长度(Byte)
  2. 当该值等于126时,咱们须要取接下来的16个Bit(2个Byte)做为长度,使得长度能够支持到2^16 - 1 = 65535(Byte)
  3. 当该值等于127时,咱们须要取接下来的64个Bit(8个Byte)做为长度,使得长度能够支持到2^64 - 1 = 很大的一个数

若是还不够怎么办?

能够考虑分片发送了-_-

Masking-Key(真实掩码)

真实掩码一共占用32个Bit(4个Byte)

该字段是咱们根据上述掩码标志位获取的,若是掩码标志位为1,则该字段存在;为0则该位为空。

协议规定,真实掩码应该是咱们使用不可预测的算法得出的随机32个Bit(4个Byte)

在Swift中咱们可使用Security.SecRandomCopyBytes()方法获取随机值

当咱们拥有掩码与真实数据后,咱们须要按照以下操做对真实数据进行处理(直接展现Swift代码)

func maskData(payloadData: Data, maskingKey: Data) -> Data {
    let finalData = Data(count: payloadData.count)
    // 转化Data为指针,方便处理
    let payloadPointer: UnsafePointer<UInt8> = payloadData.withUnsafeBytes({$0})
    let maskPointer: UnsafePointer<UInt8> = maskingKey.withUnsafeBytes({$0})
    let finalPointer: UnsafeMutablePointer<UInt8> = finalData.withUnsafeBytes({UnsafeMutablePointer(mutating: $0)})


    for index in 0..<payloadData.count {
        let indexMod = index % 4
        // 对应位异或XOR(^)
        (finalPointer + index).pointee = (payloadPointer + index).pointee ^ (maskPointer + indexMod).pointee
    }

    return finalData
}
复制代码

掩码与解码均是按照此算法进行计算

真实数据(Payload Data)

也能够称做负载数据(或许应该被称为负载数据而不是真实数据,不过没什么关系),也就是咱们主要使用的数据。也就再也不多说了。

其余

关于Websocket还有一些东西咱们还没有讲述,如子协议之类的,这些东西做者还须要再进行深刻研究。所以,在之后将会以补充文章进行讲述。

何时须要使用Websocket

做为iOS开发人员,咱们使用这个的机会很少。可是当咱们但愿服务器能主动推送数据到咱们这,同时又不但愿再进行自行开发上层协议的时候咱们能够考虑这个协议,仍是很好用的。

为何要写这篇文章?

做者最近正在研究这个协议,同时正在使用纯swift语言开发一个Websocket客户端三方库: SwiftAsyncWebsocket,目前正处于开发阶段。以为对Websocket有必定的研究心得,故此写下这篇文章

结尾

咱们如今前行的每一步,都是前人为咱们铺好的道路。

文章中若是有错误,还请各位评论指出

PS: 又用PPT画了一张图,感受好费劲啊,-_-

参考:

SocketRocket源码

RFC 6455

相关文章
相关标签/搜索