关于即时通讯:跟着源码学IM九基于Netty实现一套分布式IM系统

128次阅读

共计 9568 个字符,预计需要花费 24 分钟才能阅读完成。

本文作者小傅哥,原题“应用 DDD+Netty,开发一个分布式 IM(即时通信)零碎”。为了晋升浏览体验,有大量订正和改变,感激原作者。

0、系列文章

《跟着源码学 IM(一):手把手教你用 Netty 实现心跳机制、断线重连机制》
《跟着源码学 IM(二):自已开发 IM 很难?手把手教你撸一个 Andriod 版 IM》
《跟着源码学 IM(三):基于 Netty,从零开发一个 IM 服务端》
《跟着源码学 IM(四):拿起键盘就是干,教你徒手开发一套分布式 IM 零碎》
《跟着源码学 IM(五):正确理解 IM 长连贯、心跳及重连机制,并入手实现》
《跟着源码学 IM(六):手把手教你用 Go 疾速搭建高性能、可扩大的 IM 零碎》
《跟着源码学 IM(七):手把手教你用 WebSocket 打造 Web 端 IM 聊天》
《跟着源码学 IM(八):万字长文,手把手教你用 Netty 打造 IM 聊天》
《跟着源码学 IM(九):基于 Netty 实现一套分布式 IM 零碎》(* 本文)

1、本文引言

计算机编程的学习,能不能把常识学到手,考究的是入手实际。在我编写的文章中,根本都是以实际代码验证后果为外围来讲述文章内容。

从小我就喜爱入手,就以一个即时通信的我的项目为例,曾经基于不同技术计划实现了 5、6 次,仅仅为了实际技术,截图如下。

正如上图这样:

1)有些是刚学完 Socket 和 Swing 的时候,想入手试试这些技术能不能写个 QQ 进去;
2)也有的是因为实习培训须要实现的我的项目,不过在有了一些根底后,一周工夫就能写齐全部性能;
3)尽管这些我的项目在当初看上去还是丑丑的界面,以及代码逻辑可能也不是那么欠缺。但放在学习阶段的每一次实现中,都能为本人带来很多技术上的成长。

那么,这次借本文的机会,将 IM 实际的机会留给你,心愿你能用的上。

接下来的内容,我会为你介绍如何开发一个 IM 的方方面面,包含零碎架构、通信协议、单聊群聊、表情发送、UI 事件驱动等,以及全套的实际源码让你能够上手学习。

注:源码在本文“4、本文源码”一节的附件处可下载。

学习交换:

  • 即时通讯 / 推送技术开发交换 5 群:215477170 [举荐]
  • 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》
  • 开源 IM 框架源码:https://github.com/JackJiang2…

(本文已同步公布于:http://www.52im.net/thread-37…)

2、常识筹备

  • 重要提醒:本文不是一篇即时通讯实践文章,文章内容全副由实战代码组织而成,如果你对即时通讯(IM)技术实践理解的太少,倡议先具体浏览:《新手入门一篇就够:从零开发挪动端 IM》。

可能有人不晓得 Netty 是什么,这里简略介绍下:

Netty 是一个 Java 开源框架。Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以疾速开发高性能、高可靠性的网络服务器和客户端程序。

也就是说,Netty 是一个基于 NIO 的客户、服务器端编程框架,应用 Netty 能够确保你疾速和简略的开发出一个网络应用,例如实现了某种协定的客户,服务端利用。

Netty 相当简化和流线化了网络应用的编程开发过程,例如,TCP 和 UDP 的 Socket 服务开发。

以下是几篇无关 Netty 的入门文章,值得一读:

《新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析》
《写给初学者:Java 高性能 NIO 框架 Netty 的学习办法和进阶策略》
《史上最艰深 Netty 框架入门长文:根本介绍、环境搭建、入手实战》

如果你连 Java 的 NIO 都不晓得是什么,上面的文章倡议优先读一下:

《少啰嗦!一分钟带你读懂 Java 的 NIO 和经典 IO 的区别》
《史上最强 Java NIO 入门:放心从入门到放弃的,请读这篇!》
《Java 的 BIO 和 NIO 很难懂?用代码实际给你看,再不懂我转行!》

Netty 源码和 API 的在线浏览地址:

1)Netty-4.1.x 残缺源码(在线浏览版)(* 举荐)
2)Netty-4.0.x 残缺源码(在线浏览版)
3)Netty-4.1.x API 文档(在线版)(* 举荐)
4)Netty-4.0.x API 文档(在线版)

