作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!

一、前言

这常识学的,基本没有忘的快呀?!

是不是感觉很多材料,点珍藏起来爽看视频时候嗨读文章过后会,只有过了那个劲,就完了,基本不记得这外面都讲了啥。工夫节约了,货色还没学到手,这是为啥?

其实因为学习也分为上策、中策和上策:

  • 上策:眼睛看就行,坐着、窝着、躺着,都行,反正也不累,还能一边回复下吹水的微信群
  • 中策:看完的材料做笔记整顿演绎,长期积攒材料
  • 上策:实际、上手、利用、调试、演绎、整顿材料,总结经验输入文档

综上,上策学起来很快感觉本人如同会了不少,中策有点要入手了懒不想动,上策就很耗时耗力了要本人对每一个知识点都能身体力行到亲力亲为。就这样你在学习的时候不盲目的就抉择了上策,因而其实并没有学到什么。

学习能把常识学到手,考究的是实际,在小傅哥编写的文章中,根本都是以实际代码验证后果为外围,讲述文章内容。从小我就喜爱入手,就以一个即时通信的我的项目为例,曾经基于不同技术计划实现了5、6次,仅为了实际技术,截图如下:

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

那么,这次IM实际的机会给你,心愿你能用的上!接下来我会给你介绍一个IM的零碎架构、通信协议、单聊群聊、表情发送、UI事件驱动等各项内容,以及提供全套的源码让你能够上手学习。

二、演示

在开始学习之前,先给大家演示下这套仿照PC端微信界面的IM零碎运行成果。

聊天页面

增加好友

视频演示

https://www.bilibili.com/video/BV1BZ4y1W7fC

三、零碎设计

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

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

  • UI开发:应用JavaFxMaven搭建UI桌面工程,逐渐解说登录框体、聊天框体、对话框、好友栏等各项UI展现及操作事件。从而在这一章节中让Java 程序员学会开发桌面版利用。
  • 架构设计:在这一章节中咱们会应用DDD畛域驱动设计的四层模型构造与Netty联合应用,架构出正当的分层框架。同时还有相应库表性能的设计。置信这些内容学习后,你肯定也能够假如出更好的框架。
  • 性能实现:这部分咱们次要将通信中的各项性能逐渐实现,包含;登录、增加好友、对话告诉、音讯发送、断线重连等各项性能。最终实现整个我的项目的开发,同时也能够让你从实际中学会技能。

四、UI开发

1. 整体构造定义、侧边栏

聊天窗体,绝对于登陆窗体来说,聊天窗体的内容会比拟多,同时也会绝对简单一些。因而咱们会分章节的逐渐来实现这些窗体以及事件和接口性能。在本篇文章中咱们会次要解说聊天框体的搭建以及侧边栏 UI 开发。

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

2. 对话聊天框

对话框选中后的内容区域展示,也就是用户之间信息发送和展示。从整体上看这是一个联动的过程,点击左侧的对话框用户,右侧就有相应内容的填充。那么右侧被填充对话列表 ListView 须要与每一个对话用户关联,点击聊天用户的时候,是通过重复切换填充的过程。

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

3. 好友栏

大家都常常应用 PC 端的微信,能够晓得在好友栏里是分了几段内容的,其中蕴含;新的敌人、公众号、群组和最上面的好友。

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

4. 事件定义

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

序号接口名形容
1void doShow()关上窗口
2void setUserInfo(String userId, String userNickName, String userHead)设置登陆用户 ID、昵称、头像
3void addTalkBox(int talkIdx, Integer talkType, String talkId, String talkName, String talkHead, String talkSketch, Date talkDate, Boolean selected)填充对话框列表
4void addTalkMsgUserLeft(String talkId, String msg, Date msgData, Boolean idxFirst, Boolean selected, Boolean isRemind)填充对话框音讯 - 好友 (他人的音讯)
  • 以上这些接口就是咱们目前 UI 为内部提供的所有行为接口,这些接口的一个链路形容就是;关上窗口、搜寻好友、增加好友、关上对话框、发送音讯。

五、通信设计

1. 零碎架构

在后面咱们说到更适宜的架构,才是合乎你当下须要最好的架构。那么怎么设计这样架构呢,根本就是要找到合乎点的指标。咱们之所以这样设计是为什么,那么在这个零碎里有如下几点;

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

