开源一个自用的Android IM库,基于Netty+TCP+Protobuf实现

欢迎转载,转载请注明出处:https://juejin.im/post/5c97ae12e51d45580b681b0bhtml


1java


写在前面git

一直想写一篇关于im即时通信分享的文章,无奈工做太忙,很难抽出时间。今天终于从公司离职了,打算好好休息几天再从新找工做,趁时间空闲,决定静下心来写一篇文章,毕竟从前辈那里学到了不少东西。工做了五年半,这三四年来一直在作社交相关的项目,有 直播、 即时通信、 短视频分享、 社区论坛 等产品,深知即时通信技术在一个项目中的重要性,本着开源分享的精神,也趁这机会总结一下,因此写下这篇文章,文中有不对之处欢迎批评与指正。github

本文将介绍:数据库

Protobuf序列化TCP拆包与粘包长链接握手认证心跳机制重连机制消息重发机制读写超时机制离线消息线程池AIDL跨进程通讯编程

本想花一部分时间介绍一下利用AIDL实现多进程通讯,提高应用保活率,无奈这种方法在目前大部分Android新版本上已失效,并且也比较复杂,因此考虑再三,把AIDL这一部分去掉,须要了解的童鞋能够私信我。json

先来看看效果:windows

最终运行效果


不想看文章的同窗能够直接移步到Github fork源码:github地址[1]设计模式

接下来,让咱们进入正题。缓存


2


为何使用TCP?

这里须要简单解释一下,TCP/UDP/WebSocket的区别。 这里就很好地解释了TCP/UDP的优缺点和区别,以及适用场景[2],简单地总结一下:

优势:TCP的优势体如今稳定可靠上,在传输数据以前,会有三次握手来创建链接,并且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完以后,还会断开链接用来节约系统资源。UDP的优势体如今比TCP稍安全,UDP没有TCP拥有的各类机制,是一个无状态的传输协议,因此传递数据很是快,没有TCP的这些机制,被攻击利用的机制就少一些,可是也没法避免被攻击。缺点:TCP缺点就是效率低占用系统资源高易被攻击,TCP在传递数据以前要先创建链接,这会消耗时间,并且在数据传递时,确认机制、重传机制、拥塞机制等都会消耗大量时间,并且要在每台设备上维护全部的传输链接。UDP缺点就是不可靠不稳定,由于没有TCP的那些机制,UDP在传输数据时,若是网络质量很差,就会很容易丢包,形成数据的缺失。适用场景:TCP:当对网络通信质量有要求时,好比HTTP、HTTPS、FTP等传输文件的协议, POP、SMTP等邮件传输的协议。UDP:对网络通信质量要求不高时,要求网络通信速度要快的场景。

至于WebSocket,后续可能会专门写一篇文章来介绍。综上所述,决定采用TCP协议。


3


为何使用Protobuf?

对于App网络传输协议,咱们比较常见的、可选的,有三种,分别是json/xml/protobuf,老规矩,咱们先分别来看看这三种格式的优缺点:

优势:json优势就是较XML格式更加小巧,传输效率较xml提升了不少,可读性还不错。xml优势就是可读性强,解析方便。protobuf优势就是传输效率快(听说在数据量大的时候,传输效率比xml和json快10-20倍),序列化后体积相比Json和XML很小,支持跨平台多语言,消息格式升级和兼容性还不错,序列化反序列化速度很快。缺点:json缺点就是传输效率也不是特别高(比xml快,但比protobuf要慢不少)。xml缺点就是效率不高,资源消耗过大。protobuf缺点就是使用不太方便。

在一个须要大量的数据传输的场景中,若是数据量很大,那么选择protobuf能够明显的减小数据量,减小网络IO,从而减小网络传输所消耗的时间。考虑到做为一个主打社交的产品,消息数据量会很是大,同时为了节约流量,因此采用protobuf是一个不错的选择。


4


为何使用Netty?

首先,咱们来了解一下,Netty究竟是个什么东西。网络上找到的介绍:Netty是由JBOSS提供的基于Java NIO的开源框架,Netty提供异步非阻塞、事件驱动、高性能、高可靠、高可定制性的网络应用程序和工具,可用于开发服务端和客户端。

