在上一篇中,咱们介绍了HTTP协议。HTTP协议是一种无状态、无链接的协议。html
在HTTP 1.1 版本以前,客户端到服务器的TCP/IP链接是使用完毕便断开的,而服务器的TCP/IP的socket层是有开销的,而客户端又极可能请求屡次链接,每次创建链接都须要进行三次握手,断开链接须要进行四次挥手,咱们即可以思考如何简化这些步骤。git
因而,HTTP 1.1的版本中,便正式增长了一系列头部字段如Connection: keep-alive
等等,使得客户端到服务器的socket链接能够维持必定时间不被销毁。所以客户端到服务器的每一次请求便没必要都从新创建一次socket链接了,能够在已经创建的链接上直接发送数据了。github
即使是HTTP协议已经进化到能够复用链接了,它依然是有许多部分让人不满意:web
咱们上一篇文章中讲过 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请求相对较重服务器
HTTP请求采用的是请求-应答模式,即客户端发出请求,服务器给出回应。这样就产生了一个弊端,服务器只能被动回应数据,没法主动推送数据。websocket
咱们虽然能够主动轮询请求,可是这就又引起了问题1,HTTP请求的开销很大,服务器又是资源紧缺型的
所以这就致使了Websocket的产生:
Websocket是一种在创建在TCP链接上进行的全双工通讯的协议
全双工 指的是通讯的两端都具备主动发送数据的能力
WebSocket使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只须要完成一次额外握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输。
咱们所说的链接创建都是已经创建在TCP/IP三次握手后。
Websocket 在链接创建后 须要额外进行一次HTTP握手,目的是肯定通讯双方均可以支持 此协议(防止误访问)。
客户端须要先发送一个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请求升级成websocketSec-WebSocket-Version
字段为13
,表明着当前协议的版本号(目前通常采用13)Sec-WebSocket-Key
字段为必填项,值通常为16个字节的随机数据转成base64字符串。该字段用以提供给服务器作头部返回凭证校验(用于客户端肯定服务器是否支持websocket)Websocket的请求头字段与标准的HTTP并没有两样,可是协议规定,Websocket请求只能为GET类型,其他头部字段可由服务器与客户端双方协商增长。
Sec-WebSocket-Key主要是用于客户端肯定服务器是否支持,由于客户端有可能由于某些缘由错误的访问了一个HTTP服务器,该服务器并不支持Websocket,可是能够响应对应的GET请求,这个时候,客户端即可以经过服务器对应的返回字段肯定是否应该继续创建链接或者是关闭链接
当服务器收到客户端的请求头的时候,便须要做出响应,响应数据也为标准的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超文本协议
服务器一样也要返回对应的Connection
和 Upgrade
字段,同时服务器须要对客户端传入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最重要的环节了
首先咱们须要明确两个定义Byte和Bit:
0 0 0 0 0 0 0 0
由8个能够为0或1的组成,其中每一个0或1均为1个Bit接下来仍是要讲Websocket的数据发送结构,咱们习惯称每一次完整的数据包为一帧
帧的数据结构:
在上图中,咱们是以Bit为单位,可是在真实数据处理过程当中,咱们操做内存的最小单位也就是Byte,也就是8*Bit,在Swift中咱们可使用UInt8
将Byte转为无整形进行处理。
咱们能够看出来,Websocket的数据包的协议相关部分只占2-10个字节,若是算上相关掩码,也最多占用14个字节,和http相比,这也就是说,Websocket的额外消耗小。
这里咱们开始按照顺序开始讲解协议相关内容:
该位是整个帧的首位,用以标志该帧是否为连续帧的结束
0: 连续数据包还没有结束
1: 当前帧为数据包的最后一帧
用于子协议,或者其余相关。官方要求这3位均为0,子协议能够对此进行拓展。当这三位中有1-3位为1的时候,若是接收端不能正确理解相关数据,则应关闭相关链接
关闭:并不是指TCP/IP层的链接关闭,而是Websocket协议层定义的关闭,接下来的全部关闭都是如此,咱们将在接下来解释关闭含义
操做码占用4个Bit,因此操做码的一共有2^4=16种可能
下面我将以16进制列举状况:
Data
)在这里,一个帧有两种状况,控制帧和非控制帧
控制帧有必定的特殊要求:
控制帧意味着,当收到对应帧的时候,接收方应该作出必定的响应或者操做。
当接收方收到关闭帧的时候,有以下两种状况:
若是此时接收方正在发送连续的数据帧过程当中,则能够继续发送数据帧(此时没法肯定另外一方还会继续处理数据)。随后应该回复一个关闭帧,随后完成断开TCP/IP链接操做。
接收方在发送关闭帧以后不该再发送任何数据帧,当收到关闭帧后,断开TCP/IP链接
关闭: 若一方发起关闭,则该方主动发送关闭帧,并最终执行关闭TCP/IP链接的一整套流程被称为关闭
Ping为Websocket的心跳包机制帧,主要用于确认另外一方未由于异常关闭链接,当咱们接收到Ping帧时,咱们应该响应Pong帧做为回应。若长时间未收到回应,咱们应该考虑主动关闭链接
Pong帧为Websocket的心跳包机制帧中的响应帧。
在现有协议中未作定性要求,可能在将来Websocket升级增长(或者子协议中定义)
若是接收方未定义该帧的相应处理方法,则应该关闭链接
非控制帧也就是咱们一般意义上的数据帧,主要是用于双方发送数据,也是咱们平时用的最多的
分片
分片的主要目的是容许当消息开始但没必要缓冲该消息时发送一个未知大小的消 息。若是消息不能被分片,那么端点将不得不缓冲整个消息以便在首字节发生之 前统计出它的长度。对于分片,服务器或中间件能够选择一个合适大小的缓冲, 当缓冲满时,写一个片断到网络。
第二个分片的用例是用于多路复用,一个逻辑通道上的一个大消息独占输出通道 是不可取的,所以多路复用须要能够分割消息为更小的分段来更好的共享输出通道。
数据分片发送的要求:
咱们能够这样理解:
首先当咱们须要发送分片数据的时候,咱们最开始确定要告诉对方,咱们的这个数据是什么类型的,同时咱们确定不能在发送过程当中告诉对方,数据发送完了。同时在发送过程当中,咱们得告诉对方,咱们的数据尚未发送完成,这个数据是其中的一部分。当发送到最后一个的时候,咱们又须要告诉对方,发送完了。
其实简化来讲,规则以下:
对应的接收处理方式也如上面所说,先解析首帧,肯定数据类型,而后接收中间数据,最后接收尾帧,数据处理完成。过程当中若是接收到不符合分片发送的数据要求,则应该关闭链接
文本帧就是标志着,传输的数据是使用UTF8编码的文本,当咱们使用的时候,就须要将数据转换为UTF8字符串,当转换失败的时候咱们须要关闭链接
二进制帧表明着发送的数据为二进制文件
用以在将来协议升级,或者子协议拓展
操做码算是整个协议头里很关键的部分,它定义了数据的处理方式,与一些其余的操做
掩码占位1个Bit 用以标志着该字段发送是否使用了掩码,以及是否须要对真实数据进行解码。
若掩码位为1: 则标志着存在掩码,并须要进行转码
协议规定,客户端到服务器数据发送必须包含掩码,服务器返回数据不能携带掩码
数据长度占用7个Bit(可能更多),因此该段最大有可能2^7 - 1 = 127,可是真实的发送数据可能远远超过这个值,应该怎么处理呢?
因此协议制定者在这里规定了:
若是还不够怎么办?
能够考虑分片发送了-_-
真实掩码一共占用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
}
复制代码
掩码与解码均是按照此算法进行计算
也能够称做负载数据(或许应该被称为负载数据而不是真实数据,不过没什么关系),也就是咱们主要使用的数据。也就再也不多说了。
关于Websocket还有一些东西咱们还没有讲述,如子协议之类的,这些东西做者还须要再进行深刻研究。所以,在之后将会以补充文章进行讲述。
做为iOS开发人员,咱们使用这个的机会很少。可是当咱们但愿服务器能主动推送数据到咱们这,同时又不但愿再进行自行开发上层协议的时候咱们能够考虑这个协议,仍是很好用的。
做者最近正在研究这个协议,同时正在使用纯swift语言开发一个Websocket客户端三方库: SwiftAsyncWebsocket,目前正处于开发阶段。以为对Websocket有必定的研究心得,故此写下这篇文章
咱们如今前行的每一步,都是前人为咱们铺好的道路。
文章中若是有错误,还请各位评论指出
PS: 又用PPT画了一张图,感受好费劲啊,-_-
参考: