本文系统介绍如何开发即时通讯IM源码系统的各个方面,包括系统架构、通信协议、单聊、群聊、表情发送、UI事件驱动,以及实用源码。Netty 是一个开源的IM源码框架。Netty 提供异步和事件驱动的网络应用程序框架和工具来快速开发高性能和可靠的网络服务器和客户端程序。
完整源码:im.jstxym.top
换句话说,netty 是一个基于 NiO 的客户端和服务端编程框架。使用netty可以保证你可以快速简单的开发一个网络应用程序,比如实现了某种协议的客户端和服务端应用程序。
Netty 简化和精简了网络应用程序的编程和开发过程,例如 TCP 和 UDP 的套接字服务开发。
一、系统设计:
在这套IM中,服务器采用DDD域驱动设计模式构建。通过将netty的功能赋予springboot进行启停控制,并在服务器上设置一个控制台,可以非常方便的操作通信系统,管理用户和通信。在客户端的构建中,UI是分离的,保证了业务代码和UI显示的分离,这样扩展控制非常容易。
此外,功能实现还包括:完美模仿微信桌面客户端、登录、搜索加好友、用户交流、群组交流、表情发送等核心功能。如果有实际需要用到的功能,可以根据这个系统框架进行扩展。
关注源码im(九):实现一个基于netty的分布式IM系统
解释:
1)UI开发:用JavaFX和maven搭建UI桌面项目,逐步讲解登录框、聊天框、对话框、好友栏等各种UI显示和操作事件;
2)架构设计:利用DDD领域驱动设计的四层模型结构结合netty构建合理的层次框架(设计相应的库和表函数);
3)功能实现:包括;登录、添加好友、对话通知、消息发送、断线重连等功能。
二、UI开发
2.1 功能划分
与登录表单相比,聊天表单的内容会越来越复杂。
下图是聊天表单的功能定义示意图:
关注源码im(九):实现一个基于netty的分布式IM系统
如上图所示:
1)首先是我们整个聊天主窗体的定义,是一个空白面板,去掉了默认的边框按钮(最小化、退出等);
2)然后是左边的侧边栏,我们称之为栏,以及功能区的实现;
3)最后,添加表单事件,点击按钮时在内容面板中更改填充信息。
2.2 聊天界面
显示对话框的选定内容区域,即用户之间的信息发送和显示。
总的来说,这是一个联动的过程。点击左侧对话框中的用户,右侧会填写相应的内容。那么右边填写的对话列表listview需要和每个对话用户关联。在点击聊天用户时,是经过反复切换和填充的过程。效果如下图所示。
关注源码im(九):实现一个基于netty的分布式IM系统
参考上图。让我解释:
1)点击左侧各个对话框,右侧聊天框的填充内容会相应变化(对应的对话框名称也会发生变化);
2)对话框左侧显示好友发送的信息,右侧显示个人发送的信息(同时消息内容会随着内容的增加而增加高度和宽度);
3) 底部是文本输入框。在后面的实现中,我们的文本输入框是采用公共方式设计的。当然,您也可以设计它以供个人使用。
2.3 好友列表
我们经常使用微信PC,在好友栏中可以分为几个部分,包括新朋友、公众号、群组和底部朋友。
关注源码im(九):实现一个基于netty的分布式IM系统
参考上图。让我解释:
1)顶部搜索框的内容与上一个相同。我们目前使用的方法是fxml设计。比如这部分是通用函数,可以提取到代码中,设计成组件元素类;
2)经过我们的分析,基于JavaFX组件的开发,这部分是一个嵌套的listview,即底部面板是一个listview,朋友和群都是一个listview。经过这样的处理,我们就很容易填写数据了;
3)另外,这种结构主要有利于我们程序的运行。如果您添加了好友,我们需要将好友信息刷新到好友栏。在填充数据的时候,为了更加方便和高效,我们设计了一个嵌套的listview(如果不是特别了解,可以从后面的代码中得到答案)。
2.4 事件定义
在桌面UI开发中,为了将UI与业务逻辑隔离开来,我们需要在封装UI后提供一个显示操作界面效果的界面和一个界面操作事件的抽象类。
那么根据下图可以理解:
关注源码im(九):实现一个基于netty的分布式IM系统
以上接口都是我们UI对外提供的行为接口。这些界面的链接描述为:打开窗口、搜索好友、添加好友、打开对话框、发送消息。
三、. 沟通设计
3.1 系统架构
前面提到过,更合适的架构是最能满足你当前需求的架构。
那么如何设计所需的架构呢?
本系统设计基于以下前提条件:
1) 系统在服务器端应有一个网页,用于管理通信用户,控制和监控服务器;
2)数据库的对象类不要被外界污染,要隔离(比如你的数据库类泄露到外面做显示类,现在需要加一个字段,不是属性你的数据库。那么这个时候数据库类已经被污染了)。
3)由于netty通信目前是用Java语言实现的,所以服务端和客户端都需要在通信过程中使用协议定义和解析。然后我们需要从这一层拉开,对外提供jar包(这样有利于复用,否则客户端和服务端复制相同的代码维护。);
4) 接口、业务处理、底层服务和通信交互要明确区分和实现,避免混淆和维护困难。
结合以上四个前提,你的思维体现了怎样的模型结构?以及是否有相应技术栈的选择方案?
接下来介绍两种架构设计模型,一种是MVC,大家很熟悉,另一种是DDD领域驱动设计,大家可能听说过。
3.2 通讯协议
从图中可以看出,在传输对象时,我们需要在传输包中添加一个“帧ID”来判断当前业务对象是哪个对象,这样可以让我们的业务更加清晰,避免使用大量的if语句。
协议框架:
协议
└── src
├── main
│ ├── java
│ │ └── org.itstack.naive.chat
│ │ ├── codec
│ │ │ ├── ObjDecoder.java
│ │ │ └── ObjEncoder.java
│ │ ├── protocol
│ │ │ ├── demo
│ │ │ ├── Command.java
│ │ │ └── Packet.java
│ │ └── util
│ │ └── SerializationUtil.java
│ ├── resources
│ │ └── application.yml
│ └── webapp
│ └── chat
│ └── res
│ └── index.html
└── test
└── java
└── org.itstack.demo.test
└── ApiTest.java
协议包:
公共抽象类数据包{
private final static Map<Byte, Class<? extendsPacket>> packetType = new ConcurrentHashMap<>();
static{
packetType.put(Command.LoginRequest, LoginRequest.class);
packetType.put(Command.LoginResponse, LoginResponse.class);
packetType.put(Command.MsgRequest, MsgRequest.class);
packetType.put(Command.MsgResponse, MsgResponse.class);
packetType.put(Command.TalkNoticeRequest, TalkNoticeRequest.class);
packetType.put(Command.TalkNoticeResponse, TalkNoticeResponse.class);
packetType.put(Command.SearchFriendRequest, SearchFriendRequest.class);
packetType.put(Command.SearchFriendResponse, SearchFriendResponse.class);
packetType.put(Command.AddFriendRequest, AddFriendRequest.class);
packetType.put(Command.AddFriendResponse, AddFriendResponse.class);
packetType.put(Command.DelTalkRequest, DelTalkRequest.class);
packetType.put(Command.MsgGroupRequest, MsgGroupRequest.class);
packetType.put(Command.MsgGroupResponse, MsgGroupResponse.class);
packetType.put(Command.ReconnectRequest, ReconnectRequest.class);
}
public static Class<? extends Packet> get(Byte command) {
return packetType.get(command);
}
/**
*Get protocol instruction
*
*@ return returns the instruction value
*/
public abstract Byte getCommand();
}
3.3 添加好友
从上面的流程图可以看出,有两个部分:搜索好友和添加好友。
添加好友后,好友会出现在我们的好友栏。
另外,我们单方面同意添加好友,即当您添加好友时,对方也有您的好友信息。
如果您需要添加好友并在您的业务中同意,您可以在发起添加好友时添加状态消息并请求添加好友。对方同意后,两个用户就可以成为好友,进行交流。
添加好友的示例代码:
公共类 AddFriendHandler 扩展 MyBizHandler<AddFriendRequest> {
public AddFriendHandler(UserService userService) {
super(userService);
}
@Override
public void channelRead(Channel channel, AddFriendRequest msg) {
// 1. Add friends to database [a - > b - > A]
List<UserFriend> userFriendList = newArrayList<>();
userFriendList.add(newUserFriend(msg.getUserId(), msg.getFriendId()));
userFriendList.add(newUserFriend(msg.getFriendId(), msg.getUserId()));
userService.addUserFriend(userFriendList);
// 2. Push friend add complete a
UserInfo userInfo = userService.queryUserInfo(msg.getFriendId());
channel.writeAndFlush(newAddFriendResponse(userInfo.getUserId(), userInfo.getUserNickName(), userInfo.getUserHead()));
// 3. Push friend add complete B
Channel friendChannel = SocketChannelUtil.getChannel(msg.getFriendId());
if(null== friendChannel) return;
UserInfo friendInfo = userService.queryUserInfo(msg.getUserId());
friendChannel.writeAndFlush(newAddFriendResponse(friendInfo.getUserId(), friendInfo.getUserNickName(), friendInfo.getUserHead()));
}
}
3.4 消息响应
从整体流程我们可以看出,当用户发起好友和群组通信时,会触发一个事件行为,然后客户端向服务器发送与好友的对话请求。
服务器收到会话请求后:如果是好友会话,则需要在对话框中保存与好友的通讯信息。同时,通知我的朋友,我想和你交流。您将我添加到您的对话列表中。
群交流的情况下:这样的通知是不必要的,因为不可能通知所有不在线的群用户(他们还没有登录),所以这部分只需要在用户收到后创建一个对话框到列表网上的资料。可以仔细理解,也可以考虑其他的实现方式。
消息响应示例代码:
公共类 MsgHandler 扩展 MyBizHandler<MsgRequest> {
public MsgHandler(UserService userService) {
super(userService);
}
@Override
public void channelRead(Channel channel, MsgRequest msg) {
logger. Info ("message information processing: {}", json.tojsonstring (MSG));
//Asynchronous write library
userService.asyncAppendChatRecord(newChatRecordInfo(msg.getUserId(), msg.getFriendId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));
//Add dialog [add if the other party doesn't have your dialog]
userService.addTalkBoxInfo(msg.getFriendId(), msg.getUserId(), Constants.TalkType.Friend.getCode());
//Get friend communication pipeline
Channel friendChannel = SocketChannelUtil.getChannel(msg.getFriendId());
if(null== friendChannel) {
logger. Info ("user ID: {} not logged in!", msg. getFriendId());
return;
}
//Send message
friendChannel.writeAndFlush(newMsgResponse(msg.getUserId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));
}
}
3.5 断开与重新连接
从上面的流程我们可以看出,当网络连接断开时,它会向服务器发送relink请求。所以发起链接的过程不同于系统的初始链接。断开和重新连接需要将用户的ID信息发送给服务器,以便服务器更新用户与通信通道的绑定关系。
同时,还需要更新组内的重连信息,将用户的重连加入到组映射中。此时,用户与好友、群组之间的通讯功能即可恢复。
消息响应示例代码:
//定期巡检通道状态;每 5 秒后 3 秒
预定的执行器服务。scheduleatfixedrate(() -> {while(!nettyclient.isactive()) {system.out.println(“通信管道巡查:通信管道状态”+nettyclient.isactive());
Try {system. Out. Println ("communication pipeline inspection: disconnection and reconnection [begin]);
Channel freshChannel = executorService.submit(nettyClient).get();
if(null== CacheUtil.userId) continue;
freshChannel.writeAndFlush(newReconnectRequest(CacheUtil.userId));
}Catch (interruptedexception | executionexception E) {system. Out. Println ("communication pipeline inspection: disconnection reconnection [error]);}
}
}, 3, 5, TimeUnit.SECONDS);
3.6 集群通信
关注源码im(九):实现一个基于netty的分布式IM系统
如上图,我是这样实现im集群通信的:
1)对于跨服务的情况,使用redis发布和订阅来传递消息。如果是大型服务,可以使用zookeeper;
2) 用户A在向用户B发送消息时,需要传递B的channelid让服务器判断该channelid是否属于自己的服务;
3)单机也可以启动多个netty服务,程序会自动寻找可用端口。
四、本文小结
这个 IM 系统涉及到很多技术栈:netty4 x. 采用springboot、mybatis、mysql、JavaFX、layui等技术栈,以及整个系统框架结构,采用DDD四层架构+socket模块的方式搭建。所有的 UI 都是以事件驱动的方式设计的,分离后端。在这个过程中,只要你能不断地学习,你就会收获很多。吹牛够了!
任何新技术栈的学习过程都会包括这样一条路线:运行HelloWorld,熟练使用API,项目实践,最后是深度源码挖掘。那么,Java程序员听到这样的需求,肯定会想到一系列技术知识点来填补我们项目中的每一个模块(比如JavaFX和swing为接口,socket为通信,或者知道netty框架,MVC模型为服务器控制和springboot等)。然而,如何合理地设置我们的系统是学习、实践和成长过程中最重要的一环。
嗯,Im开发其实涉及到很多知识维度,限于篇幅,这里就不罗嗦了。读者一定要同步学习源码,这样效果会更好(源码可以在附件“4.本文源码”部分下载)。
五、 参考文献
[1] 新手入门:迄今为止最透彻的分析netty的高性能原理和框架架构
[2] 初学者:Java高性能NiO框架netty的学习方法和进阶策略
[3] 最强Java NiO入门历史:那些担心开始放弃的人,请阅读这篇文章!
[4] Java bio和NiO难懂?我将向您展示代码练习。如果你不理解我,我就换专业!
[5] 史上最流行的netty框架介绍:基础介绍、环境搭建及动手实践
[6] 理论联系实际:详解一套典型的IM通信协议设计
[7]浅谈IM系统架构设计
[8]简述移动IM开发的陷阱:架构设计、通信协议和客户端
[9]一套面向海量在线用户的移动端IM架构设计实践分享(包括详细的图形)
[10]一套原创的分布式即时通讯(IM)系统理论框架方案
[11]一套高可用、可扩展、高并发的IM群聊和单聊架构的设计与实践
[12]A亿用户IM架构技术干货集(上):整体架构、服务拆分等
[13] 一亿用户的一套IM架构技术干货(下):可靠性、秩序、弱网优化等
[14] 从新手到专家:如何设计一个亿级消息的分布式IM系统
[15 ] 基于实践:百万级小规模IM系统技术要点总结