终于可以开始写这个系列的文章了,本系列文章预计将分为 13 篇,由于IM涉及的知识点稍复杂,所以每个知识点都会单独用一篇文章来阐述,尽量讲透彻,方便大家理解。
可能大家会问,有了之前的 NettyChat 和 开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现 ,为什么还需要写这个系列的文章呢?主要是因为一开始开源NettyChat和发布文章的时候,旨在起一种抛砖引玉的作用,带领大家入门IM而已。而且一篇文章难以阐述所有的知识点,加上NettyChat也是一个Demo,有些代码写得不严谨。一年多以来,从群里的反馈、文章的评论可以看到大家对这一块知识有不少的需求,大家去集成NettyChat到自己项目里也比较麻烦,网上也缺乏完整的IM实现,大多是零零碎碎的知识点。所以打算从零开始,手把手教大家实现自己的IM系统。
根据大家的反馈,支持TCP/WebSocket、Protobuf/Json等。优化消息重发管理器,不再是一条消息一个Timer实现(严重浪费资源,影响程序性能)。另外,优化代码结构,提升可扩展性、可维护性等。
这也是重新写本系列文章的重要原因,在本系列文章中,将包含 Java 服务端代码及Android代码,至于IOS,后续有时间会增加。文章完成后,会在 Github 开源三个项目,分别是:
其实就是一时兴起,Kula是作者比较喜欢的SNK拳皇系列里的一个角色,没什么具体含义。附上Kula高清图:
大纲如图:
本系列文章将包含:
本文为第一篇: 技术选型及协议定义 。
首先,讲一下项目整体架构以及使用到的开源框架。根据群里小伙伴的建议以及大家对 Java
及 Kotlin
的熟悉程度,Android端开发语言还是采用 Java
,后续有时间会考虑用 Kotlin
开发一个版本。
由于项目未完成,目前是边写文章边写代码的方式,所以在后续项目完成后,会专门写一篇文章介绍项目架构,包括 Android客户端
和 Java服务端
。
Android客户端项目整体采用MVP架构,用到的开源库如下:
Java服务端
还在开发中,就不一一列举了。
感谢以上开源库的作者。
常用的通信协议有以下几种,分别 简单地 讲讲每种通信协议的优缺点及适用场景。
UDP是一个面向报文的、非连接的协议,也就是无连接的。即发送数据前,双方无需建立连接,数据发送完毕后,也无需断开连接(没有连接可断开)。这样一来,减少了连接和断开连接的开销(无需像TCP一样连接时需要三次握手,断开连接时需要四次挥手)。同时,UDP不存在拥塞机制,即网络拥塞时,不会使源主机的发送速率降低。另外,UDP一个很大的特点就是尽最大努力交付(即不保证交付),也就是可能会存在丢包、乱序等情况。 总的来说,UDP是一个不可靠的协议 。
优点
效率高
由于UDP在发送数据前,无需建立连接,并且没有TCP一系列的确认机制、重传机制、拥塞机制等,所以在数据传输上,效率较高。
开销小
UDP首部开销仅8个字节(源端口[16bit],目的端口[16bit],长度[16bit]、校验和[16bit])。
稍安全
UDP没有TCP拥有的各种机制,被攻击利用的机制就少一些,但是也无法避免被攻击。
支持 广播 、 单播 、 组播
缺点
不可靠
由于UDP没有TCP一系列的可靠性机制保证传输,在网络质量不好时,很容易丢包。所以在使用UDP作为通信协议时,往往需要自己实现可靠性保证,例如确认重传等。据说QQ早期是使用UDP作为通信协议,自己在UDP的基础上,实现类似TCP的可靠性保证,这样一来,既可实现高速率的传输,又可兼顾可靠性。网上找到的一篇讨论文: 为什么QQ用的是UDP协议而不是TCP协议? ,感兴趣的可以看看。
对网络通讯质量要求不高、实时性要求较高的情况下,可用UDP。比如:
实时音视频聊天,这种情况丢一些帧影响不大,不需要重传,对传输速度要求高。
遥控器,丢一些指令不影响。
WebSocket
的命名可以看出,比较适合Browser的即时通讯实现。 注:WebSocket和HTTP一样都是基于TCP的应用层协议。握手部分的设计目的就是兼容现有的基于HTTP的服务端组件(web服务器软件)或者中间件(代理服务器软件)。这样一个端口就可以同时接受普通的HTTP请求或者WebSocket请求了。为了这个目的,WebSocket客户端的握手是一个 HTTP升级版的请求(HTTP Upgrade request)。感兴趣可以阅读以下文章:
MQTT是一种基于发布/订阅模式的“轻量级”通信协议,也是TCP长连接应用的一种(当然中间可能多一层WebSocket)。为什么既然有了TCP和WebSocket了,还需要有MQTT的存在呢?这是因为,MQTT有一个很大的优点: 可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务 ,同时根据网络环境不同,可以选择三种消息发布的质量(QoS Level):
作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。在MQTT协议中,一个MQTT数据包由固定头(Fixed header)、可变头(Variable header)、消息体(payload)三部分构成:
由于MQTT是基于TCP实现的协议,所以优点跟TCP基本相同。同时,MQTT也是标准的RFC协议,相比于私有协议而言更加标准,优点有:
当然,以上用TCP自己开发协议也能实现,那为什么需要MQTT呢?其实就是MQTT另外还实现了很多功能,降低了开发复杂度,比如:心跳机制、异步机制、遗嘱消息、订阅发布机制,QoS消息质量等,而且MQTT做了一些优化,比如消息头最小只有两个字节等。所以,可以简单理解为, MQTT其实就是TCP协议的一种封装实现,在TCP的基础上做了一系列优化,并且封装了很多实用的机制,一句话总结:MQTT就是观察者模式的网络放大版 。
同TCP。另外,虽然MQTT封装了很多机制,但还是不够成熟,实现起来较复杂。
适用场景
物联网IoT
即时通讯IM
嵌入式开发设备(不能经常联网或网络环境较差)
推送
车联网平台
其它协议开销较小的场景等
常用的传输协议有以下几种,分别讲讲每种传输协议的优缺点及使用场景。
常用的通信框架有以下几种,分别讲讲每种通信框架的优缺点及使用场景。
Java NIO 是 java 1.4, 之后新出的一套面向缓冲区、非阻塞的IO接口。既可以说是New IO,也有人认为是No-Blocking IO,但这种观点不太严谨。NIO不仅仅就是等于Non-blocking IO(非阻塞IO),NIO中有实现非阻塞IO的具体类,但不代表NIO就是Non-blocking IO(非阻塞IO)。Java NIO由以下几个核心部分组成:
传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。NIO主要的事件有:
更多细节可以查看官方文档介绍。
Socket.IO
Socket.IO也是一个开源框架,可用于在浏览器和服务器之间进行实时,双向和基于事件的通信。用得比较少,就不详细介绍了。
主要是讲讲 Protobuf
的文件格式定义, JSON
就是key/value键值对,没什么好说的。
我们先分析一下,怎样的消息格式,才算是通用的,也就是单聊、群聊、系统消息等,都可以用的统一消息格式,这个比较重要,关系到后续的扩展性、通用性等,先看个图:
对应编写的msg.proto
代码如下:
syntax = "proto3";// 指定protobuf版本 option java_package = "com.freddy.kulaims.protobuf";// 指定包名 option java_outer_classname = "MessageProtobuf";// 指定类名 message Msg { Head head = 1;// 消息头 Body body = 2;// 消息体 } message Head { string msgId = 1;// 消息id int32 msgType = 2;// 消息类型 string sender = 3;// 发送者 string receiver = 4;// 接收者 int64 timestamp = 5;// 发送时间戳,单位:毫秒 int32 report = 6;// 消息发送状态报告 } message Body { string content = 1;// 消息内容 int32 contentType = 2;// 消息内容类型 string data = 3;// 扩展字段,以key/value形式存储的json字符串 } 复制代码
编写完 msg.proto
文件后,通过以下步骤即可生成我们需要用到的 MessageProtobuf
Java类:
在项目 src/main
目录下,新建 proto
文件夹,与 src/main/java
同级。
将 msg.proto
文件复制到项目 src/main/proto
文件夹。
在 project
级的 build.gradle
文件的 dependencies
节点下,加入
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12' 复制代码
app
级的 build.gradle
文件,加入 apply plugin: 'com.google.protobuf' 复制代码
app
级的 build.gradle
文件的 android
节点,加入 sourceSets { main { java { srcDir 'src/main/java' } proto { srcDir 'src/main/proto' } } } 复制代码
app
级的 build.gradle
文件的 dependencies
节点,加入 implementation 'com.google.protobuf:protobuf-java:3.8.0' 复制代码
app
级的 build.gradle
文件根节点(也就是与 android
、 dependencies
节点同级),加入 protobuf { //配置protoc编译器 protoc { artifact = 'com.google.protobuf:protoc:3.8.0' } //这里配置生成目录,编译后会在build的目录下生成对应的java文件 generateProtoTasks { all().each { task -> task.builtins { remove java } task.builtins { java {} } } } } 复制代码
点击 build->Make Project
,即可在项目生成的 build/generated/source/proto/debug/java/proto文件java_package指定的包名
下看到生成的 MessageProtobuf.java
文件,文件自动生成,不需要改动:
注:以上protobuf版本不是规定的,大家可以选择自己喜欢的版本,但强烈建议前后端版本一致,否则有可能会出现兼容性的问题。
不少同学也会用多个proto文件来表示不同消息,比如用户登录消息 user_login.proto
、聊天消息 chat.proto
,这样也未尝不可,只是这样会有很多个proto文件,后期维护比较麻烦,这也就是为什么需要设计通用的proto文件格式的原因。
最后,贴一张 JSON
和 Protobuf
序列化后的字节长度对比图,两个User对象和一个timestamp字段,可以看到json序列化后,字节长度为 140 ,而同样的内容在Protobuf序列化后,字节长度为 49 :
拿到 MessageProtobuf.java
文件,意味着我们已经完成了第一步,距离我们开发完成的商业级IM系统又接近了一步,在下一篇文章,我将会详细介绍 接口定义及封装 ,敬请期待。
综上所述,在即时通讯方面,最终技术选型如下:
通信协议采用 TCP
和 WebSocket
两种, UDP
不考虑,至于 MQTT
,后续如果有需要的话会考虑实现。
传输协议采用 Protobuf
和 JSON
两种,在IM SDK初始化时指定。 XML
不考虑。
通信框架采用 Netty
,后续如果有需要,会采用 Java NIO
和 Mina
实现。 Socket.IO
不考虑。
之前在 开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现 ,有同学评论,TCP是面向字节流的,没有包的概念,哪来的拆包/粘包的说法呢?首先说明,作者不会误导大家,TCP确实没有拆包/粘包的说法,相关的TCP/IP书籍上也没有提到过,这个说法只是误传,但已经深入人心,所以作者也就用这词了。拆包/粘包的概念应只存在应用层,TCP不存在粘包/拆包的说法,只是没有消息边界而已。后续在第3篇文章,会专门解释。
终于写完了,发现写原创文章太难了,一来要考虑表述的方式,二来要考虑排版是否美观,还要考虑是否符合大家的需要,所以拖延症又发作了~ 但会坚持把整个系列的文章都写完,把项目完善并开源,希望对大家有所帮助。之所以分系列文章来写,一方面是因为一篇文章实在没办法讲清楚。另一方面,希望在写文章的过程中,大家可以给我提点意见或建议。一个人精力及水平有限,有很多观点也许不太正确和完善,希望大家体谅。欢迎吐槽,欢迎拍砖,接受批评。
PS:新开的公众号不能留言,如果大家有不同的意见或建议,可以到掘金上评论或者加到QQ群:1015178804,如果群满人的话,也可以在公众号给我私信,谢谢。
The end.