MelonTeam 移动终端前沿技术的探索者

移动互联网IM之协议设计


导语:如果想自己动手实现一个移动互联网IM app,要怎么做?第一个要解决的问题就是IM协议的设计。本文将讲述如何从0到1设计一个私有的tcp协议。

       虽然现在市面上已经存在各种各样的消息推送SDK如信鸽,但可能由于各种原因无法全面满足需求,还是想自己实现一个IM或推送功能。那么你需要解决哪些问题呢?首先面临的第一个问题就是如何实现IM协议?

传输协议选择

       传输协议一般是指TCP和UDP协议。UDP协议是无连接的,面向消息的,主要提供高效率服务。它的效率高,占资源少,但是其传输不可靠,只管发送,不管对方是否收到,虽然可以通过其他手段来实现可靠性。TCP是面向连接的,面向流的,主要提供可靠性服务。可靠性正是IM最需要的特性,所以现在主流IM基本都是使用TCP协议实现的。        关于PC QQ仍然在使用UDP的问题,经过私下了解是由于历史原因,所以一直沿用到现在。笔者猜测应该是因为当年C10K问题没有得到很好的解决,因为TCP是面向连接的,当时还没有epoll技术的存在,无法很好地解决同时在线的高负载问题,所以只能使用UDP了,因为UDP是无连接的,没有负载问题,但UDP又不可靠,所以只能在UDP上实现TCP的超时、重传、确认等机制。

协议格式选择

       常见的TCP协议格式通常有3种:文本协议、二进制协议、XML协议。

文本协议

       文本协议一般是由一串ACSII字符组成的数据。文本协议容易被人类解读,比较适合面向公众,典型的如HTTP协议。举一个HTTP GET的例子:

