移动互联网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 条评论
登录 后参与评论

相关文章

来自专栏架构师之路

im协议设计选型(上)

im协议设计选型(上) 周末在一个Qcon群里分享了一些im技术,抽取出其中im协议选型相关的内容,跟大家分享。 分享人:58沈剑,58同城技术委员会主席,高级...

34910
来自专栏IT技术精选文摘

微服务架构中的进程间通信

介绍 在单体应用程序中,组件通过语言级的方法或函数调用进行彼此的调用。相比之下,基于微服务的应用程序是在多台机器上运行的分布式系统。每个服务实例通常是一个进程...

2315
来自专栏高性能服务器开发

服务器开发中网络数据分析与故障排查经验漫谈

一、 操作系统提供的网络接口 为了能更好的排查网络通信问题,我们需要熟悉操作系统提供的以下网络接口函数,列表如下: 接口函数名称接口函数描述接口函数签名soc...

2695
来自专栏Golang语言社区

linux服务器开发三(网络编程) --二

半关闭 当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收...

3977
来自专栏杨建荣的学习笔记

数据库收缩数据文件的尝试(三)(r11笔记第22天)

不知道大家在数据库运维中是否会有这样的困扰,一个数据文件里没有多少数据,但是数据文件的大小却调不下来,尝试使用resize来调整屡屡失败。如果一个数据文件里...

33312
来自专栏EAWorld

DevOps的支撑服务:K8s容器管理与应用部署

? 大家好,本期微课堂介绍在新一代数字化企业云平台中对于Kubernetes的学习以及使用的总结。 ? 本次分享分为两部分: 1.介绍Kubernetes是什...

3667
来自专栏Java开发者杂谈

ActiveMQ专题2: 持久化

​ 前面一篇AMQ专题中,我们发现对于Topic这种类型的消息,即使将deliveryMode设置为持久化,只要生产者在消费者之前启动。消息生产者发布的消息还是...

513
来自专栏大闲人柴毛毛

java处理高并发高负载类网站的优化方法

一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF。尤其是Web2.0的应用,数据库的响应是首先要解决的。   ...

4376
来自专栏张绍文的专栏

基于 TLS 1.3的微信安全通信协议 mmtls 介绍(下)

随着近些年网络安全事情的频繁发生,使得用户对网络通信安全的意识越来越强。国内外的网络服务提供商都逐渐提供全站的安全通信服务,如国内的淘宝、百度先后宣布已经完成了...

5721
来自专栏聊聊技术

原 华为网络工程师认证HCNA R&S笔记

2668

扫码关注云+社区