3、运行成果

在开始学习之前,先给大家演示下本文配套源码的运行成果(源码在本文“4、本文源码”一节的附件处可下载)。

聊天页面:

增加好友:

音讯揭示:

4、本文源码

本文残缺代码附件下载:
(请从同步公布链接中下载:http://www.52im.net/thread-37…)

源码的目录构造,如下图所示:

这套 IM 代码分为了三组模块:UI、客户端、服务端。

之所以这样拆分,是为了将 UI 展现与业务逻辑隔离,应用事件和接口进行驱动,让代码档次更加洁净整洁易于扩大和保护。

各模块的作用,具体解释如下:

5、零碎设计

在这套 IM 中,服务端采纳 DDD 畛域驱动设计模式进行搭建。将 Netty 的性能交给 SpringBoot 进行启停管制,同时在服务端搭建控制台能够十分不便的操作通信零碎,进行用户和通信治理。在客户端的建设上采纳 UI 拆散的形式进行搭建,以保障业务代码与 UI 展现拆散,做到十分易于扩大的管制。

另外,在性能实现上包含:完满仿照微信桌面版客户端、登录、搜寻增加好友、用户通信、群组通信、表情发送等外围性能。如果有对于理论须要应用的性能,能够依照这套零碎框架进行扩大。

解释一下:

1)UI 开发:应用 JavaFx 与 Maven 搭建 UI 桌面工程,逐渐解说登录框体、聊天框体、对话框、好友栏等各项 UI 展现及操作事件;
2)架构设计:应用 DDD 畛域驱动设计的四层模型构造与 Netty 联合应用,架构出正当的分层框架(相应库表性能的设计);
3)性能实现:包含;登录、增加好友、对话告诉、音讯发送、断线重连等各项性能。

6、UI 开发

6.1 性能划分
聊天窗体,绝对于登陆窗体来说,聊天窗体的内容会比拟多,同时也会绝对简单一些。

下图是聊天窗体的性能定义草图:

如上图所示:

1)首先是咱们整个聊天主窗体的定义,是一块空白面板,并去掉默认的边框按钮 (最小化、退出等);
2)之后是咱们左侧边栏,咱们称之为条形 Bar,性能区域的实现;
3)最初增加窗体事件,当点击按钮时变换 内容面板 中的填充信息。

6.2 聊天界面
对话框选中后的内容区域展示,也就是用户之间信息发送和展示。

从整体上看这是一个联动的过程,点击左侧的对话框用户,右侧就有相应内容的填充。那么右侧被填充对话列表 ListView 须要与每一个对话用户关联,点击聊天用户的时候,是通过重复切换填充的过程。成果如下图所示。

参见上图,我解释一下:

1)点击左侧的每一个对话框体,右侧聊天框填充内容即随之变动(同时还有相应的对话名称也会也变动);
2)对话框中左侧展现好友发送的信息,右侧展现集体发送的信息(同时音讯内容会随着内容的增多而减少高度和宽度);
3)最上面是文本输入框,在前面的实现里咱们文本输入框采纳专用的形式进行设计,当然你也能够设计为独自的集体应用。

6.3 好友列表
大家都常常应用 PC 端的微信,能够晓得在好友栏里是分了几段内容的,其中蕴含:新的敌人、公众号、群组和最上面的好友(性能划分如下图)。

参见上图,我解释一下:

1)最下面的搜寻框这部分内容不变,和后面的一样。咱们目前应用的形式是 fxml 设计,例如这部分是通用性能,能够抽取进去放到代码中,设计成一个组件元素类;
2)通过咱们的剖析,在应用 JavaFx 组件开发为根底下,这部分是一种嵌套 ListView,也就是最底层的面板是一个 ListView,好友和群组有各是一个 ListView,这样解决后咱们会很不便的进行数据填充;
3)另外这样的构造次要有利于在咱们程序运行过程中,如果你增加了好友,那么咱们须要将好友信息刷新到好友栏中,而在数据填充的时候,为了更加便捷高效,所以咱们设计了嵌套的 ListView(如果还不是特地了解,能够从后续的代码中取得答案)。

6.4 事件定义
在桌面版 UI 开发中,为了能使 UI 与业务逻辑隔离,须要在咱们把 UI 打包后提供出操作界面的展现成果的接口以及界面操作事件抽象类。

那么能够依照下图了解:

