终于可以开始写这个系列的文章了,本系列文章预计将分为13篇,由于IM涉及的知识点稍复杂,所以每个知识点都会单独用一篇文章来阐述,尽量讲透彻,方便大家理解。
为什么需要写这个系列的文章呢?
可能大家会问,有了之前的NettyChat和开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现,为什么还需要写这个系列的文章呢?主要是因为一开始开源NettyChat和发布文章的时候,旨在起一种抛砖引玉的作用,带领大家入门IM而已。而且一篇文章难以阐述所有的知识点,加上NettyChat也是一个Demo,有些代码写得不严谨。一年多以来,从群里的反馈、文章的评论可以看到大家对这一块知识有不少的需求,大家去集成NettyChat到自己项目里也比较麻烦,网上也缺乏完整的IM实现,大多是零零碎碎的知识点。所以打算从零开始,手把手教大家实现自己的IM系统。
对比NettyChat有什么新增的功能和优化?
根据大家的反馈,支持TCP/WebSocket、Protobuf/Json等。优化消息重发管理器,不再是一条消息一个Timer实现(严重浪费资源,影响程序性能)。另外,优化代码结构,提升可扩展性、可维护性等。
项目包含服务端代码吗?
这也是重新写本系列文章的重要原因,在本系列文章中,将包含Java服务端代码及Android代码,至于IOS,后续有时间会增加。文章完成后,会在Github开源三个项目,分别是:
本系列文章将包含:
本文为第一篇:技术选型及协议定义。
首先,讲一下项目整体架构以及使用到的开源框架。根据群里小伙伴的建议以及大家对Java
及Kotlin
的熟悉程度,Android端开发语言还是采用Java
,后续有时间会考虑用Kotlin
开发一个版本。
由于项目未完成,目前是边写文章边写代码的方式,所以在后续项目完成后,会专门写一篇文章介绍项目架构,包括Android客户端
和Java服务端
。
Android客户端
项目整体采用MVP架构,用到的开源库如下:
Java服务端
还在开发中,就不一一列举了。
感谢以上开源库的作者。
以下的通信协议选型、传输协议选型、通信框架选型在开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现文章中有说明过,但既然打算重新写本系列的文章,就在此重新讲解一下,一来加入自己新的理解,二来可以整合到本系列文章里。
以下所涉及的原理和理论知识,由于篇幅原因及作者水平有限,没办法详细阐述。本系列文章的着重点在于教大家怎么去实现自己的IM系统,至于偏理论的知识,作者帮大家找了一下原文链接,感兴趣的可以跳转原文阅读。同时感谢以下文章的作者。
常用的通信协议有以下几种,分别简单地讲讲每种通信协议的优缺点及适用场景。
UDP
简单概述
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。比如:
实时音视频聊天,这种情况丢一些帧影响不大,不需要重传,对传输速度要求高。
遥控器,丢一些指令不影响。
TCP
注:作者给大家找了几篇很不错的文章,有兴趣的话可以阅读:
WebSocket
WebSocket
的命名可以看出,比较适合Browser的即时通讯实现。注:WebSocket和HTTP一样都是基于TCP的应用层协议。握手部分的设计目的就是兼容现有的基于HTTP的服务端组件(web服务器软件)或者中间件(代理服务器软件)。这样一个端口就可以同时接受普通的HTTP请求或者WebSocket请求了。为了这个目的,WebSocket客户端的握手是一个 HTTP升级版的请求(HTTP Upgrade request)。感兴趣可以阅读以下文章:
MQTT
作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。在MQTT协议中,一个MQTT数据包由固定头(Fixed header)、可变头(Variable header)、消息体(payload)三部分构成:
总的来说,MQTT是在TCP之上的应用层协议,对物联网应用环境做了非常多的优化,TCP传输层协议,是更通用层的协议。
当然,以上用TCP自己开发协议也能实现,那为什么需要MQTT呢?其实就是MQTT另外还实现了很多功能,降低了开发复杂度,比如:心跳机制、异步机制、遗嘱消息、订阅发布机制,QoS消息质量等,而且MQTT做了一些优化,比如消息头最小只有两个字节等。所以,可以简单理解为,MQTT其实就是TCP协议的一种封装实现,在TCP的基础上做了一系列优化,并且封装了很多实用的机制,一句话总结:MQTT就是观察者模式的网络放大版。
缺点
同TCP。另外,虽然MQTT封装了很多机制,但还是不够成熟,实现起来较复杂。
适用场景
物联网IoT
即时通讯IM
嵌入式开发设备(不能经常联网或网络环境较差)
推送
车联网平台
其它协议开销较小的场景等
注:感兴趣可阅读以下文章:
常用的传输协议有以下几种,分别讲讲每种传输协议的优缺点及使用场景。
JSON
XML
Protobuf
常用的通信框架有以下几种,分别讲讲每种通信框架的优缺点及使用场景。
Java Nio
传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。NIO主要的事件有:
更多细节可以查看官方文档介绍。
Mina
Netty
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
文件,文件自动生成,不需要改动: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.