应用层长链接协议设计

Author Avatar
mrriddler 10月 08, 2018

本文的内容如题,在客户端场景下,自定义一款“通用”的应用层长链接。而对于协议要想要做到极致,一定要着眼业务,用户的网络状况、应用的使用模式等等都是要考虑的因素。自然也就不存在“通用”协议,本文目的在于分解协议,阐述其中可被抽象出来的部分。鉴于笔者场景和阅历的有限,如果有偏颇之处,还望指出。

对于客户端这样的场景,网络层至关重要。一般的数据请求,HTTP作为短协议能hold住场。但当遇到IM、数据增量更新这样实时性较强的要求,HTTP协议由于并不是应对这种场景而设计的,就会力不从心。而对于一些要对网络进行深层优化的应用,是绕不过HTTP的,最终的解决方案便是自定义一款长连接协议。

在设计应用层协议之前,最好是学习过TCP/IP协议栈,设计协议会碰到的很多问题在TCP/IP协议栈中已经被优雅的解决了。

协议分层

本篇讲述设计一款可靠协议,而做到可靠就依赖TCP。TCP历经了多少年的考验,里面很多的边角都是长时间工程实践出来的,不是短期可以代替的。

客户端应用协议有不同场景,请求、IM、增量更新等。针对不同场景,抽象其传输过程,将编解码交给具体场景。为了应对不同场景,在设计上有个关键点,模拟TCP/IP协议栈对协议分层。TCP/IP协议分层使数据经过每层都会在数据加上本层协议的head或footer,到目的地再在相应层将首部依次剥离。本篇设计的协议类似,payload中包含上层协议的head和payload。其整体结构:

0     1   变长       变长    
+-----+--------+-----------+----------+-----------------+---------------------
|magic|head len|payload len|   head   |   payload head  |   payload payload  |  
+-----+--------+-----------+----------+-----------------+---------------------
  • magic:标识协议
  • head len:head长度
  • payload len:payload长度
  • head:头部
  • payload head:上层协议head
  • payload payload:上层协议payload

双工通信

不同于短连接的单向“一问一答”,长连接需要应对任一端的主动传输数据,这使得要设计的协议是一个双工通信协议。这一点在head中体现出来。

Head设计

head是协议中的关键环节,head的设计牵一发而动全身,如果要升级还要注意前向兼容。

为了版本兼容,head需要version(版本)。

为了协议分层,head需要load(上层承载的协议标识)。

为了双工通信,head需要direction(流向)、request–response(请求或响应)。对于双端,流向和请求或响应可以由一个字段推断出来,但是通信的中间节点就做不到了,所以分成两个字段。

为了区分消息类型,head需要type。比说为了区分保活的心跳消息不用承载数据,普通的数据消息承载应用数据。type字段可以有较好的拓展性区分不同类型的消息。

为了压缩payload,head需要compress(标识payload压缩算法)。

为了分开payload中的head和payload,head需要load_head_len(标识payload中head的长度)。

最后再加上拓展字段extension。head结构为:

+-----------------------------+----------------------------------+
|         version             |               type               |
+-----------------------------+----------------------------------+
|  direction/request-respone  |             compress             |
+-----------------------------+----------------------------------+
|           load              |           load_head_len          |
+-----------------------------+----------------------------------+
|                         extension                              |
+-----------------------------+----------------------------------+

direction和request-respone在结构中可以合并为一个数据项,通过位运算得出。

编解码

magic字段是定长字段这个好处理。

head len和payload len是变长字段,可以参考UTF-8变长编码。UTF-8变长编码将字节分为两部分,前两位或者几位表示下一字节是否在包含在内,1就表示在,0则反之,这两部分用0作为分隔。共有多少字节,第一字节就从头开始有多少个1。

比如说,只有一个字节,则为0xxxxxxx。有两个字节110xxxxx 10xxxxxx,依次类推。

head是一个固定的复合结构,ProtocolBuffer、Thrift是不错的选择。而其extension的字段中,本质就是key-value形式的,可以吸收HTTP2.0的优点,考虑使用hpack来压缩。