以上这些接口就是咱们目前 UI 为内部提供的所有行为接口,这些接口的一个链路形容就是:关上窗口、搜寻好友、增加好友、关上对话框、发送音讯。

7、通信设计

7.1 零碎架构

在后面我说到更适宜的架构,才是合乎你当下须要最好的架构。

那么怎么设计须要的架构呢?

之所以这样设计,在这个零碎里有如下几点前提:

1)零碎在服务端要有 web 页面进行治理通信用户以及服务端的管制和监控;
2)数据库的对象类,不要被内部净化,要有隔离性(比方:你的数据库类暴漏给内部做展现类应用了,那么当初须要减少一个字段,而这个字段又不是你数据库存在的属性。那么这个时候就曾经把数据库类净化了)。
3)因为目前都是在 Java 语言下实现 Netty 通信,那么服务端与客户端都会须要应用到通信过程中的协定定义和解析。那么咱们须要抽离这一层对外提供 Jar 包(利于重用,不然客户端和服务端复制同样的代码保护,就太恶心了);
4)接口、业务解决、底层服务、通信交互,要有明确的辨别和实现,防止造成凌乱难以保护。

联合咱们下面这四点的前提,你头脑中有什么模型构造体现了?以及相应的技术栈抉择上是否有打算了?

接下来我会介绍两种架构设计的模型,一种是你十分相熟的 MVC,另外一种是你可能据说过的 DDD 畛域驱动设计。

7.2 通信协议

从图稿上来看,咱们在传输对象的时候须要在传输包中增加一个“帧标识”以此来判断以后的业务对象是哪个对象,也就能够让咱们的业务更加清晰,防止应用大量的 if 语句判断。

协定框架:

agreement
└── 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

协定包:

public abstract class Packet {

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);

}



/**

 * 获取协定指令

 *

 * @return 返回指令值

 */

public abstract Byte getCommand();

}

7.3 增加好友

从下面的流程图中能够看到,这里蕴含了两局部内容:搜寻好友和增加好友。

当增加实现好友后,好友会呈现到咱们的好友栏中。

并且这外面咱们采纳的是单方面批准加好友,也就是你增加一个好友的时候,对方也同样有你的好友信息。

如果你的业务中是须要增加好友并批准的,那么能够在发动好友增加的时候,增加一条状态信息,申请加好友。对方批准后,两个用户能力成为好友并进行通信。

增加好友的样例代码:

public class AddFriendHandler extends MyBizHandler<AddFriendRequest> {

public AddFriendHandler(UserService userService) {super(userService);

}

@Override

public void channelRead(Channel channel, AddFriendRequest msg) {// 1. 增加好友到数据库中[A->B B->A]

    List<UserFriend> userFriendList = newArrayList<>();

    userFriendList.add(newUserFriend(msg.getUserId(), msg.getFriendId()));

    userFriendList.add(newUserFriend(msg.getFriendId(), msg.getUserId()));

    userService.addUserFriend(userFriendList);

    // 2. 推送好友增加实现 A

    UserInfo userInfo = userService.queryUserInfo(msg.getFriendId());

    channel.writeAndFlush(newAddFriendResponse(userInfo.getUserId(), userInfo.getUserNickName(), userInfo.getUserHead()));

    // 3. 推送好友增加实现 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()));

}

}

7.4 音讯应答

从整体的流程能够看到:在用户发动好友、群组通信的时候,会触发一个事件行为,接下来客户端向服务端发送与好友的对话申请。

服务端收到对话申请后:如果是好友对话,那么须要保留与好友的通信信息到对话框中。同时告诉好友,我与你要通信了。你在本人的对话框列表中,把我加进去。

如果是群组通信:是能够不必这样告诉的,因为不可能把还没有在线的所有群组用户全副告诉(人家还没登录呢),所以这部分只须要在用户上线收到信息后,创立出对话框到列表中即可。能够认真了解下,同时也能够想想其余实现的形式。

音讯应答样例代码:

public class MsgHandler extends MyBizHandler<MsgRequest> {

public MsgHandler(UserService userService) {super(userService);

}

@Override

public void channelRead(Channel channel, MsgRequest msg) {logger.info("音讯信息处理:{}", JSON.toJSONString(msg));

    // 异步写库

    userService.asyncAppendChatRecord(newChatRecordInfo(msg.getUserId(), msg.getFriendId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));

    // 增加对话框[如果对方没有你的对话框则增加]

    userService.addTalkBoxInfo(msg.getFriendId(), msg.getUserId(), Constants.TalkType.Friend.getCode());

    // 获取好友通信管道

    Channel friendChannel = SocketChannelUtil.getChannel(msg.getFriendId());

    if(null== friendChannel) {logger.info("用户 id:{}未登录!", msg.getFriendId());

        return;

    }

    // 发送音讯

    friendChannel.writeAndFlush(newMsgResponse(msg.getUserId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));

}

}

