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

期刊列表

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

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

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

于是,HTTP 1.1的版本中,便正式增加了一系列头部字段如Connection: keep-alive等等,使得客户端到服务器的socket连接可以维持一定时间不被销毁。因此客户端到服务器的每一次请求便不必都重新建立一次socket连接了,可以在已经建立的连接上直接发送数据了。

HTTP协议的缺点

即便是HTTP协议已经进化到可以复用连接了,它依然是有许多部分让人不满意:

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

我们上一篇文章中讲过 HTTP协议中 我们操作的部分一般是body,也有一部分的header

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

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

GET / HTTP/1.1\r\n
Host: www.example.com\r\n
\r\n

可能有人会觉得,这个数据并不多啊。

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

2. HTTP请求只能单向发送

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

我们虽然可以主动轮询请求,但是这就又引发了问题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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 一、内容概览 WebSocket的出现,使得浏览器具备了实时双向通信的能力。本文由浅入深,介绍了WebSocket...
    Calvin李阅读 2,503评论 2 10
  • 1 述 WebSocket是一种网络通信协议WebSocket 协议在2008年诞生,2011年成为国际标准。HT...
    凯玲之恋阅读 674评论 0 0
  • 1 台风天来的太快,以致于还未来得及习惯每天的潮湿日常,便又要接受阴雨连绵的常态。这一晚,照例打着伞,...
    陈十七爱吃肉阅读 357评论 0 1
  • 昨天我从一家待了两个星期的公司离开了。 离开的原因是公司长时间的加班和领导的管理方式,最让人惊讶的是,当我提出加班...
    秋青阅读 543评论 3 10
  • 文|东雨夏晨 电影《无问西东》里有两句台词,学生困惑的问老师,什么是真实? 老师平和的回答道:“你看到什么,听到什...
    东雨夏晨阅读 259评论 0 5