1.写在前面

 

 我一直想写一篇关于分享的文章,但是我太忙了,没有时间。今天,我终于离开了公司。我计划在再次找工作之前好好休息几天。我决定冷静下来,有空的时候写一篇文章。毕竟,我从我的前任那里学到了很多。

 

 在工作了五年半之后,在过去的三四年里,我一直在做与社会相关的项目,包括直播、即时通讯、视频分享、社区论坛和其他产品。我知道即时通讯技术在项目中的重要性,并借此机会总结开源共享的精神,所以我写了这篇文章。

 

 *重要提示:本文不是一篇即时通讯理论文章,但文章的内容都是由实战代码组织的。如果你对即时通讯(即时通讯)技术理论知之甚少,建议你仔细阅读它:“初学者足够了:从头开始开发移动即时通讯”。这个网站上的其他文章,“拿起键盘是干的:用我的双手开发一个分布式即时消息系统”,“适合初学者:从头开始开发一个即时消息服务器(基于Netty,带有完整的源代码)”和“适合初学者:教你用Go(带有源代码)构建一个高性能和可扩展的即时消息系统”,也值得一读。

 

 本文的实际内容将涉及以下即时通讯技术内容:

 

 

 1)Protobuf序列化;

 2)TCP拆包和粘贴;

 3)长连接握手认证;

 4)心跳机制;

 5)重连机制;

 6)消息重传机制;

 7)读写超时机制;

 8)离线消息;

 9)线程池。

 

 不想阅读本文的学生可以直接从Github下载本文的源代码:

 

 

 1)原地址:https://github.com/FreddyChen/NettyChat

 2)备用地址:https://github。com/52im/nettychat

 

 接下来,让我们言归正传。

 

 2.本文的阅读对象

 

 这篇文章适合没有开发经验的白人小开发者。它将教你如何从头开始开发一个典型的基于Netty+TCP+Protobuf的即时通讯(即时消息)系统。它非常适合从头开始的安卓开发者。

 

 本文不适合没有编程的潜在开发人员,因为即时通讯(IM)系统属于特定的业务领域,如果您几乎不能编写一般的逻辑代码,不建议您阅读本文。这篇文章显然不是编程语言的入门教程。

 

 此外,本网站上另一篇类似的文章“拉起键盘是干的:一个徒手的分布式即时通讯系统”也值得一读。

 

 3.关于作者

 

 自己开发即时通讯有这么难吗?教你自己创建一个简单的即时消息版本的Andriod(源代码)_WX20190721-165501@2x.jpg)

 这篇文章的原始内容最初由FreddyChen共享。作者现在从事安卓程序开发。他的技术博客地址是https://金爵。im/user/5bd 7 aff be 51d 4547 f 763 Fe 72

 

 4.为什么要使用传输控制协议?

 

 在这里,我们需要简单地解释一下TCP/UDP的区别,并简单地总结一下。

 

 优势:

 

 

 1)TCP:其优点是稳定可靠。在数据传输之前,会有一个三次握手来建立连接。数据传输时,将有确认、窗口、重传和拥塞控制机制。数据传输后,连接将被断开,以节省系统资源。

 2)UDP:它的优点是比TCP更快、更安全。UDP是一种无状态传输协议,没有TCP所拥有的各种机制,因此它可以非常快速地传输数据。如果没有TCP的这些机制,被攻击使用的机制会更少,但是无法避免被攻击。

 

 缺点:

 

 

 1)TCP:它的缺点是速度慢、效率低、系统资源高、易受攻击。TCP需要在传输数据之前建立连接,这将消耗时间。此外,当传输数据时,确认机制、重传机制和拥塞机制将消耗大量时间,并且所有传输连接都应该在每个设备上维护。

 2)UDP:缺点是不可靠和不稳定,因为没有TCP机制。UDP传输数据时,如果网络质量不好,很容易丢失数据包,导致数据丢失。

 

 适用场景:

 

 

 1)TCP:当对网络通信质量有要求时,如HTTP、HTTPS、FTP等文件传输协议,POP、SMTP等邮件传输协议。

 2)UDP:当网络通信质量不高时,要求网络通信速度快。

 

 至于WebSocket,稍后可能会写一篇特别的文章来介绍它。总而言之,我们决定采用TCP协议。

 

 有关TCP和UDP的比较和选择的详细文章,请参见:

 

 

 简单介绍了TCP和UDP的区别

 为什么QQ使用UDP而不是TCP?》

 "移动即时通讯协议选择:UDP还是TCP?》

 网络编程中的懒人介绍(4):快速理解TCP和UDP的区别

 网络编程中的懒人入门(5):快速理解为什么UDP有时优于TCP

 安卓程序员必须知道的网络通信传输层协议——UDP和TCP

 

 或者,如果您对TCP和UDP协议知之甚少,您可以阅读本文:

 

 

 TCP/IP的详细说明-第11章UDP:用户数据报协议

 TCP/IP的详细说明-第17章TCP:传输控制协议

 TCP/IP的详细说明-第18章TCP连接的建立和终止

 TCP/IP的详细说明-第21章TCP的超时和重传

 脑残网络编程导论(1):学习TCP三次握手和四波动画

 技术过去:TCP/IP协议改变世界(珍贵的多画面,手机注意)

 易于理解——对TCP协议的深刻理解(一):理论基础

 网络编程中的懒人介绍(3):快速理解TCP协议就足够了

 更高层次:优秀安卓程序员必须了解的网络基础。

 

 

 5.为什么使用原蟾?

 

 有三种常见的和可选的应用网络传输协议,即json/xml/protobuf,它们是旧的规则。让我们先分别看看这三种格式的优缺点。

 

 附言:如果您不知道什么是protobuf,建议您详细阅读:Protobuf通信协议详细说明:代码演示、详细原理介绍等。

 

 优势:

 

 

 1)json:它的优点是比xml格式小,传输效率比XML高得多,可读性也不错。

 2)xml:它的优点是可读性强,解析方便。

 3)protobuf:优点是传输效率快(据说当数据量大时,传输效率比xml和json快10-20倍)。序列化后,体积比Json和XML小,并且支持跨平台多语言。消息格式升级和兼容性还不错,序列化和反序列化速度非常快。

 

 缺点:

 

 

 1)json:缺点是传输效率不是特别高(比xml快,但比protobuf慢得多)。

 2)xml:缺点是低效率和过度的资源消耗。

 3)原虫:缺点是使用不方便。

 

 在需要大量数据传输的场景中,如果数据量很大,protobuf可以明显减少数据量和网络IO,从而减少网络传输所消耗的时间。考虑到作为一种社交产品,消息数据量将非常大,为了节省流量,protobuf是一个不错的选择。

 

 有关即时消息相关协议格式选择的更多文章,请进一步阅读:

 

 

 如何选择即时通讯应用的数据传输格式

 强烈建议使用Protobuf作为您的即时通讯应用程序数据传输格式

 综合评估:Protobuf的性能比JSON快五倍吗?》

 移动即时通信发展中面临的技术问题(包括通信协议选择)

 简要描述移动即时消息开发的难点:架构设计、通信协议和客户端

 理论与实践相结合:典型即时通信协议的详细设计

 分享技术实践,如58户实时信息系统的协议设计

 详细解释如何在节点中使用谷歌的原型

 技术素养:新一代基于UDP的低延迟网络传输层协议

 “金蝶手写团队分享:仍在使用JSON?Protobuf使数据传输更经济、更快(原理)

 “金蝶手写团队分享:仍在使用JSON?Protobuf使数据传输越来越快(实战)

 > >更多类似的文章...

 

 6.为什么使用Netty?

 

 首先,让我们看看什么是内蒂。网络上的介绍:Netty是一个基于JBOSS提供的Java NIO的开源框架。Netty提供异步无阻塞、事件驱动、高性能、高可靠性和高度可定制的网络应用程序和工具,可用于开发服务器和客户端。

 

 附言:如果你不知道经典的信息作战、网络作战或网络作战框架,请阅读以下文章:

 

 

 历史上最强大的Java NIO简介:如果你担心开始和放弃,请阅读这篇文章!》

 “放开我!让您在一分钟内了解Java NIO和经典IO之间的区别

 初学者:网络的学习方法和高级策略——一个Java高性能NIO框架

 NIO框架的详细说明:Netty的高性能

 

 为什么不使用Java生物?

 

 

 1)一个连接一个线程:由于线程数量有限,消耗大量资源,最终无法满足高并发连接的需求。

 2)低性能:频繁的上下文切换导致CUP的利用率低。

 3)可靠性差:由于所有的IO操作都是同步的,即使对于业务线程,业务线程的IO操作也可能被阻塞,这将导致系统过于依赖网络的实时性和外部组件的处理能力,从而大大降低了可靠性。

 

 为什么不使用Java NIO呢?

 

 

 1)NIO的类库和API相当复杂。要使用它进行开发,您需要掌握选择器、字节缓冲、服务器套接字通道、套接字通道等。

 2)需要许多额外的编程技巧来帮助NIO的使用。例如,因为NIO涉及反应器线程模型,所以有必要熟悉多线程和网络编程来编写高质量的NIO程序。

 3)要具有高可靠性,工作量和难度都很大,因为服务器需要面对频繁的客户端访问和断开、网络闪烁、半包读写、故障缓存和网络阻塞等问题,这些问题会严重影响我们的可靠性,用本机NIO解决起来相当困难。

 4)JDK NIO BUG - epoll空轮询中的著名错误,当select返回0时,将导致选择器的空轮询,并导致100%的CUP。这位官员说,这个问题在JDK1.6之后已经解决了,但实际上,发生的可能性降低了,而且没有从根本上解决。

 

 为什么使用Netty?

 

 

 1)应用编程接口简单易用,开发门槛低;

 2)功能强大,预置多种编解码功能,支持多种主流协议;

 3)定制能力强,可以通过ChannelHandler灵活扩展通信框架;

 4)高性能。与许多NIO主流框架相比,Netty具有最高的综合性能;

 5)稳定性高,解决了BUGJDK NIO;

 6)经历了大规模商业应用的测试,质量和可靠性得到了很好的验证。

 

 为什么不使用第三方软件开发工具包,如融云、环欣和腾讯TIM?

 

 这是一个意见问题。有时,这是因为公司的技术选择,因为使用第三方SDK意味着消息数据需要存储在第三方服务器上。此外,可伸缩性和灵活性肯定不如我们自己开发的那些。还有一个小问题,那就是充电。例如,融云的免费版只支持100个注册用户,超过100个就要收费,群聊支持者的数量有限,等等...

 

 自己开发即时通讯有这么难吗?手工1.jpg教你一个简单的安卓版本的即时消息(带源代码)。

 ▲以上截图内容来自云即时通讯官方网站

 

 Mina实际上与Netty非常相似,大多数API都是相同的,因为它们是由同一作者开发的。然而,我觉得米娜没有妮蒂成熟。在使用Netty的过程中,如果出现问题,很容易找到解决方案,因此Netty是一个不错的选择。

 

 注意:关于MINA和Netty框架之间的关系和比较,请参见下面的文章了解详细信息:

 

 

 关于“为什么选择Netty”的11个问题和答案

 关于开源NIO框架的流言蜚语——首先是MINA还是Netty?》

 选择内蒂还是米娜:深入研究与比较(一)

 选择内蒂还是米娜:深入研究和比较(2)

 

 好吧,我们废话少说,开始吧。

 

 7.准备工作

 

 首先,我们创建一个新项目,然后在项目中创建一个安卓库。模块名称暂时为im_lib,如图所示:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-1.jpg。

 

 然后,在分析了我们的消息结构之后,每条消息应该有一个消息唯一的id、发送者id、接收者id、消息类型、发送时间等等。经过分析,一般的消息类型分类如下:

 

 

 MsgId:消息Id

 发件人Id:发件人id

 ToId:收件人Id

 MsgType:消息类型

 MsgContentType:消息内容类型

 Timestamp:消息时间戳

 状态报告:状态报告

 扩展:扩展字段

 

 根据以上所述,我编制了一张思维导图供你参考:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-2.jpg。

 

 上面的图片比原始图片大。如果你看不清楚,请下载一张清晰的大图:

 信息结构——思维导图[清晰的大图]。zip (314.71 KB,下载次数:175)

 

 这是最基本的部分。当然,您可以根据自己的需要定制适合自己的消息结构。

 

 我们根据用户定义的消息类型编写原型文件:

 

 01020304050607080910111213141516171819 syntax = " proto 3 ";//指定原型版本选项。原蟾蜍";//指定包名选项。//指定生成的类名消息消息{ Head Head = 1;//消息头字符串正文= 2;//消息正文}消息头{字符串MsGid = 1;//消息id int 32 MSgType = 2;//消息类型int 32 MsgContentType = 3;//消息内容类型字符串FromId = 4;//消息发送者id字符串ToID = 5;//消息接收方id int 64timestamp = 6;//消息时间戳int 32 StatusReport = 7;//状态报告字符串extend = 8;//扩展字段,json}以键/值形式存储}

 

 

 然后执行命令(我使用的mac和windows命令应该类似):

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 7-3.jpg。

 

 然后,我们将看到一个java类将在与原型文件相同的目录中生成,这就是我们需要使用的:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-4.jpg。

 

 我们打开扫描:

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_7-5.jpg

 

 有很多事情,所以不要担心它们。这是谷歌为我们生成的protobuf类。直接用吧。如何使用它?

 

 只需直接使用这个类文件,并将其复制到我们开始指定的项目包的路径中:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_7-6.jpg。

 

 添加依赖项后,您可以看到MessageProtobuf类文件没有报告错误。顺便介绍一下内蒂的罐子包和法斯特森的:

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_7-7.jpg

 

 建议使用最终版的jar包。如果您以后熟悉它,可以使用简化的jar包。

 

 至此,准备工作结束了。现在,让我们编写java代码来实现即时通讯的功能。

 

 8.代码封装

 

 为什么你需要封装?直截了当地说,这是为了解耦,并且在不改变调用位置的情况下,将来切换到不同的框架实现是很方便的。

 

 以栗子为例,早期流行的安卓图片加载框架是通用图像加载器。后来,由于某些原因,原作者停止了项目的维护。目前,流行的图片加载框架是毕加索或格莱德,因为有很多地方可以调用图片加载功能。如果在早期使用通用图像加载器时没有进行一些封装,那么现在就必须切换到Glide,而且变化会非常非常大,可能会有遗漏,风险非常高。

 

 那么,解决方案是什么?

 

 非常简单,我们可以使用工厂设计模式进行一些包装。有三种工厂模式:工厂方法模式、抽象工厂模式和工厂方法模式。在这里,我使用工厂方法模式进行包装。具体差异请参考“我对三种设计模式的理解:简单工厂、工厂方法和抽象工厂”。

 

 让我们分析一下,ims(即时消息服务,以下简称ims)应该具有初始化、建立连接、重新连接、关闭连接、释放资源、判断长连接是否关闭、发送消息等功能。

 

 基于以上分析,我们可以抽象出一个接口:

 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_8-1.jpg。

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_8-2.jpg。

 

 OnEventListener是一个与应用层交互的侦听器:

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 8-3.jpg。

 

 IMConnectStatusCallback是im的连接状态回调侦听器:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_8-4.jpg。

 

 然后编写一个Netty tcp实现类:

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_8-5.jpg

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_8-6.jpg。

 

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

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_8-7.jpg

 

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

 

 9.初始化

 

 我们首先实现init (vector server urllist,onevent侦听器,imsconnectstatuscallback回调)方法,初始化一些参数,并建立第一个连接。

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_9-1.jpg

 

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

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_9-2.jpg。

 

 ExecutorServiceFactory是一个线程池工厂,负责调度重新连接和心跳线程:

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 9-3.jpg。

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_ 9-4.jpg。

 自己开发即时通讯有这么难吗?教你自己创建一个Andriod版本的sIMple im(带源代码)_9-5.jpg

 

 10.连接和重新连接

 

 resetConnect()方法用作连接的起点,第一个连接和重新连接逻辑都在resetConnect()方法中进行逻辑处理。

 

 让我们看一眼:

 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_10-1.jpg。

 

 可以看出,当第一次连接时,即当连接失败一个周期后重新连接时,线程将休眠一段时间,因为此时网络条件可能不是很好。然后,判断ims是否关闭或者是否正在进行重新连接操作。由于重新连接操作是由子线程执行的,因此需要一些并发处理来避免重复的重新连接。

 

 重新连接任务开始后,分四个步骤执行:

 

 

 1)更改重新连接状态标识;

 2)向应用层回调连接状态;

 3)关闭先前打开的连接通道;;

 4)使用线程池执行新的重新连接任务。

 

 ResetConnectRunnable是一个重新连接任务,核心重新连接逻辑放在这里执行:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-2.jpg。

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-3.jpg。

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-4.jpg。

 

 ToServer()是服务器实际连接的位置:

 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_10-5.jpg。

 

 InitBootstrap()用于初始化Netty引导:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-6.jpg。

 

 注意:将NioEventLoopGroup的线程数设置为4可以满足QPS超过一百万的情况。如果应用程序需要承受数千万的流量,它需要额外调整线程的数量。(请参考:“netty的实际百万流量NioEventLoopGroup线程数配置”)

 

 接下来,让我们看一下TCPChannelInitializerHanlder:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_10-7.jpg。

 

 其中,protobufEncoder和ProtobufDecoder增加了对Protobuf的支持,LoginAuthRespHandler是接收服务器握手认证消息响应的处理程序,HeartbeatRespHandler是接收服务器心跳消息响应的处理程序,TCPReadHandler是从服务器接收其他消息后的处理程序。让我们别管它。我们将重点分析纵向字段预处理器和纵向字段基帧解码器,这需要扩展到解包和绑定TCP。

 

 11.解包和粘贴TCP

 

 什么是TCP解包?为什么要进行TCP解包?

 简而言之,我们都知道,TCP以“流”的形式传输数据,为了提高TCP的性能,发送方会将待发送的数据刷入缓冲区,等待缓冲区满,然后将缓冲区中的数据发送给接收方。类似地,接收器也将具有接收数据的缓冲机制。解包意味着当套接字读取时,它不会读取一个完整的数据包,而只会读取其中的一部分。

 

 什么是TCP粘性数据包?为什么会出现TCP粘性数据包?

 同上,粘贴包是指在读取套接字时,读取实际意义上的两个或多个数据包的内容,并同时将它们作为一个数据包进行处理。

 

 引用一张图片来解释三种情况:拆包、粘贴和正常状态:

 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_11-1.jpg。

 

 了解TCP拆包/卡的原因,如何解决?

 

 一般来说,有四种解决方案:

 

 

 1)消息固定长度;

 2)使用回车换行作为消息结束标志;

 3)特殊分隔符用作消息结束标志,如\ t \ n等。,而回车换行实际上是一种特殊的分隔符;

 4)消息分为消息头和消息体,消息的总长度由消息头中的字段标识。

 

 Netty为上述四种场景封装了以下四个相应的解码器:

 

 

 1)固定长度的帧解码器,固定长度的消息解码器;

 2)LineBasedFrameDecoder,回车换行符消息解码器;

 3)DELimITERBASEDFRAMEDCODER,特殊分隔符消息解码器;

 4)长度字段基本帧解码器,一个自定义长度的消息解码器。

 

 我们使用的是LengthFieldBasedframeCoder自定义长度消息解码器,它与LengthFieldRepeater编码器一起使用。对于参数配置,建议参考文章“netty -最常见的TCP粘性包解决方案:纵向字段基础帧编码器和纵向字段中继器”,并详细解释。

 

 我们的配置是消息头的长度是2字节,所以消息包的最大长度需要小于65536字节。netty将消息内容的长度存储在消息头字段中,接收者可以根据消息头字段获得该消息的总长度。当然,netty提供的LengthFieldBasedFrameDecoder已经打包了处理逻辑。我们只需要配置长度字段偏移量、长度字段长度、长度调整、初始化标签条,就可以解决TCP的解包和粘贴问题。与本机nio相比,这是netty的便利之处,本机nio需要自己处理解包/粘贴问题。

 

 12.长连接握手认证

 

 然后,让我们来看看LoginAuthHandler和HeartbeatRespHandler。

 

 当客户机和服务器之间的连接成功建立时,客户机主动向服务器发送一个登录验证消息,引入与当前用户相关的参数,如令牌。服务器收到此消息后,会向数据库查询用户信息。如果是合法有效的用户,它会向客户端返回登录成功消息;否则,它会向客户端返回登录失败消息。这里,它是接收到服务器返回的登录状态后的处理程序。

 

 例如:

 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_12-1.jpg。

 

 可以看出,在收到服务器握手消息响应后,状态将从扩展字段中取出。如果状态=1,则表示握手成功。此时,将首先向服务器发送心跳消息,然后使用Netty的IdleStateHandler读写超时机制定期向服务器发送心跳消息,以维持长连接并检测长连接是否仍然存在。

 

 当客户端收到服务器成功登录的消息时,它会主动向服务器发送心跳消息。心跳消息可以是一个空包,包越小越好。服务器从客户端接收到心跳数据包后,会将其原样返回给客户端。这里,它是接收服务器返回的心跳消息响应的处理程序。

 

 例如:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_12-2.jpg。

 

 这相对简单。当您收到心跳消息响应时,您不需要处理任务,因此可以直接打印出来供我们分析。

 

 13、心跳机制和读写超时机制

 

 心跳包定期发送,也可以自己定义一个周期,比如“移动即时通讯练习:在安卓版实现微信智能心跳机制”。为简单起见,这里规定当它应用于前台时,心跳包将在8秒内发送,当它被切换到后台时,它将在30秒内发送一次,这可以根据您的实际情况进行修改。心跳数据包用于维护长连接,并检测长连接是否断开。

 

 附注:关于心跳保持活力的更多文章,请参阅:

 

 

 "安卓端消息推送概述:实现原理、心跳保持、遇到的问题等."

 “为什么基于TCP的移动即时消息仍然需要心跳保持机制?》

 “微信团队原创分享:安卓版微信背景保活战斗分享(网络保活文章)”

 “移动即时通讯实践:WhatsApp、Line和微信心跳策略分析”

 

 然后,我们使用Netty的读写超时机制来实现心跳消息管理处理程序:

 自己开发即时通讯有这么难吗?为Andriod教你一个简单的im(带源代码)_13-1.jpg。

 

 可以看出,读超时/写超时/读和写超时可以通过回调userEventTriggered()方法来判断,下面的代码将被粘贴。

 

 首先,我们可以在READER_IDLE事件中检测到在指定时间内是否没有收到服务器心跳数据包响应,如果是,它将触发重新连接操作。在WRITER_IDEL事件中,可以检测客户端是否在指定时间内没有向服务器发送心跳数据包,如果是,它将主动发送心跳数据包。发送心跳数据包是在子线程中执行的,因此我们可以使用以前编写的工作线程池来进行线程管理。

 

 AddHeartbeatHandler()代码如下:

 自己开发即时通讯有这么难吗?为Andriod教你一个sIMple im(带源代码)_13-2.jpg。

 

 从图中可以看出,在IdleStateHandler中配置的读取超时是心跳间隔的3倍,也就是说,当3次心跳没有响应时,长连接被视为断开,并触发重新连接操作。写超时是心跳间隔的长度,这意味着每个心跳间隔都会发送一个心跳数据包。没有使用读写超时,因此它被配置为0。

 

 onconnectstatuscallback(int connectstatus)是一个连接状态回调,以及一些常见的逻辑处理:



