长连接

Posted by Maqy on January 18, 2020

前段时间项目里重写了长连接,自己就学习了一番,并根据大佬的教学来做个小分享吧

背景

传统的APP开发中通常都是通过HTTPS请求来进行和服务端的通信的,但是对于一些复杂的业务,是需要服务端来通知客户端的,也就是全双工的通信。

其本质是通过WebSocket来建立一个长连接,然后通过长连接来进行全双工通信,这样不仅解决了客户端轮询通信的问题,也降低了宽带。

本质

长连接的本质呢,其实就是通过WebSocket来连接服务器的ip和端口号来建立连接,我们这里就不关心那些socket的字段定义之类的问题了,而为了实现我们自己项目里的长连接呢,我们需要自定义每次传输的协议。

HEADER格式

通常我们将发送的自定义数据的格式定义为Header和Body,通常情况下,定义Header的格式通常有三种。

1.定长

固定长度的Header主要将每个位置都标记成不同功能的代表,比如我们将Header定义成18byte,每个字节都带表不同的含义,比如:

第一个 byte 代表 消息版本
第二个 byte 代表 消息类型(ping pong,空消息,握手,auth 等阶段)
第三个 byte 代表 用户信息
第四个 byte 代表 body长度
如果考虑扩展的话我们还可以增加一个字段留给以后扩展

显而易见,定长的header的方式不方便后期扩展,对版本升级等不太友好

2.预留字段变长

为了方便后期扩展,方便后期增加不同字段,我们可以在Head的固定位增加一个字节来代表扩展的字段长度,这样后期解析的时候,我们就可以增加新的功能了

3.特殊字节变长

参考HTTP的格式,我们也可以在HEAD结尾处加一个特殊字符,来代表HEAD读取完毕,接下来可以读取body数据。

可以看出变长的HEAD对后期维护更方便一些。

大小端

iOS里数据是小段模式

// 0x 是十六进制
int num = 0x12345678;                   // 十进制为305419896
char a = num & 0xff;                    // 取(0 ~ 7位)一个子节
char b = num >> 8 & 0xff;               // 取(8 ~15位)一个子节
char c = num >> 16 & 0xff;              // 取(16~23位)一个子节
char d = num >> 24 & 0xff;              // 取(24~31位)一个子节
printf("%x, %x, %x, %x", a, b, c, d);   // 小端模式将输出78,56,34,12

但后端都是大端,所以在获取后端数据的时候,解析header的时候,需要注意大小端问题

安全

我们知道HTTPS是在HTTP的基础上添加了SSL协议,那么为了长连接的安全,防止被运营商抓包,或被第三方劫持,我们也要给自己的长连接加上一层安全协议,一般都是加SSL或TLS协议。

在握手阶段完成后,需要建立安全连接,其实主要就是交换对称加密的密钥,其大致流程如下:

          Client                                      Service
生成cPublicKey,cPriKey ----send cPublicKey---> 生成sPublicKey,sPriKey
  校验签名(sPublicKey) <----send sPublicKey & 签名 -----  |
          生成key                                     生成key
            |          ------ auth ---------->          |
            |      <----- authAck & info -------     生成info
         保存info   <----- 收发使用 info ------->      保存info

主要的安全流程就是通过上述的流程来进行的,先通过非对称加密来进行签名,最终来达到交换对称加密的key的目的,验证过后就可以交换业务信息了。

Body

body主要用来定义我们要发送的model,一般我们会采用JSON或者某些特定格式的三方库来完成model的解析,我们项目里用的就是 Protobuf 三方库,用起来还算方便。

异常重连

既然我们建立了长连接那么就要维护好这个长连接,避免时不时的就断开,而长连接的重连机制我们主要监控三方面来完成。

1.前后台切换

2.网络抖动

3.NAT超时

心跳机制

大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。

为了避免长连接断连,长连接心跳间隔必须要小于NAT超时时间(aging-time),如果超过aging-time不做心跳,TCP长连接链路就会中断,Server就无法发送Push给手机,只能等到客户端下次心跳失败后,重建连接才能取到消息。

而所谓的心跳机制,其实就是发送一个ping,让后后端给一个pong,看下现在链路是否正常连接。

客户端实现上其实就是一个timer,每个一段时间发送一次心跳。

为了避免资源浪费,对于心跳机制还有多种算法可供选择。

常见的两种处理机制是:

1.顺延:在接收到新的消息或者应答包的时候,如果还没有发送ping包,就将这个ping包在这个时间的基础上顺延一定时间。

2.渐进式:从刚开始每秒发送一次,如果正常收到的话就将事件放大一点点,通过监控通讯来控制ping的频率。

其实方法2和网络拥塞的快重传机制比较相似,但不管是哪种算法,其最终目的都是为了节省资源,总而减轻服务器压力。

总结

其实长连接就是在websocket的基础上增加了自己定义的一些传输协议,我们常遇到的问题也就大概上面这些,需要对网络的东西多多了解才能更好的封装。

Tip

AFNetworking的HTTPShouldUsePipelining这个属性是干啥的?

如果设置为yes的话,对于同一个tcp连接,可以多次放松请求,这样就需要taskid来标示每条TCP的任务的先后顺序,但这个服务器的要求太高了,现在还很少能做到,所以客户端请求的时候一般不会打开。

之所以对服务器要求高,是因为对一个TCP连接而言,如果同时收到多个请求,如果某个任务处理完了,服务端很难确定这个任务是哪个请求的回调,目前很少有无服务会标示,且标示的难度很大