移动互联网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协议。举一个二进制协议例子:

二进制协议特点: 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个包消息,如下图表示:

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

这就是所谓的“粘包”问题,其解决办法一般有如下两种: 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对称加解密。流程比较复杂,具体如下图所示:

公钥请求:

1、客户端携带帐号发起请求;

2、服务端根据帐号生成对应的RSA公、私钥;

3、服务端下发公钥,保留私钥;

4、服务端返回RSA公钥给客户端,客户端保存RSA公钥;

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

非登录请求:

1、客户端使用session_key作为密钥对请求进行AES对称加密,发起请求;

2、服务端使用session_key对请求进行AES解密;

3、根据请求处理业务逻辑;

4、服务端使用session_key作为密钥对处理结果进行AES加密,返回给客户端。    

 终上所述,文章主要阐述了移动互联网IM的协议设计会面临的主要包括传输协议、协议格式、协议设计、协议序列化、协议安全等问题,以及对应的解决方案,这些是笔者对过往项目的总结和思考。在身处微信和QQ两大主流移动互联网IM压力下,该文章确有班门弄斧之嫌,如有不足或错误,还请各路IM大神指教:)        值得一提的是,文章的思考也将同样也适用于其他使用tcp长连接的场景,如物联网、手游等。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏owent

针对Java JIT的优化(转表工具:xresloader)

之前做了一个转Excel表到lua/二进制/json/xml的工具-xresloader。目的一方面是方便策划。另一方面是统一客户端和服务器的转表模式,并且要灵...

572
来自专栏张善友的专栏

MongoDB核心贡献者:不是MongoDB不行,而是你不懂!

近期MongoDB在Hack News上是频繁中枪。许多人更是声称恨上了MongoDB,David mytton就在他的博客中揭露了MongoDB许多现存问题。...

20010
来自专栏IT笔记

SpringBoot开发案例之整合mail队列篇

? 科帮网邮件队列.png 前言 前段时间搞了个SpringBoot开发案例之整合mail发送服务,也是基于目前各项目平台的邮件发送功能做一个抽离和整合。 问...

4157
来自专栏容器化

详解k8s零停机滚动发布微服务 - kubernetes

在当下微服务架构盛行的时代,用户希望应用程序时时刻刻都是可用,为了满足不断变化的新业务,需要不断升级更新应用程序,有时可能需要频繁的发布版本。实现"零停机"、“...

1181
来自专栏xcywt

程序员需要知道的十个操作系统的概念

说明:我之前在网上看到这篇文章觉得非常好,于是把它翻译了下来。当然很多地方翻译的很渣,见笑了。温馨提示,文章有点长。

551
来自专栏Java进阶架构师

dubbo专题-深入分析zookeeper创建节点过程(高清大图无水印版)

在之前dubbo源码解析-本地暴露中的前言部分提到了两道高频的面试题,其中一道dubbo中zookeeper做注册中心,如果注册中心集群都挂掉,那发布者和订阅者...

1102
来自专栏Jackson0714

WCF安全1-开篇

3488
来自专栏即时通讯技术

理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性

WebSocket的出现,使得浏览器具备了实时双向通信的能力。本文由浅入深,介绍了WebSocket如何建立连接、交换数据的细节,以及数据帧的格式。此外,还简要...

1032
来自专栏進无尽的文章

基础篇-服务器工作实现的浅析

对于一个前端开发的人员来说,了解服务器的基础知识,个人觉得是非常必要的,于是就有一个这篇侧重于Java的服务器相关知识的文章,只是简单介绍对于我也是一个拓展。

642
来自专栏WeTest质量开放平台团队的专栏

我这样减少了26.5M Java内存!

商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。

3110

扫码关注云+社区