联合咱们下面这四点的指标,你头脑中有什么模型构造体现了呢?以及相应的技术栈抉择上是否有打算了?接下来咱们会介绍两种架构设计的模型,一种是你十分相熟的 MVC,另外一种是你可能据说过的 DDD 畛域驱动设计。

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<? extends Packet>> 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();}

3. 增加好友

  • 从下面的流程中能够看到,这里蕴含了两局部内容;(1) 搜寻好友,(2) 增加好友。当天就实现好友后,好友会呈现到咱们的好友栏中。
  • 并且这外面咱们采纳的是单方面批准加好友,也就是你增加一个好友的时候,对方也同样有你的好友信息。
  • 如果你的业务中是须要增加好友并批准的,那么能够在发动好友增加的时候,增加一条状态信息,申请加好友。对方批准后,两个用户能力成为好友并进行通信。

增加好友,案例代码

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 = new ArrayList<>();        userFriendList.add(new UserFriend(msg.getUserId(), msg.getFriendId()));        userFriendList.add(new UserFriend(msg.getFriendId(), msg.getUserId()));        userService.addUserFriend(userFriendList);        // 2. 推送好友增加实现 A        UserInfo userInfo = userService.queryUserInfo(msg.getFriendId());        channel.writeAndFlush(new AddFriendResponse(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(new AddFriendResponse(friendInfo.getUserId(), friendInfo.getUserNickName(), friendInfo.getUserHead()));    }}

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(new ChatRecordInfo(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(new MsgResponse(msg.getUserId(), msg.getMsgText(), msg.getMsgType(), msg.getMsgDate()));    }}

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(new ReconnectRequest(CacheUtil.userId));        } catch (InterruptedException | ExecutionException e) {System.out.println("通信管道巡检:断线重连 [Error]");}    }}, 3, 5, TimeUnit.SECONDS);

6. 集群通信

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

六、源码下载

本我的项目是作者小傅哥应用JavaFx、Netty4.x、SpringBoot、Mysql等技术栈和偏差于DDD畛域驱动设计形式,搭建的仿桌面版微信实现通信外围性能。

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

序号工程介绍
1itstack-naive-chat-ui应用JavaFx开发的UI端,在咱们的UI端中提供了;登录框体、聊天框体,同时在聊天框体中有大量的行为交互界面以及接口和事件。最终我的UI端应用Maven打包的形式向外提供Jar包,以此来达到UI界面与业务行为流程拆散。
2itstack-naive-chat-client客户端是咱们的通信外围工程,次要应用Netty4.x作为咱们的socket框架来实现通信交互。并且在此工程中负责引入UI的Jar包,实现UI定义的事件(登录验证、搜寻增加好友、对话告诉、发送信息等等),以及须要应用咱们在服务端工程定义的通信协议来实现信息的交互操作。
3itstack-navie-chat-server服务端同样应用Netty4.x作为socket的通信框架,同时在服务端应用Layui作为治理后盾的页面,并且咱们的服务端采纳偏差于DDD畛域驱动设计的形式与Netty汇合,以此来达到咱们的框架结构整洁洁净易于扩大。
4itstack.sql系统工程数据库表构造以及初始化数据信息,共计6张外围表;用户表、群组表、用户群组关联表、好友表、对话表以及聊天记录表。用户在理论业务开发中能够自行拓展欠缺,目前库表构造只以外围性能为根底。
  • 源码获取:https://github.com/fuzhengwei/NaiveChat 亲,源码给我点个Star,不要白皮袄!!!

七、总结

  • 此IM零碎波及到的技术栈内容较多,Netty4.x、SpringBoot、Mybatis、Mysql、JavaFx、layui等技术栈的应用,以及整个零碎框架结构采纳DDD四层架构+Socket模块的形式进行搭建,所有的UI都以前后端拆散事件驱动形式进行设计,在这个过程中只有你能保持学习下来,那么肯定会播种十分多的内容。足够吹牛啦!
  • 任何一个新技术栈的学习过程都会包含这样一条路线;运行HelloWorld、纯熟应用API、我的项目实际以及最初的深度源码开掘。 那么在听到这样一个需要时候,Java程序员必定会想到一些列的技术知识点来填充咱们我的项目中的各个模块,例如;界面用JavaFx、Swing等,通信用Socket或者晓得Netty框架、服务端管制用MVC模型加上SpringBoot等。然而怎么将这些各个技术栈正当的架设出咱们的零碎确是学习、实际、成长过程中最重要的局部。