自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-3.jpg 


连接成功后,立即发送一条握手消息,再次梳理一下整体流程:



1)客户端根据服务端返回的host及port,进行第一次连接;

2)连接成功后,客户端向服务端发送一条握手认证消息(1001);

3)服务端在收到客户端的握手认证消息后,从扩展字段里取出用户token,到本地数据库校验合法性;

4)校验完成后,服务端把校验结果通过1001消息返回给客户端,也就是握手消息响应;

5)客户端收到服务端的握手消息响应后,从扩展字段取出校验结果。若校验成功,客户端向服务端发送一条心跳消息(1002),然后进入心跳发送周期,定期间隔向服务端发送心跳消息,维持长连接以及实时检测链路可用性,若发现链路不可用,等待一段时间触发重连操作,重连成功后,重新开始握手/心跳的逻辑。


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

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-4.jpg 

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-5.jpg 


可以看到,在channelInactive()及exceptionCaught()方法都触发了重连,channelInactive()方法在当链路断开时会调用,exceptionCaught()方法在当出现异常时会触发,另外,还有诸如channelUnregistered()、channelReadComplete()等方法可以重写,在这里就不贴了,相信聪明的你一眼就能看出方法的作用。


我们仔细看一下channelRead()方法的逻辑,在if判断里,先判断消息类型,如果是服务端返回的消息发送状态报告类型,则判断消息是否发送成功,如果发送成功,从超时管理器中移除,这个超时管理器是干嘛的呢?