为何不用Java BIO?一链接一线程,因为线程数是有限的,因此这样很是消耗资源,最终也致使它不能承受高并发链接的需求。性能低,由于频繁的进行上下文切换,致使CUP利用率低。可靠性差,因为全部的IO操做都是同步的,即便是业务线程也如此,因此业务线程的IO操做也有可能被阻塞,这将致使系统过度依赖网络的实时状况和外部组件的处理能力,可靠性大大下降。为何不用Java NIO?NIO的类库和API至关复杂,使用它来开发,须要很是熟练地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等。须要不少额外的编程技能来辅助使用NIO,例如,由于NIO涉及了Reactor线程模型,因此必须必须对多线程和网络编程很是熟悉才能写出高质量的NIO程序。想要有高可靠性,工做量和难度都很是的大,由于服务端须要面临客户端频繁的接入和断开、网络闪断、半包读写、失败缓存、网络阻塞的问题,这些将严重影响咱们的可靠性,而使用原生NIO解决它们的难度至关大。JDK NIO中著名的BUG--epoll空轮询,当select返回0时,会致使Selector空轮询而致使CUP100%,官方表示JDK1.6以后修复了这个问题,其实只是发生的几率下降了,没有根本上解决。为何用Netty?API使用简单,更容易上手,开发门槛低功能强大,预置了多种编解码功能,支持多种主流协议定制能力高,能够经过ChannelHandler对通讯框架进行灵活地拓展高性能,与目前多种NIO主流框架相比,Netty综合性能最高高稳定性,解决了JDK NIO的BUG经历了大规模的商业应用考验,质量和可靠性都有很好的验证。

以上摘自:为何要用Netty开发[3]

为何不用第三方SDK,如:融云、环信、腾讯TIM?
这个就见仁见智了,有的时候,是由于公司的技术选型问题,由于用第三方的SDK,意味着消息数据须要存储到第三方的服务器上,再者,可扩展性、灵活性确定没有本身开发的要好,还有一个小问题,就是收费。好比,融云免费版只支持100个注册用户,超过100就要收费,群聊支持人数有限制等等...

融云收费


Mina其实跟Netty很像,大部分API都相同,由于是同一个做者开发的。但感受Mina没有Netty成熟,在使用Netty的过程当中,出了问题很轻易地能够找到解决方案,因此,Netty是一个不错的选择。

好了,废话很少说,直接开始吧。


5


准备工做

首先,咱们新建一个Project,在Project里面再新建一个Android Library,Module名称暂且叫作im_lib,如图所示:

新建项目

而后,分析一下咱们的消息结构,每条消息应该会有一个消息惟一id,发送者id,接收者id,消息类型,发送时间等,通过分析,整理出一个通用的消息类型,以下:msgId 消息idfromId 发送者idtoId 接收者idmsgType 消息类型msgContentType 消息内容类型timestamp 消息时间戳statusReport 状态报告extend 扩展字段根据上述所示,我整理了一个思惟导图,方便你们参考:

消息结构


这是基础部分,固然,你们也能够根据本身须要自定义比较适合本身的消息结构。咱们根据自定义的消息类型来编写proto文件。

编写proto文件

而后执行命令(我用的mac,windows命令应该也差很少):

执行protoc命令

而后就会看到,在和proto文件同级目录下,会生成一个java类,这个就是咱们须要用到的东东:

生成的protobuf java类文件

咱们打开瞄一眼:

打开的protobuf java类文件

东西比较多,不用去管,这是google为咱们生成的protobuf类,直接用就行,怎么用呢?直接用这个类文件,拷到咱们开始指定的项目包路径下就能够啦:

导入protobuf java类文件到项目中

添加依赖后,能够看到,MessageProtobuf类文件已经没有报错了,顺便把netty的jar包也导进来一下,还有fastjson的:

导入protobuf以及netty的依赖

建议用netty-all-x.x.xx.Final的jar包,后续熟悉了,能够用精简的jar包。至此,准备工做已结束,下面,咱们来编写java代码,实现即时通信的功能。



6


封装

为何须要封装呢?说白了,就是为了解耦,为了方便往后切换到不一样框架实现,而无需处处修改调用的地方。举个栗子,好比Android早期比较流行的图片加载框架是Universal ImageLoader,后期由于某些缘由,原做者中止了维护该项目,目前比较流行的图片加载框架是Picasso或Glide,由于图片加载功能可能调用的地方很是多,若是不做一些封装,早期使用了Universal ImageLoader的话,如今须要切换到Glide,那改动量将很是很是大,并且还颇有可能会有遗漏,风险度很是高。