payload是传输过程中的货物,到站后就交给具体场景,其编解码由上层协议决定。而在此层,针对传输过程,对其进行一次压缩,压缩方式记录在head中。如果上层协议使用的是ProtocolBuffer,则压缩配上gzip有不错的收益。

有了整体结构和编解码,协议已经有了骨架。其五脏六腑,在优化上。

连接优化

从连接这个层次来讲,重中之重就是在较差的网络环境下、网络抖动的情况下,针对连接优化。

竞速建连

针对建连,在较差网络状况下,提升建连的效率。

最简单的方案,串行建连,等到连接成功和失败以后再发起其他建连。然而这种方案效率不高。相比之下,就有并行建连,一起发起多条建连,这种方案对服务器负载会造成很大压力。综合来看,可以用竞速建连的思路。

竞速建连在尽量短的时间内、不给服务器增加负载压力情况下,提升建连的效率。每过间隔发起一个建连,直到一条建连成功。不等待某条建连结果,也不会一下发起多条建连。多个间隔后会有多个建连,直到其中任一建连成功,则为胜者,断掉其他建连尝试。还可以通过不断切换远端IP地址来减少服务端的负载。

智能心跳

心跳是长连接必备的一个模块,在服务端、网络环境下保证长连接的活性。而智能心跳的难点在于如何尽量少消耗链路流量、设备电量情况下,维持连接的活性。心跳包的设计一般是空包,没有优化的空间,那么优化就都反映在心跳的间隔上。

而针对于客户端场景,要区分前台和后台,前台相比于后台肯定需要更短的间隔。

一个比较简单的方案就是针对前台和后台的不同网络状态使用静态的统计值。而相比于此,就是要动态自适应调整出合适的值。

mars中的智能心跳模块提出以下几点:

  • 延迟探测:消除短暂环境抖动对连接的影响。在进行了三次成功的短心跳后,无需连续三次,再开始动态自适应。
  • 间隔自适应:逐渐增大间隔,找到稳定的最大间隔。在合理的区间内,连续心跳成功三次后增大间隔,连续心跳失败三次回退间隔。回退要看是否已经得到稳定最大间隔,如果是稳定阶段则回退到底,否则只回退上一阶段。
  • 避让临界值:得到稳定最大间隔后,使用比稳定最大间隔小一点的间隔。

然而这两种方案那种合适,要看应用的用户使用时长和使用习惯。相对于来说,用户使用时长较长的可以尝试动态自适应调整合适的值,用户使用时长较短可以直接使用静态统计值。

socket调优

从socket编程这个层次来讲,能做的就是对socekt接口调整参数。

  • 禁用Nagle算法:Nagle就是对于小内容的停等协议,如果是小内容的话,要查看是否所有已发送的小内容都已被确认,都被确认才能发送。 Nagle认为小内容就是小于1单位(mss)的量。而Nagle算法针对IM和增量数据更新的协议非常不友好,在客户端场景下,可以考虑设置TCP_NODELAY禁用。
  • 重用地址:socket对于源IP地址、port是唯一对应的。如果TCP连接在最后的TIME_WAIT阶段,挥手迟迟没有收到最终的确认,socket长时间占用源IP地址、port,其他socket无法重用源IP地址、port造成资源的浪费。针对这种情况,在给socket bind本地地址之前,可以设置SO_REUSEADDR,使socket在TCP连接最后的TIME_WAIT阶段,可被重用地址。

安全优化

针对安全这一层次,HTTPS在HTTP上添加一个安全层,而自定义长链接协议也是如此。在应用层协议上再加一层私有安全协议,这样的协议一般是基于TLS1.3理论基础设计。

在现代安全协议加密模型下,已经形成加密通道后能做的就不多了,握手和认证是优化的入手点。相关内容强烈推荐微信团队的这篇文章

  • 参考TLS1.3草案实现0rrt,安全协议handshake是安全性能的关键点。
  • 内置Server Root证书(SSL Pinning),节省证书链认证资源消耗,也更加安全。更进一步的做法,身份验证本身就做一次非对称密钥的签名、验证的过程,直接将公钥内置,认证时直接使用。

共享声明:本作品采用 知识共享署名 4.0 国际许可协议 转载时请注明作者和原文链接
本文链接:http://blog.mrriddler.com/应用层长链接协议设计/