下面讲到消息重发机制的时候会详细地讲。在else里,收到其他消息后,会立马给服务端返回一个消息接收状态报告,告诉服务端,这条消息我已经收到了,这个动作,对于后续需要做的离线消息会有作用。如果不需要支持离线消息功能,这一步可以省略。最后,调用消息转发器,把接收到的消息转发到应用层即可。


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


发送消息:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-6.jpg 


关闭ims:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-7.jpg 


ims默认配置:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-8.jpg 


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

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-9.jpg 


由于代码有点多,不太方便全部贴上,如果有兴趣可以下载本文的完整demo进行体验。


额,对了,还有一个简易的服务端代码,如下:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-10.jpg 

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-11.jpg

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_13-12.jpg 


14、运行调试


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


启动服务端:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_14-1.gif


启动客户端:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_14-2.gif


可以看到,正常的情况下已经连接成功了,接下来,我们来试一下异常情况。


比如服务端没启动,看看客户端的重连情况:



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


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



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


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


15、消息重发机制


消息重发,顾名思义,即使对发送失败的消息进行重发。考虑到网络环境的不稳定性、多变性(比如从进入电梯、进入地铁、移动网络切换到wifi等),在消息发送的时候,发送失败的概率其实不小,这时消息重发机制就很有必要了。