GET /HTTP/1.1
User-Agent: curl
Host: qq.com
Accept: */*

文本协议的特点: a. 可读性好,便于开发调试; b. 扩展性好,key-value扩展容易; c. 解析效率较好; d. 流量较小。        曾经一方霸主的IM产品MSN使用的是就是文本协议。

XML协议

       主流IM协议之一XMPP就是一种以XML为基础的开放式实时通信协议。举一个XMPP发送消息的例子:

<message from="sendinguser@somedomain" to="recipient@somedomain" xml:lang='en'>
  <body>
    Body of message
  </body>
</message> 

XML协议的特点: a. 继承了XML的优点,可读性好,扩展性好; b. 解析代价较高,效率低,占用资源多; c. 流量大。        Google出品的IM产品GTalk正是使用XMPP协议。

二进制协议

       二进制协议就是一串字节流,一般包括定长的包头和可扩展变长的包体,典型的如MQTT协议。举一个二进制协议例子:

Alt text

二进制协议特点: a. 可读性差,难于调试; b. 扩展性较差; c. 解析效率高,几乎没有解析代价; d. 流量占用极少。        QQ和微信正是使用二进制的典型代表,现在市面上大部分IM产品也都是使用二进制。虽然它可读性差,难于调试,可这正也是提高协议被破解的门槛。所以对流量和电量敏感的移动互联网IM来说,二进制协议最为适合。

主流协议比较

       在比对了协议格式后,我们接着比较一下各种协议标准。目前市面上主流的IM协议主要有应用于PC互联网的XMPP,嵌入式设备物联网上的MQTT,一起来看下它们之间的优缺点比较: | 名称 | 优点 | 缺点 | | :—- |:——— | :——— | | XMPP | 基于XML协议,容易理解,使用广泛,易于扩展 | 流量大,在移动终端解析也耗电。交互过程复杂,多被pc时代的产品使用,不适合应用于移动互联网IM | | MQTT | 低带宽,适合推送,适配多平台 | 协议简单,但是需要自己扩展好友,群组等功能 | | 私有协议 | 灵活、低带宽、自主控制 | 要考虑可扩展、兼容性、序列化和反序列化、安全等问题 |

私有协议设计

       基于TCP的应用层协议一般都分为包头和包体(如HTTP),IM协议也不例外。所以常见的做法是:定长二进制包头,可扩展变长包体,包体可以使用文本如Protobuf、MessagePack、JSON、XML等扩展性好的协议。包头负责传输和解析效率,是所有包的公共部分,与业务无关。包体保证扩展性,与业务相关。一个典型的二进制协议如下: | 字段 | length | message_id | version | type | data | | :—: | :—: | :—: | :—: | :—: | :—: | | 类型 | int | int | byte | int | byte[] | | 字节数 | 4 | 4 | 1 | 4 | n |

1、length:包长度,告知服务端要接收多长的包数据; 2、message_id:消息ID,由于网络复杂性,客户端和服务端的交互消息可能无法保证必达,所以需要重发来保证,为了避免消息重复,可以使用消息的唯一标识来去重; 3、version:消息版本号,由于二进制格式扩展性不好,如果要扩展字段,旧版协议就不兼容了,所以一般会有一个version字段用于区分版本; 4、type:消息类型,用来区分不同功能的消息包,如密钥交换消息、心跳消息、业务消息、错误返回消息、推送消息等; 5、data:包体数据,业务不同,长度可变。

粘包问题

       值得一提的是,由于TCP是基于流式数据传输的,所以会存在“粘包”问题,所谓“粘包”,是指在一次接收数据不能完整收到一个完整的消息包数据。举个例子,假设服务端按顺序发了3个包消息,如下图表示:

Alt text

但客户端读取到的数据很可能会被分成下面几个片段:

Alt text

这就是所谓的“粘包”问题,其解决办法一般有如下两种: 1、消息包头中包含表示消息包的总长度的字段(或者消息包体长度),上述举例的length正是采用该方案; 2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者特定字符作为报文分隔符,接收方通过特殊分隔符切分报文,比如上述举例可以修改成如下格式: | 字段 | length | message_id | version | type | data_length | data | delmiter | | :—: | :—: | :—: | :—: | :—: | :—: | :—: | :—: | | 类型 | int | int | byte | int | int | byte[] | byte| | 字节数 | 4 | 4 | 1 | 4 | 4 | n | 1 |

       其中delmiter可以固定为“@”等特殊字符,delmiter应尽量小,减少流量占用。另外由于包体可能包含分隔符,所以delmiter需要转义以防止解析错误,所以一般更为建议使用第一种方案解决“粘包”问题。

反设计

       包头和包尾都包含分包分隔符:笔者过往接触到不少项目的协议都采用了这种方法来分包,通过以上“粘包”问题分析可知,这种做法只会浪费流量,不会有更多好处。

序列化选择

       包体可以使用文本如Protobuf、MessagePack、JSON、XML等扩展性好的协议,但我们推荐优先考虑Protobuf,网上对序列化和反序列化的方案选择的讨论也非常多,我们这里就不再赘述,这也是目前主流IM的选择。 Protobuf优点:

  1. 标准的IDL和IDL编译器,这使得其对工程师非常友好;
  2. 序列化数据非常简洁,紧凑,序列化后的大小是json的1/10,xml格式的1/20,是二进制序列化的1/10;
  3. 解析速度非常快,比对应的XML快约20-100倍;
  4. 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。

Protobuf适合的场景:

  1. 需要和其它系统做消息交换的,对消息大小敏感的,消息空间相对xml和json等节省很多;
  2. 小数据的场合。如果你是大数据,用它并不适合;
  3. 项目语言是c++,java,python,因为它们可以使用google原生类库,序列化和反序列化效率非常高。 所以Protobuf解析性能高,序列化后数据量相对少,非常适合应用到移动互联网IM的场景。

安全性考虑

       敏感信息直接通过IM进行网络传输,所以安全层是必不可少的,一般只需要对包体进行加密,包头明文即可。换句话说,TCP协议的安全性主要可以从以下几个方面进行考虑:

使用SSL

       和HTTPS一样,使用SSL安全性高,但不同的是,HTTPS是由专门机构去验证证书合法性的,而IM不可能这样做,可行的做法是把证书打包进客户端,证书更新可以随客户端升级而一起升级,或者通过协议升级。加密的交互流程就是客户端产生一个对称的密钥,并通过证书加密后请求交给服务器,服务器解密后获得这个对称密钥,后续的通讯就全部使用这个对称的密钥来加解密,具体原理请参考SSL,这里不再赘述。不过证书成本稍高和管理稍复杂,代价较高。

自己加解密

       自己实现加解密,重点在于密钥的生成与管理,密钥管理方式主要有这么两种: 1) 固定密钥        服务端和客户端约定好一个密钥,同时约定好一个对称加密算法如AES,每次客户端发送消息前,使用约定好的算法和密钥对消息进行加密,服务端收到报文后,使用约定好的算法和密钥进行解密。这种方式优点是实现比较简单,但缺点也很明显,约定好的密钥和算法存在客户端,存在被反编译破解的风险,该方案比较适合对加密要求不高的场景; 2) 动态密钥        由于固定密钥容易暴露,所以动态密钥的理念就是对固定密钥再加一层保护。和SSL密钥协商过程类似,动态密钥的中心思想就是客户端和服务器通过非对称RSA加解密(增加破解难度)进行协商,最终客户端获得一个当前session的密钥,后续的数据传输都通过这个密钥进行AES对称加解密。流程比较复杂,具体如下图所示:

Alt text

公钥请求: 1、客户端携带帐号发起请求; 2、服务端根据帐号生成对应的RSA公、私钥; 3、服务端下发公钥,保留私钥; 4、服务端返回RSA公钥给客户端,客户端保存RSA公钥;

Alt text

登录鉴权: 1、客户端使用RSA公钥对帐号和密码等价物(帐号密码按一定规则编码)进行RSA非对称加密,然后携带这个加密结果发起请求; 2、服务端使用RSA私钥解密,获得帐号和密码; 3、服务端验证帐号和密码是否正确; 4、服务端给客户端分配当前session的密钥session_key; 5、服务端返回经过AES加密的session密钥session key,AES的密钥为帐号/密码等价物。后续请求都使用session_key作为密钥进行加解密。

Alt text

非登录请求: 1、客户端使用session_key作为密钥对请求进行AES对称加密,发起请求; 2、服务端使用session_key对请求进行AES解密; 3、根据请求处理业务逻辑; 4、服务端使用session_key作为密钥对处理结果进行AES加密,返回给客户端。        终上所述,文章主要阐述了移动互联网IM的协议设计会面临的主要包括传输协议、协议格式、协议设计、协议序列化、协议安全等问题,以及对应的解决方案,这些是笔者对过往项目的总结和思考。在身处微信和QQ两大主流移动互联网IM压力下,该文章确有班门弄斧之嫌,如有不足或错误,还请各路IM大神指教:)        值得一提的是,文章的思考也将同样也适用于其他使用tcp长连接的场景,如物联网、手游等。


说一说

目录