那么,有什么解决方案呢?

很简单,咱们能够用工厂设计模式进行一些封装,工厂模式有三种:简单工厂模式、抽象工厂模式、工厂方法模式。在这里,我采用工厂方法模式进行封装,具体区别,能够参见:通俗讲讲我对简单工厂、工厂方法、抽象工厂三种设计模式的理解[4]

咱们分析一下,ims(IM Service,下文简称ims)应该是有初始化创建链接重连关闭链接释放资源判断长链接是否关闭发送消息等功能,基于上述分析,咱们能够进行一个接口抽象:

抽象的ims接口1
抽象的ims接口2

OnEventListener是与应用层交互的listener:

OnEventListener

IMConnectStatusCallback是ims链接状态回调监听器:

IMConnectStatusCallback


而后写一个Netty tcp实现类:

Netty tcp ims1
Netty tcp ims2


接下来,写一个工厂方法:

ims实例工厂方法

封装部分到此结束,接下来,就是实现了。  


7


初始化

咱们先实现init(Vector  serverUrlList, OnEventListener listener, IMSConnectStatusCallback callback)方法,初始化一些参数,以及进行第一次链接等:

初始化参数


其中,MsgDispatcher是消息转发器,负责将接收到的消息转发到应用层:

MsgDispatcher

ExecutorServiceFactory是线程池工厂,负责调度重连及心跳线程:

ExecutorServiceFactory1
ExecutorServiceFactory2
ExecutorServiceFactory3


8


链接与重连

resetConnect()方法做为链接的起点,首次链接以及重连逻辑,都是在resetConnect()方法进行逻辑处理,咱们来瞄一眼:

resetConnect

能够看到,非首次进行链接,也就是链接一个周期失败后,进行重连时,会先让线程休眠一段时间,由于这个时候也许网络情况不太好,接着,判断ims是否已关闭或者是否正在进行重连操做,因为重连操做是在子线程执行,为了不重复重连,须要进行一些并发处理。开始重连任务后,分四个步骤执行:


改变重连状态标识回调链接状态到应用层关闭以前打开的链接channel利用线程池执行一个新的重连任务

ResetConnectRunnable是重连任务,核心的重连逻辑都放到这里执行:

ResetConnectRunnable1
ResetConnectRunnable2
ResetConnectRunnable3


toServer()是真正链接服务器的地方:

toServer


initBootstrap()是初始化Netty Bootstrap:

initBootstrap

注:NioEventLoopGroup线程数设置为4,能够知足QPS是一百多万的状况了,至于应用若是须要承受上千万上亿流量的,须要另外调整线程数。参考自:netty实战之百万级流量NioEventLoopGroup线程数配置[5]


接着,咱们来看看TCPChannelInitializerHanlder

TCPChannelInitializerHandler

其中,ProtobufEncoderProtobufDecoder是添加对protobuf的支持,LoginAuthRespHandler是接收到服务端握手认证消息响应的处理handler,HeartbeatRespHandler是接收到服务端心跳消息响应的处理handler,TCPReadHandler是接收到服务端其它消息后的处理handler,先不去管,咱们重点来分析下LengthFieldPrependerLengthFieldBasedFrameDecoder,这就须要引伸到TCP的拆包与粘包啦。



9


TCP粘包与拆包

什么是TCP拆包?为何会出现TCP拆包?简单地说,咱们都知道TCP是以“流”的形式进行数据传输的,并且TCP为提升性能,发送端会将须要发送的数据刷入缓冲区,等待缓冲区满了以后,再将缓冲区中的数据发送给接收方,同理,接收方也会有缓冲区这样的机制,来接收数据。
拆包就是在socket读取时,没有完整地读取一个数据包,只读取一部分。
什么是TCP粘包?为何会出现TCP粘包?同上。
粘包就是在socket读取时,读到了实际意义上的两个或多个数据包的内容,同时将其做为一个数据包进行处理。

引用网上一张图片来解释一下在TCP出现拆包、粘包以及正常状态下的三种状况,如侵请联系我删除:

TCP拆包、粘包、正常状态

了解了TCP出现拆包/粘包的缘由,那么,如何解决呢?一般来讲,有如下四种解决方式:


消息定长用回车换行符做为消息结束标志用特殊分隔符做为消息结束标志,如\t、\n等,回车换行符其实就是特殊分隔符的一种。将消息分为消息头和消息体,在消息头中用字段标识消息总长度。

netty针对以上四种场景,给咱们封装了如下四种对应的解码器:

FixedLengthFrameDecoder,定长消息解码器LineBasedFrameDecoder,回车换行符消息解码器DelimiterBasedFrameDecoder,特殊分隔符消息解码器LengthFieldBasedFrameDecoder,自定义长度消息解码器。

咱们用到的就是LengthFieldBasedFrameDecoder自定义长度消息解码器,同时配合LengthFieldPrepender编码器使用,关于参数配置,建议参考netty--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender[6]这篇文章,讲解得比较细致。咱们配置的是消息头长度为2个字节,因此消息包的最大长度须要小于65536个字节,netty会把消息内容长度存放消息头的字段里,接收方能够根据消息头的字段拿到此条消息总长度,固然,netty提供的LengthFieldBasedFrameDecoder已经封装好了处理逻辑,咱们只须要配置lengthFieldOffset、lengthFieldLength、lengthAdjustment、initialBytesToStrip便可,这样就能够解决TCP的拆包与粘包,这也就是netty相较于原生nio的便捷性,原生nio须要本身处理拆包/粘包等问题。


10


长链接握手认证

接着,咱们来看看LoginAuthHandlerHeartbeatRespHandler

LoginAuthRespHandler是当客户端与服务端长链接创建成功后,客户端主动向服务端发送一条登陆认证消息,带入与当前用户相关的参数,好比token,服务端收到此消息后,到数据库查询该用户信息,若是是合法有效的用户,则返回一条登陆成功消息给该客户端,反之,返回一条登陆失败消息给该客户端,这里,就是在接收到服务端返回的登陆状态后的处理handler,好比:

LoginAuthRespHandler

能够看到,当接收到服务端握手消息响应后,会从扩展字段取出status,若是status=1,则表明握手成功,这个时候就先主动向服务端发送一条心跳消息,而后利用Netty的IdleStateHandler读写超时机制,按期向服务端发送心跳消息,维持长链接,以及检测长链接是否还存在等。HeartbeatRespHandler是当客户端接收到服务端登陆成功的消息后,主动向服务端发送一条心跳消息,心跳消息能够是一个空包,消息包体越小越好,服务端收到客户端的心跳包后,原样返回给客户端,这里,就是收到服务端返回的心跳消息响应的处理handler,好比:

HeartbeatRespHandler

这个就比较简单,收到心跳消息响应,无需任务处理,直接打印一下方便咱们分析便可。



11


心跳机制与读写超时机制

心跳包是按期发送,也能够本身定义一个周期,好比Android微信智能心跳方案,为了简单,此处规定应用在前台时,8秒发送一个心跳包,切换到后台时,30秒发送一次,根据本身的实际状况修改一下便可。心跳包用于维持长链接以及检测长链接是否断开等。

接着,咱们利用Netty的读写超时机制,来实现一个心跳消息管理handler:

HeartbeatHandler

能够看到,利用userEventTriggered()方法回调,经过IdleState类型,能够判断读超时/写超时/读写超时,这个在添加IdleStateHandler时能够配置,下面会贴上代码。首先咱们能够在READER_IDLE事件里,检测是否在规定时间内没有收到服务端心跳包响应,若是是,那就触发重连操做。在WRITER_IDEL事件能够检测客户端是否在规定时间内没有向服务端发送心跳包,若是是,那就主动发送一个心跳包。发送心跳包是在子线程中执行,咱们能够利用以前写的work线程池进行线程管理。
addHeartbeatHandler()代码以下:

addHeartbeatHandler

从图上可看到,在IdleStateHandler里,配置的读超时为心跳间隔时长的3倍,也就是3次心跳没有响应时,则认为长链接已断开,触发重连操做。写超时则为心跳间隔时长,意味着每隔heartbeatInterval会发送一个心跳包。读写超时没用到,因此配置为0。


onConnectStatusCallback(int connectStatus)为链接状态回调,以及一些公共逻辑处理:

onConnectStatusCallback

链接成功后,当即发送一条握手消息,再次梳理一下总体流程:


客户端根据服务端返回的host及port,进行第一次链接。链接成功后,客户端向服务端发送一条握手认证消息(1001)服务端在收到客户端的握手认证消息后,从扩展字段里取出用户token,到本地数据库校验合法性。校验完成后,服务端把校验结果经过1001消息返回给客户端,也就是握手消息响应。客户端收到服务端的握手消息响应后,从扩展字段取出校验结果。若校验成功,客户端向服务端发送一条心跳消息(1002),而后进入心跳发送周期,按期间隔向服务端发送心跳消息,维持长链接以及实时检测链路可用性,若发现链路不可用,等待一段时间触发重连操做,重连成功后,从新开始握手/心跳的逻辑。

看看TCPReadHandler收到消息是怎么处理的:

TCPReadHandler1
TCPReadHandler2

能够看到,在channelInactive()及exceptionCaught()方法都触发了重连,channelInactive()方法在当链路断开时会调用,exceptionCaught()方法在当出现异常时会触发,另外,还有诸如channelUnregistered()、channelReadComplete()等方法能够重写,在这里就不贴了,相信聪明的你一眼就能看出方法的做用。
咱们仔细看一下channelRead()方法的逻辑,在if判断里,先判断消息类型,若是是服务端返回的消息发送状态报告类型,则判断消息是否发送成功,若是发送成功,从超时管理器中移除,这个超时管理器是干吗的呢?下面讲到消息重发机制的时候会详细地讲。在else里,收到其余消息后,会立马给服务端返回一个消息接收状态报告,告诉服务端,这条消息我已经收到了,这个动做,对于后续须要作的离线消息会有做用。若是不须要支持离线消息功能,这一步能够省略。最后,调用消息转发器,把接收到的消息转发到应用层便可。


代码写了这么多,咱们先来看看运行后的效果,先贴上缺失的消息发送代码及ims关闭代码以及一些默认配置项的代码。
发送消息:

发送消息

关闭ims:

关闭ims

ims默认配置:

ims默认配置

还有,应用层实现的ims client启动器:

IMSClientBootstrap

因为代码有点多,不太方便所有贴上,若是有兴趣能够下载demo体验。额,对了,还有一个简易的服务端代码,以下:

NettyServerDemo1
NettyServerDemo2
NettyServerDemo3


12


调试

咱们先来看看链接及重连部分(因为录制gif比较麻烦,体积较大,因此我先把重连间隔调小成3秒,方便看效果)。

启动服务端:

启动服务端

启动客户端:

启动客户端

能够看到,正常的状况下已经链接成功了,接下来,咱们来试一下异常状况,好比服务端没启动,看看客户端的重连状况:

调试重连

此次咱们先启动的是客户端,能够看到链接失败后一直在进行重连,因为录制gif比较麻烦,在第三次链接失败后,我启动了服务端,这个时候客户端就会重连成功。



而后,咱们再来调试一下握手认证消息即心跳消息:

握手消息及心跳消息测试

能够看到,长链接创建成功后,客户端会给服务端发送一条握手认证消息(1001),服务端收到握手认证消息会,给客户端返回了一条握手认证状态消息,客户端收到握手认证状态消息后,即启动心跳机制。gif不太好演示,下载demo就能够直观地看到。


接下来,在讲完消息重发机制及离线消息后,我会在应用层作一些简单的封装,以及在模拟器上运行,这样就能够很直观地看到运行效果。


13


消息重发机制

消息重发,顾名思义,即便对发送失败的消息进行重发。考虑到网络环境的不稳定性、多变性(好比从进入电梯、进入地铁、移动网络切换到wifi等),在消息发送的时候,发送失败的几率其实不小,这时消息重发机制就颇有必要了。
咱们先来看看实现的代码逻辑。MsgTimeoutTimer:

MsgTimeoutTimer1
MsgTimeoutTimer2

MsgTimeoutTimerManager:

MsgTimeoutTimerManager1
MsgTimeoutTimerManager2


而后,咱们看看收消息的TCPReadHandler的改造:

加入消息重发机制的TCPReadHandler

最后,看看发送消息的改造:

加入消息重发机制的发送消息