7.5 断线重连

从上述流程中咱们看到:当网络连接断开当前,会像服务端发送从新链接的申请。那么在这个发动链接的过程,和零碎的最开始链接有所区别。断线重连是须要将用户的 ID 信息一起发送给服务端,好让服务端能够去更新用户与通信管道 Channel 的绑定关系。

同时还须要更新群组内的重连信息,把用户的重连退出群组映射中。此时就能够复原用户与好友和群组的通信性能。

音讯应答样例代码:

// Channel 状态定时巡检;3 秒后每 5 秒执行一次

scheduledExecutorService.scheduleAtFixedRate(() -> {while(!nettyClient.isActive()) {System.out.println(“ 通信管道巡检:通信管道状态 ”+ nettyClient.isActive());

    try{System.out.println("通信管道巡检:断线重连 [Begin]");

        Channel freshChannel = executorService.submit(nettyClient).get();

        if(null== CacheUtil.userId) continue;

        freshChannel.writeAndFlush(newReconnectRequest(CacheUtil.userId));

    } catch(InterruptedException | ExecutionException e) {System.out.println("通信管道巡检:断线重连 [Error]");}

}

}, 3, 5, TimeUnit.SECONDS);

相干文章学习:

《为何基于 TCP 协定的挪动端 IM 依然须要心跳保活机制?》
《一文读懂即时通讯利用中的网络心跳包机制:作用、原理、实现思路等》
《融云技术分享:融云安卓端 IM 产品的网络链路保活技术实际》
《正确理解 IM 长连贯的心跳及重连机制,并入手实现(有残缺 IM 源码)》
《一种 Android 端 IM 智能心跳算法的设计与实现探讨(含样例代码)》
《手把手教你用 Netty 实现网络通信程序的心跳机制、断线重连机制》
《Web 端即时通讯实际干货:如何让你的 WebSocket 断网重连更疾速?》

7.6 集群通信

如上图所示,我是这样实现 IM 集群通信的:

1)跨服务之间案例采纳 redis 的公布和订阅进行传递音讯,如果你是大型服务能够应用 zookeeper;
2)用户 A 在发送音讯给用户 B 时候,须要传递 B 的 channeId,以用于服务端进行查找 channeId 所属是否本人的服务内;
3)单台机器也能够启动多个 Netty 服务,程序内会主动寻找可用端口。

8、本文小结

此 IM 零碎波及到的技术栈内容较多:Netty4.x、SpringBoot、Mybatis、Mysql、JavaFx、layui 等技术栈的应用,以及整个零碎框架结构采纳 DDD 四层架构 +Socket 模块的形式进行搭建,所有的 UI 都以前后端拆散事件驱动形式进行设计。在这个过程中只有你能保持学习下来,那么肯定会播种十分多的内容。足够吹牛啦!

任何一个新技术栈的学习过程都会包含这样一条路线:运行 HelloWorld、纯熟应用 API、我的项目实际以及最初的深度源码开掘。那么在听到这样一个需要时候,Java 程序员必定会想到一些列的技术知识点来填充咱们我的项目中的各个模块(例如:界面用 JavaFx、Swing 等,通信用 Socket 或者晓得 Netty 框架、服务端管制用 MVC 模型加上 SpringBoot 等)。然而怎么将这些各个技术栈正当的架设出咱们的零碎确是学习、实际、成长过程中最重要的局部。

好了,IM 开发实际上波及的常识维度十分多,限于篇幅就不在这里啰嗦更多,各位读者务必对着源码同步进行学习,这样成果会更好(源码在本文“4、本文源码”一节的附件处可下载)。

9、参考资料

[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] 一套亿级用户的 IM 架构技术干货(上篇):整体架构、服务拆分等
[13] 一套亿级用户的 IM 架构技术干货(下篇):可靠性、有序性、弱网优化等
[14] 从老手到专家:如何设计一套亿级音讯量的分布式 IM 零碎
[15] 基于实际:一套百万音讯量小规模 IM 零碎技术要点总结

本文已同步公布于“即时通讯技术圈”公众号。
同步公布链接是:http://www.52im.net/thread-37…

正文完
 0