有关即时通讯(IM)应用中的消息送达保证机制,可以详细阅读以下文章:



《IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》

《IM群聊消息如此复杂,如何保证不丢不重?》

《完全自已开发的IM该如何设计“失败重试”机制?》


我们先来看看实现的代码逻辑。


MsgTimeoutTimer:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-1.jpg 

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-2.jpg 


MsgTimeoutTimerManager:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-3.jpg

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-4.jpg


然后,我们看看收消息的TCPReadHandler的改造:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-5.jpg 


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

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-6.jpg 


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


另外,在用户握手认证成功时,应该检查消息发送超时管理器里是否有发送超时的消息,如果有,则全部重发:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_15-7.jpg


16、离线消息


由于离线消息机制,需要服务端数据库及缓存上的配合,代码就不贴了,太多太多。


我简单说一下实现思路吧:客户端A发送消息到客户端B,消息会先到服务端,由服务端进行中转。


这个时候,客户端B存在两种情况:



1)长连接正常,就是客户端网络环境良好,手机有电,应用处在打开的情况;

2)废话,那肯定就是长连接不正常咯。这种情况有很多种原因,比如wifi不可用、用户进入了地铁或电梯等网络不好的场所、应用没打开或已退出登录等,总的来说,就是没有办法正常接收消息。