说一下逻辑吧:发送消息时,除了心跳消息、握手消息、状态报告消息外,消息都加入消息发送超时管理器,立马开启一个定时器,好比每隔5秒执行一次,共执行3次,在这个周期内,若是消息没有发送成功,会进行3次重发,达到3次重发后若是仍是没有发送成功,那就放弃重发,移除该消息,同时经过消息转发器通知应用层,由应用层决定是否再次重发。若是消息发送成功,服务端会返回一个消息发送状态报告,客户端收到该状态报告后,从消息发送超时管理器移除该消息,同时中止该消息对应的定时器便可。
另外,在用户握手认证成功时,应该检查消息发送超时管理器里是否有发送超时的消息,若是有,则所有重发:

握手认证成功检查是否有发送超时的消息


14



因为离线消息机制,须要服务端数据库及缓存上的配合,代码就不贴了,太多太多,我简单说一下实现思路吧:客户端A发送消息到客户端B,消息会先到服务端,由服务端进行中转。这个时候,客户端B存在两种状况:

1.长链接正常,就是客户端网络环境良好,手机有电,应用处在打开的状况。2.废话,那确定就是长链接不正常咯。这种状况有不少种缘由,好比wifi不可用、用户进入了地铁或电梯等网络很差的场所、应用没打开或已退出登陆等,总的来讲,就是没有办法正常接收消息。

若是是长链接正常,那没什么可说的,服务端直接转发便可。
若是长链接不正常,须要这样处理:服务端接收到客户端A发送给客户端B的消息后,先给客户端A回复一条状态报告,告诉客户端A,我已经收到消息,这个时候,客户端A就不用管了,消息只要到达服务端便可。而后,服务端先尝试把消息转发到客户端B,若是这个时候客户端B收到服务端转发过来的消息,须要立马给服务端回一条状态报告,告诉服务端,我已经收到消息,服务端在收到客户端B返回的消息接收状态报告后,即认为此消息已经正常发送,不须要再存库。若是客户端B不在线,服务端在作转发的时候,并无收到客户端B返回的消息接收状态报告,那么,这条消息就应该存到数据库,直到客户端B上线后,也就是长链接创建成功后,客户端B主动向服务端发送一条离线消息询问,服务端在收到离线消息询问后,到数据库或缓存去查客户端B的全部离线消息,并分批次返回,客户端B在收到服务端的离线消息返回后,取出消息id(如有多条就取id集合),经过离线消息应答把消息id返回到服务端,服务端收到后,根据消息id从数据库把对应的消息删除便可。
以上是单聊离线消息处理的状况,群聊有点不一样,群聊的话,是须要服务端确认群组内全部用户都收到此消息后,才能从数据库删除消息,就说这么多,若是须要细节的话,能够私信我。


不知不觉,NettyTcpClient中定义了不少变量,为了防止你们不明白变量的定义,仍是贴上代码吧:

定义了不少变量的NettyTcpClient


15


应用层封装

这个就见仁见智啦,每一个人代码风格不一样,我把本身简单封装的代码贴上来吧:
MessageProcessor消息处理器:

MessageProcessor1
MessageProcessor2

IMSEventListener与ims交互的listener:

IMSEventListener1
IMSEventListener2
IMSEventListener3

MessageBuilder消息转换器:

MessageBuilder1
MessageBuilder2
MessageBuilder3

AbstractMessageHandler抽象的消息处理handler,每一个消息类型对应不一样的messageHandler:

AbstractMessageHandler

SingleChatMessageHandler单聊消息处理handler: 

SingleChatMessageHandler

GroupChatMessageHandler群聊消息处理handler: 

GroupChatMessageHandler

MessageHandlerFactory消息handler工厂:

MessageHandlerFactory

MessageType消息类型枚举:

MessageType

IMSConnectStatusListenerIMS链接状态监听器:

IMSConnectStatusListener

因为每一个人代码风格不一样,封装代码都有本身的思路,因此,在此就不过多讲解,只是把本身简单封装的代码所有贴上来,做一个参考便可。只须要知道,接收到消息时,会回调OnEventListener的dispatchMsg(MessageProtobuf.Msg msg)方法:

应用层接收ims消息入口

发送消息须要调用imsClient的sendMsg(MessageProtobuf.Msg msg)方法:

应用层调用ims发送消息入口

便可,至于怎样去封装得更好,你们自由发挥吧。



最后,为了测试消息收发是否正常,咱们须要改动一下服务端:

改动后的服务端1
改动后的服务端2
改动后的服务端3
改动后的服务端4
改动后的服务端5

能够看到,当有用户握手成功后,会保存该用户对应的channel到容器里,给用户发送消息时,根据用户id从容器里取出对应的channel,利用该channel发送消息。当用户断开链接后,会把该用户对应的channel从容器里移除掉。


运行一下,看看效果吧:

最终运行效果


首先,启动服务端。而后,修改客户端链接的ip地址为192.168.0.105(这是我本机的ip地址),端口号为8855,fromId,也就是userId,定义成100001,toId为100002,启动客户端A。再而后,fromId,也就是userId,定义成100002,toId为100001,启动客户端B。客户端A给客户端B发送消息,能够看到在客户端B的下面,已经接收到了消息。用客户端B给客户端A发送消息,也能够看到在客户端A的下面,也已经接收到了消息。至于,消息收发测试成功。至于群聊或重连等功能,就不一一演示了,仍是那句话,下载demo体验一下吧。。。

因为gif录制体积较大,因此只能简单演示一下消息收发,具体下载demo体验吧。。。

若是有须要应用层UI实现(就是聊天页及会话页的封装)的话,我再分享出来吧。

github地址[7]




发现的bug

1.MsgTimeoutTimer

MsgTimeoutTimer bug1

这个bug是本身在检查代码时发现的,多是连续熬几天夜写文章魔怔了。。。修改以下:

MsgTimeoutTimer bug1 fix


一我的精力有限,你们在使用过程当中,若是发现其它bug,烦请告诉我,反正我是会虚心接受,坚定不改,呸,必定改,必定改。另外,欢迎fork,期待你们与我一块儿完善。。。


16


写在最后

终于写完了,这篇文章大概写了10天左右,有很大部分的缘由是本身有拖延症,每次写完一小段,总静不下心来写下去,致使一直拖到如今,之后得改改。第一次写技术分享文章,有不少地方也许逻辑不太清晰,因为篇幅有限,也只是贴了部分代码,建议你们把源码下载下来看看。一直想写这篇文章,之前在网上也尝试过找过不少im方面的文章,都找不到一篇比较完善的,本文谈不上完善,但包含的模块不少,但愿起到一个抛砖引玉的做用,也期待着你们跟我一块儿发现更多的问题并完善,最后,若是这篇文章对你有用,但愿在github上给我一个star哈。。。

应你们要求,精简了netty-all-4.1.33.Final.jar包。原netty-all-4.1.33.Final.jar包大小为3.9M,经测试发现目前im_lib库只须要用到如下jar包:

netty-buffer-4.1.33.Final.jarnetty-codec-4.1.33.Final.jarnetty-common-4.1.33.Final.jarnetty-handler-4.1.33.Final.jarnetty-resolver-4.1.33.Final.jarnetty-transport-4.1.33.Final.jar

因此,抽取以上jar包,从新打成了netty-tcp-4.1.33-1.0.jar,目前自测没有问题,若是发现bug,请告诉我,谢谢。

附上原jar及裁剪后jar包的大小对比:

原netty-all-4.1.33.Final.jar大小

裁剪后netty-tcp-4.1.33-1.0.jar大小

代码已更新到Github.




接下来,会抽时间把下图想写的文章都写了,没有前后顺序,想到哪就写到哪吧。。。

想写的文章


另外,建立了一个Android即时通信技术交流QQ群:1015178804,有须要的同窗能够加进来,不懂的问题,我会尽可能解答,一块儿学习,一块儿成长。

The end.

References

[1] github地址: https://github.com/FreddyChen/NettyChat
[2] 这里就很好地解释了TCP/UDP的优缺点和区别,以及适用场景: https://www.cnblogs.com/Leonardo-li/p/8206945.html
[3] 为何要用Netty开发: https://blog.csdn.net/xu_melon/article/details/79201198
[4] 通俗讲讲我对简单工厂、工厂方法、抽象工厂三种设计模式的理解: https://blog.andyqiao.top/article/12/
[5] netty实战之百万级流量NioEventLoopGroup线程数配置: https://blog.csdn.net/linsongbin1/article/details/77698479
[6] netty--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender: https://blog.csdn.net/u010853261/article/details/55803933
[7] github地址: https://github.com/FreddyChen/NettyChat


本文分享自微信公众号 - FreddyChen(FreddyChenAndroid)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索