如果是长连接正常,那没什么可说的,服务端直接转发即可。


如果长连接不正常,需要这样处理:


服务端接收到客户端A发送给客户端B的消息后,先给客户端A回复一条状态报告,告诉客户端A,我已经收到消息,这个时候,客户端A就不用管了,消息只要到达服务端即可。然后,服务端先尝试把消息转发到客户端B,如果这个时候客户端B收到服务端转发过来的消息,需要立马给服务端回一条状态报告,告诉服务端,我已经收到消息,服务端在收到客户端B返回的消息接收状态报告后,即认为此消息已经正常发送,不需要再存库。


如果客户端B不在线,服务端在做转发的时候,并没有收到客户端B返回的消息接收状态报告,那么,这条消息就应该存到数据库,直到客户端B上线后,也就是长连接建立成功后,客户端B主动向服务端发送一条离线消息询问,服务端在收到离线消息询问后,到数据库或缓存去查客户端B的所有离线消息,并分批次返回,客户端B在收到服务端的离线消息返回后,取出消息id(若有多条就取id集合),通过离线消息应答把消息id返回到服务端,服务端收到后,根据消息id从数据库把对应的消息删除即可。



以上是单聊离线消息处理的情况,群聊有点不同,群聊的话,是需要服务端确认群组内所有用户都收到此消息后,才能从数据库删除消息,就说这么多,如果需要细节的话,可以私信我。


更多有关离线消息处理思路的文章,可以详细阅读:



《IM消息送达保证机制实现(二):保证离线消息的可靠投递》

《IM群聊消息如此复杂,如何保证不丢不重?》

《浅谈移动端IM的多点登陆和消息漫游原理》


不知不觉,NettyTcpClient中定义了很多变量,为了防止大家不明白变量的定义,还是贴上代码吧:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_16-1.jpg 


17、应用层封装


这个就见仁见智啦,每个人代码风格不同,我把自己简单封装的代码贴上来吧。


MessageProcessor消息处理器:


001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103package com.freddy.chat.im; import android.util.Log; import com.freddy.chat.bean.AppMessage;import com.freddy.chat.bean.BaseMessage;import com.freddy.chat.bean.ContentMessage;import com.freddy.chat.im.handler.IMessageHandler;import com.freddy.chat.im.handler.MessageHandlerFactory;import com.freddy.chat.utils.CThreadPoolExecutor; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:MessageProcessor.java</p> * <p>@ PackageName:com . freddy . chat .im</p> *  * <p>@描述:消息处理器</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/10 03:27</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class MessageProcessor implements IMessageProcessor { private static final String TAG = MessageProcessor.class.getSimpleName(); private MessageProcessor() { } private static class MessageProcessorInstance { private static final IMessageProcessor INSTANCE = new MessageProcessor(); } public static IMessageProcessor getInstance() { return MessageProcessorInstance.INSTANCE; } /** * 接收消息 * @param message */ @Override public void receiveMsg(final AppMessage message) { CThreadPoolExecutor.runInBackground(new Runnable() { @Override public void run() { try { IMessageHandler messageHandler = MessageHandlerFactory.getHandlerByMsgType(message.getHead().getMsgType()); if (messageHandler != null) { messageHandler.execute(message); } else { Log.e(TAG, "未找到消息处理handler,msgType=" + message.getHead().getMsgType()); } } catch (Exception e) { Log.e(TAG, "消息处理出错,reason=" + e.getMessage()); } } }); } /** * 发送消息 * * @param message */ @Override public void sendMsg(final AppMessage message) { CThreadPoolExecutor.runInBackground(new Runnable() { @Override public void run() { boolean isActive = IMSClientBootstrap.getInstance().isActive(); if (isActive) { IMSClientBootstrap.getInstance().sendMessage(MessageBuilder.getProtoBufMessageBuilderByAppMessage(message).build()); } else { Log.e(TAG, "发送消息失败"); } } }); } /** * 发送消息 * * @param message */ @Override public void sendMsg(ContentMessage message) { this.sendMsg(MessageBuilder.buildAppMessage(message)); } /** * 发送消息 * * @param message */ @Override public void sendMsg(BaseMessage message) { this.sendMsg(MessageBuilder.buildAppMessage(message)); }}



IMSEventListener与ims交互的listener:


001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163/** * <p>@ProjectName: NettyChat</p> * <p>@ class name:IMSEventListener.java</p> * <p>@ PackageName:com . freddy . chat .im</p> *  * <p>@描述:侦听器与ims交互</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/07 23:55</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class IMSEventListener implements OnEventListener { private String userId; private String token; public IMSEventListener(String userId, String token) { this.userId = userId; this.token = token; } /** * 接收ims转发过来的消息 * * @param msg */ @Override public void dispatchMsg(MessageProtobuf.Msg msg) { MessageProcessor.getInstance().receiveMsg(MessageBuilder.getMessageByProtobuf(msg)); } /** * 网络是否可用 * * @return */ @Override public boolean isNetworkAvailable() { ConnectivityManager cm = (ConnectivityManager) NettyChatApp.sharedInstance().getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = cm.getActiveNetworkInfo(); return info != null && info.isConnected(); } /** * 设置ims重连间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getReconnectInterval() { return 0; } /** * 设置ims连接超时时长,0表示默认使用ims的值 * * @return */ @Override public int getConnectTimeout() { return 0; } /** * 设置应用在前台时ims心跳间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getForegroundHeartbeatInterval() { return 0; } /** * 设置应用在后台时ims心跳间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getBackgroundHeartbeatInterval() { return 0; } /** * 构建握手消息 * * @return */ @Override public MessageProtobuf.Msg getHandshakeMsg() { MessageProtobuf.Msg.Builder builder = MessageProtobuf.Msg.newBuilder(); MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder(); headBuilder.setMsgId(UUID.randomUUID().toString()); headBuilder.setMsgType(MessageType.HANDSHAKE.getMsgType()); headBuilder.setFromId(userId); headBuilder.setTimestamp(System.currentTimeMillis()); JSONObject jsonObj = new JSONObject(); jsonObj.put("token", token); headBuilder.setExtend(jsonObj.toString()); builder.setHead(headBuilder.build()); return builder.build(); } /** * 构建心跳消息 * * @return */ @Override public MessageProtobuf.Msg getHeartbeatMsg() { MessageProtobuf.Msg.Builder builder = MessageProtobuf.Msg.newBuilder(); MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder(); headBuilder.setMsgId(UUID.randomUUID().toString()); headBuilder.setMsgType(MessageType.HEARTBEAT.getMsgType()); headBuilder.setFromId(userId); headBuilder.setTimestamp(System.currentTimeMillis()); builder.setHead(headBuilder.build()); return builder.build(); } /** * 服务端返回的消息发送状态报告消息类型 * * @return */ @Override public int getServerSentReportMsgType() { return MessageType.SERVER_MSG_SENT_STATUS_REPORT.getMsgType(); } /** * 客户端提交的消息接收状态报告消息类型 * * @return */ @Override public int getClientReceivedReportMsgType() { return MessageType.CLIENT_MSG_RECEIVED_STATUS_REPORT.getMsgType(); } /** * 设置ims消息发送超时重发次数,0表示默认使用ims的值 * * @return */ @Override public int getResendCount() { return 0; } /** * 设置ims消息发送超时重发间隔时长,0表示默认使用ims的值 * * @return */ @Override public int getResendInterval() { return 0; }}



MessageBuilder消息转换器:


001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146package com.freddy.chat.im; import com.freddy.chat.bean.AppMessage;import com.freddy.chat.bean.BaseMessage;import com.freddy.chat.bean.ContentMessage;import com.freddy.chat.bean.Head;import com.freddy.chat.utils.StringUtil;import com.freddy.im.protobuf.MessageProtobuf; /** * <p>@ProjectName: BoChat</p> * <p>@ class name:MessageBuilder.java</p> * <p>@ PACkageName:com . bochat . app . message</p> *  * <p>@描述:消息转换</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/02/07 17:26</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class MessageBuilder { /** * 根据聊天消息,生成一条可以能够传输通讯的消息 * * @param msgId * @param type * @param subType * @param fromId * @param toId * @param extend * @param content * @return */ public static AppMessage buildAppMessage(String msgId, int type, int subType, String fromId, String toId, String extend, String content) { AppMessage message = new AppMessage(); Head head = new Head(); head.setMsgId(msgId); head.setMsgType(type); head.setMsgContentType(subType); head.setFromId(fromId); head.setToId(toId); head.setExtend(extend); message.setHead(head); message.setBody(content); return message; } /** * 根据聊天消息,生成一条可以能够传输通讯的消息 * * @param msg * @return */ public static AppMessage buildAppMessage(ContentMessage msg) { AppMessage message = new AppMessage(); Head head = new Head(); head.setMsgId(msg.getMsgId()); head.setMsgType(msg.getMsgType()); head.setMsgContentType(msg.getMsgContentType()); head.setFromId(msg.getFromId()); head.setToId(msg.getToId()); head.setTimestamp(msg.getTimestamp()); head.setExtend(msg.getExtend()); message.setHead(head); message.setBody(msg.getContent()); return message; } /** * 根据聊天消息,生成一条可以能够传输通讯的消息 * * @param msg * @return */ public static AppMessage buildAppMessage(BaseMessage msg) { AppMessage message = new AppMessage(); Head head = new Head(); head.setMsgId(msg.getMsgId()); head.setMsgType(msg.getMsgType()); head.setMsgContentType(msg.getMsgContentType()); head.setFromId(msg.getFromId()); head.setToId(msg.getToId()); head.setExtend(msg.getExtend()); head.setTimestamp(msg.getTimestamp()); message.setHead(head); message.setBody(msg.getContent()); return message; } /** * 根据业务消息对象获取protoBuf消息对应的builder * * @param message * @return */ public static MessageProtobuf.Msg.Builder getProtoBufMessageBuilderByAppMessage(AppMessage message) { MessageProtobuf.Msg.Builder builder = MessageProtobuf.Msg.newBuilder(); MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder(); headBuilder.setMsgType(message.getHead().getMsgType()); headBuilder.setStatusReport(message.getHead().getStatusReport()); headBuilder.setMsgContentType(message.getHead().getMsgContentType()); if (!StringUtil.isEmpty(message.getHead().getMsgId())) headBuilder.setMsgId(message.getHead().getMsgId()); if (!StringUtil.isEmpty(message.getHead().getFromId())) headBuilder.setFromId(message.getHead().getFromId()); if (!StringUtil.isEmpty(message.getHead().getToId())) headBuilder.setToId(message.getHead().getToId()); if (message.getHead().getTimestamp() != 0) headBuilder.setTimestamp(message.getHead().getTimestamp()); if (!StringUtil.isEmpty(message.getHead().getExtend())) headBuilder.setExtend(message.getHead().getExtend()); if (!StringUtil.isEmpty(message.getBody())) builder.setBody(message.getBody()); builder.setHead(headBuilder); return builder; } /** * 通过protobuf消息对象获取业务消息对象 * * @param protobufMessage * @return */ public static AppMessage getMessageByProtobuf( MessageProtobuf.Msg protobufMessage) { AppMessage message = new AppMessage(); Head head = new Head(); MessageProtobuf.Head protoHead = protobufMessage.getHead(); head.setMsgType(protoHead.getMsgType()); head.setStatusReport(protoHead.getStatusReport()); head.setMsgContentType(protoHead.getMsgContentType()); head.setMsgId(protoHead.getMsgId()); head.setFromId(protoHead.getFromId()); head.setToId(protoHead.getToId()); head.setTimestamp(protoHead.getTimestamp()); head.setExtend(protoHead.getExtend()); message.setHead(head); message.setBody(protobufMessage.getBody()); return message; }}



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


010203040506070809101112131415161718192021222324package com.freddy.chat.im.handler; import com.freddy.chat.bean.AppMessage; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:AbstractMessageHandler.java</p> * <p>@ PackageName:com . freddy . chat .im。处理者</p> *  * <p>@描述:抽象消息处理程序</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/10 03:41</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public abstract class AbstractMessageHandler implements IMessageHandler { @Override public void execute(AppMessage message) { action(message); } protected abstract void action(AppMessage message);}



SingleChatMessageHandler单聊消息处理handler:


010203040506070809101112131415161718192021222324252627282930313233343536373839404142package com.freddy.chat.im.handler; import android.util.Log; import com.freddy.chat.bean.AppMessage;import com.freddy.chat.bean.SingleMessage;import com.freddy.chat.event.CEventCenter;import com.freddy.chat.event.Events; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:SingleChatMessageHandler.java</p> * <p>@ PackageName:com . freddy . chat .im。处理者</p> *  * <p>@描述:类描述</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/10 03:43</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class SingleChatMessageHandler extends AbstractMessageHandler { private static final String TAG = SingleChatMessageHandler.class.getSimpleName(); @Override protected void action(AppMessage message) { Log.d(TAG, "收到单聊消息,message=" + message); SingleMessage msg = new SingleMessage(); msg.setMsgId(message.getHead().getMsgId()); msg.setMsgType(message.getHead().getMsgType()); msg.setMsgContentType(message.getHead().getMsgContentType()); msg.setFromId(message.getHead().getFromId()); msg.setToId(message.getHead().getToId()); msg.setTimestamp(message.getHead().getTimestamp()); msg.setExtend(message.getHead().getExtend()); msg.setContent(message.getBody()); CEventCenter.dispatchEvent(Events.CHAT_SINGLE_MESSAGE, 0, 0, msg); }}



GroupChatMessageHandler群聊消息处理handler:


0102030405060708091011121314151617181920212223242526package com.freddy.chat.im.handler; import android.util.Log; import com.freddy.chat.bean.AppMessage; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:GroupChatMessageHandler.java</p> * <p>@ PackageName:com . freddy . chat .im。处理者</p> *  * <p>@描述:类描述</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/10 03:43</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class GroupChatMessageHandler extends AbstractMessageHandler { private static final String TAG = GroupChatMessageHandler.class.getSimpleName(); @Override protected void action(AppMessage message) { Log.d(TAG, "收到群聊消息,message=" + message); }}



MessageHandlerFactory消息handler工厂:


0102030405060708091011121314151617181920212223242526272829303132333435363738394041424344package com.freddy.chat.im.handler; import android.util.SparseArray; import com.freddy.chat.im.MessageType; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:MessageHandlerFactory.java</p> * <p>@ PackageName:com . freddy . chat .im。处理者</p> *  * <p>@描述:消息处理处理程序工厂</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/10 03:44</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class MessageHandlerFactory { private MessageHandlerFactory() { } private static final SparseArray HANDLERS = new SparseArray(); static { /** 单聊消息处理handler */ HANDLERS.put(MessageType.SINGLE_CHAT.getMsgType(), new SingleChatMessageHandler()); /** 群聊消息处理handler */ HANDLERS.put(MessageType.GROUP_CHAT.getMsgType(), new GroupChatMessageHandler()); /** 服务端返回的消息发送状态报告处理handler */ HANDLERS.put(MessageType.SERVER_MSG_SENT_STATUS_REPORT.getMsgType(), new ServerReportMessageHandler()); } /** * 根据消息类型获取对应的处理handler * * @param msgType * @return */ public static IMessageHandler getHandlerByMsgType(int msgType) { return HANDLERS.get(msgType); }}



MessageType消息类型枚举:


0102030405060708091011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283package com.freddy.chat.im; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:MessageType.java</p> * <p>@ PackageName:com . freddy . chat .im</p> *  * <p>@描述:消息类型</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/08 00:04</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public enum MessageType { /* * 握手消息 */ HANDSHAKE(1001), /* * 心跳消息 */ HEARTBEAT(1002), /* * 客户端提交的消息接收状态报告 */ CLIENT_MSG_RECEIVED_STATUS_REPORT(1009), /* * 服务端返回的消息发送状态报告 */ SERVER_MSG_SENT_STATUS_REPORT(1010), /** * 单聊消息 */ SINGLE_CHAT(2001), /** * 群聊消息 */ GROUP_CHAT(3001); private int msgType; MessageType(int msgType) { this.msgType = msgType; } public int getMsgType() { return this.msgType; } public enum MessageContentType { /** * 文本消息 */ TEXT(101), /** * 图片消息 */ IMAGE(102), /** * 语音消息 */ VOICE(103); private int msgContentType; MessageContentType(int msgContentType) { this.msgContentType = msgContentType; } public int getMsgContentType() { return this.msgContentType; } }}



IMSConnectStatusListenerIMS连接状态监听器:


0102030405060708091011121314151617181920212223242526272829package com.freddy.chat.im; import com.freddy.im.listener.IMSConnectStatusCallback; /** * <p>@ProjectName: NettyChat</p> * <p>@ class name:IMSConnectStatusListener.java</p> * <p>@ PackageName:com . freddy . chat .im</p> *  * <p>@描述:类描述</p> *  * <p>@author: FreddyChen</p> * <p>@date: 2019/04/08 00:31</p> * <p>@ email:[URL = mailto:chenshichao @ outlook . com]chenshichao @ outlook . com[/URL]</p> */public class IMSConnectStatusListener implements IMSConnectStatusCallback { @Override public void onConnecting() { } @Override public void onConnected() { } @Override public void onConnectFailed() { }}



由于每个人代码风格不同,封装代码都有自己的思路,所以,在此就不过多讲解,只是把自己简单封装的代码全部贴上来,作一个参考即可。


只需要知道,接收到消息时,会回调OnEventListener的dispatchMsg(MessageProtobuf.Msg msg)方法:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_17-1.jpg 


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

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_17-2.jpg 


即可,至于怎样去封装得更好,大家自由发挥吧。由于代码较多,这里就不一一贴出来了,请自行从github下载完整工程源码:https://github.com/52im/NettyChat


18、最终运行


运行一下,看看效果吧:

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_16a42c85e653b88c11aaa.gif 


运行步骤是:



1)首先,启动服务端。

2)然后,修改客户端连接的ip地址为192.168.0.105(这是我本机的ip地址),端口号为8855,fromId,也就是userId,定义成100001,toId为100002,启动客户端A。

3)再然后,fromId,也就是userId,定义成100002,toId为100001,启动客户端B。

4)客户端A给客户端B发送消息,可以看到在客户端B的下面,已经接收到了消息。

5)用客户端B给客户端A发送消息,也可以看到在客户端A的下面,也已经接收到了消息。


至于,消息收发测试成功。至于群聊或重连等功能,就不一一演示了,还是那句话,下载demo体验一下吧:https://github.com/52im/NettyChat。


由于gif录制体积较大,所以只能简单演示一下消息收发,具体下载demo体验吧。如果有需要应用层UI实现(就是聊天页及会话页的封装)的话,我再分享出来吧。


19、写在最后


终于写完了,这篇文章大概写了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.jar

netty-codec-4.1.33.Final.jar

netty-common-4.1.33.Final.jar

netty-handler-4.1.33.Final.jar

netty-resolver-4.1.33.Final.jar

netty-transport-4.1.33.Final.jar


所以,抽取以上jar包,重新打成了netty-tcp-4.1.33-1.0.jar(已经上传到github工程了),目前自测没有问题,如果发现bug,请告诉我,谢谢。


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

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_19-1.png

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)_19-2.png













-------------------------------------------------------

相关文章

即时通讯

即时通讯im

什么是私有云?

im 移动社交聊天APP

Im通讯有哪些APP

如何选择即时通讯软件?

推荐一款好用的企业办公聊天APP

使用企业IM要不要收费

IM软件要多少钱?‍‍有必要定制么?

有没有必要开发企业聊天APP

企业IM是什么,有什么使用优势?

哇谷IM有什么特点,为什么很多企业在使用?

即时通讯私有化部署是什么?

企业即时通讯的使用价值有哪些

企业聊天APP有什么作用,可以带来哪些便利?

企业即时通讯软件支持多设备登陆吗?

购买海外云服务器注意避免这些误区

怎么样正确认识海外云服务器

员工上网影响效率 关闭外网做内网必备聊天APP

即时通讯软件是否具有聊天回执的功能?

即时通讯聊天APP是否支持消息漫游

即时通讯软件可以发送表情和语音么?

即时通讯软件支持群聊和音视频功能么?

有微信有QQ,为啥还要定制即时通讯软件?

IM软件功能丰富 随心所欲定制您需要的功能

盘点全球几大即时通讯软件

介绍两款国外大佬级别的即时通讯APP

原创互联网未来世界企业政府通讯APP办公节约成本类似马云思维-哇谷IM

免费十万SEO优化超级外链工具各大站长工具

敲黑板,企业聊天APP这些事项一定要注意!

JM沟通优势有哪些,安全性怎么样?

JM沟通app即时通讯聊天系统功能介绍

jm沟通 企业即时通讯有什么特点,为什么受欢迎?

JM沟通实现零距离交流 成现代企业办公的必备

即时通讯私有化完整版源代码分析后台数据库说明

即时通讯IM融云世界

新的市场叫板环信、融云、腾讯云!开源版IM即使聊天工具

公有云和私有云之间有什么区别?类似融云、环信云、网易云、哇谷云?

IM云系统即时通讯公有云、私有云、企业云、海外云-哇谷IM团队

2.0哇谷即时通讯系统正式上线适合大型项目运营超稳定性

哇谷科技JM沟通即时通讯内核升级新版哇谷2.0即将上线

im即时通讯社交软件APP红包技术分析(五):微信红包、聊呗红包、诚信红包、高并发技术

im即时通讯-微信红包、支付宝红包、聊呗红包、诚信红包、谈功能逻辑、容灾、运维、架构等。Q红包

更多文章

.

 企业即时通讯服务 | 商用红包功能构架 | 哇谷IM首页 | JM沟通IM下载 | IM功能与价格 | 即时通讯动态 | 热门动态 | 关于哇谷 |联系我们