Gointerface接口声明实现及作用详解
218
2022-10-11
基于Netty,从零开发IM(三):编码实践篇(群聊功能)(netty实现im)
本文由作者“大白菜”分享,有较多修订和改动。注意:本系列是给IM初学者的文章,IM老油条们还望海涵,勿喷!
1、引言
接上两篇《IM系统设计篇》、《编码实践篇(单聊功能)》,本篇主要讲解的是通过实战编码实现IM的群聊功能,内容涉及群聊技术实现原理、编码实践等知识。
2、写在前面
建议你在阅读本文之前,务必先读本系列的前两篇《IM系统设计篇》、《编码实践篇(单聊功能)》,在着重理解IM系统的理论设计思路之后,再来阅读实战代码则效果更好。
最后,在开始本文之前,请您务必提前了解Netty的相关基础知识,可从本系列首篇《IM系统设计篇》中的“知识准备”一章开始。
3、系列文章
本文是系列文章的第3篇,以下是系列目录:
《基于Netty,从零开发IM(一):IM系统设计篇》《基于Netty,从零开发IM(二):编码实践篇(单聊功能)》《基于Netty,从零开发IM(三):编码实践篇(群聊功能)》(* 本文)《基于Netty,从零开发IM(四):编码实践篇(系统优化)》(稍后发布.. )
4、本篇概述
在上篇《编码实践篇(单聊功能)》中,我们主要实现了IM的单聊功能,本节主要是实现IM群聊功能。
本篇涉及的群聊核心功能,大致如下所示:
1)登录:每个客户端连接服务端的时候,都需要输入自己的账号信息,以便和连接通道进行绑定;2)创建群组:输入群组 ID 和群组名称进行创建群组。需要先根据群组 ID 进行校验,判断是否已经存在了;3)查看群组:查看目前已经创建的群组列表;4)加入群组:主要参数是群组 ID 和用户 ID,用户 ID 只需从 Channel 的绑定属性里面获取即。主要是判断群组 ID 是否存在,如果存在还需要判断该用户 ID 是否已经在群组里面了;5)退出群组:主要是判断群组 ID 是否存在,如果存在则删除相应的关系;6)查看组成员:根据群组 ID 去查询对应的成员列表;7)群发消息:选择某个群进行消息发送,该群下的成员都能收到信息。主要判断群组 ID 是否存在,如果存在再去获取其对应的成员列表。
5、群聊原理
其实群聊和单聊,整体上原理是一样的,只是做了一下细节上的升级。
在首篇《IM系统设计篇》的“6、IM群聊思路设计”设计部分也做了详细的说明了。
群聊的大概流程就是:根据群组 ID 查找到所有的成员集合,然后再遍历找到每个成员对应的连接通道。
具体的群聊架构思路如下图:
如上图所示,群聊通讯流程技术原理如下:
1)群聊和单聊整体上的思路一致:需要保存每个用户和通道的对应关系,方便后期通过用户 ID 去查找到对应的通道,再跟进通道推送消息;2)群聊把消息发送给群员的原理:其实很简单,服务端再保存另外一份映射关系,那就是聊天室和成员的映射关系。发送消息时,首先根据聊天室 ID 找到对应的所有成员,然后再跟进各个成员的 ID 去查找到对应的通道,最后由每个通道进行消息的发送;3)群成员加入某个群聊聊的时候:往映射表新增一条记录,如果成员退群的时候则删除对应的映射记录。
6、运行效果
补充说明:因为本系列文章主要目的是引导IM初学者在基于Netty的情况下,如何一步一步从零写出IM的逻辑和思维能力,因而为了简化编码实现,本篇中编码实现的客户端都是基于控制台实现的(希望不要被嫌弃),因为理解技术的本质显然比炫酷的外在表现形式更为重要。
用户登录效果图:
群组操作效果图:
7、实体定义实战
7.1 服务端实体
服务端映射关系的管理,分别是:
1)登录信息(用户 ID 和通道);2)群组信息(群组 ID 和群组成员关系)。
主要通过两个 Map 去维护,具体如下:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map
7.2 实体和指令关系
我们准备好相应的实体,以及实体和指令的映射关系,具体如下所示:
private static Map
通过下面这张图,能看的更清晰一些:
8、Handler定义实战
IM群聊功能的实现,我们需要两个两个业务 Handler:
1)分别是客户端(ClientChatGroupHandler);2)服务端(ServerChatGroupHandler)。
8.1 客户端 Handler
客户端 Handler,主要是通过判断实体类型来做不同的业务操作,当然也可以使用 SimpleChannelInboundHandler 去进行 Handler 拆分。
public class ClientChatGroupHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //在链接就绪时登录 login(ctx.channel()); } //主要是“接受服务端”的响应信息 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if(msg instanceof LoginResBean){ LoginResBean res=(LoginResBean) msg; System.out.println("登录响应:"+res.getMsg()); if(res.getStatus()==0){ //登录成功 //1.给通道绑定身份 ctx.channel().attr(AttributeKey.valueOf("userid")).set(res.getUserid()); //2.显示操作类型【请看下面】 deal(ctx.channel()); }else{ //登录失败,继续登录 login(ctx.channel()); } }else if(msg instanceof GroupCreateResBean){ GroupCreateResBean res=(GroupCreateResBean)msg; System.out.println("创建响应群组:"+res.getMsg()); }else if(msg instanceofGroupListResBean){ GroupListResBean res=(GroupListResBean)msg; System.out.println("查看群组列表:"+res.getLists()); }elseif(msg instanceofGroupAddResBean){ GroupAddResBean res=(GroupAddResBean)msg; System.out.println("加入群组响应:"+res.getMsg()); }elseif(msg instanceof GroupQuitResBean){ GroupQuitResBean res=(GroupQuitResBean)msg; System.out.println("退群群组响应:"+res.getMsg()); }else if(msg instanceof GroupMemberResBean){ GroupMemberResBean res=(GroupMemberResBean)msg; if(res.getCode()==1){ System.out.println("查看成员列表:"+res.getMsg()); }else{ System.out.println("查看成员列表:"+res.getLists()); } }else if(msg instanceof GroupSendMsgResBean){ GroupSendMsgResBean res=(GroupSendMsgResBean)msg; System.out.println("群发消息响应:"+res.getMsg()); }else if(msg instanceof GroupRecMsgBean){ GroupRecMsgBean res=(GroupRecMsgBean)msg; System.out.println("收到消息fromuserid="+ res.getFromuserid()+ ",msg="+res.getMsg()); } }}
通过子线程循环向输出控制台输出操作类型的方法,以下方法目前都是空方法,下面将详细讲解。
private void deal(final Channel channel){ final Scanner scanner=new Scanner(System.in); new Thread(new Runnable() { public void run() { while(true){ System.out.println("请选择类型:0创建群组,1查看群组,2加入群组,3退出群组,4查看群成员,5群发消息"); int type=scanner.nextInt(); switch(type){ case 0: createGroup(scanner,channel); break; case 1: listGroup(scanner,channel); break; case 2: addGroup(scanner,channel); break; case 3: quitGroup(scanner,channel); break; case 4: listMembers(scanner,channel); break; case 5: sendMsgToGroup(scanner,channel); break; default: System.out.println("输入的类型不存在!"); } } } }).start(); }
8.2 服务端 Handler
服务端 Handler,主要是通过判断实体类型来做不同的业务操作,当然也可以使用 SimpleChannelInboundHandler 去进行 Handler 拆分。
以下方法目前都是空方法,下面将详细讲解。
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map
9、具体功能编码实战
9.1 创建群组
客户端请求:
private void createGroup(Scanner scanner,Channel channel){ System.out.println("请输入群组ID"); Integer groupId=scanner.nextInt(); System.out.println("请输入群组名称"); String groupName=scanner.next(); GroupCreateReqBean bean=new GroupCreateReqBean(); bean.setGroupId(groupId); bean.setGroupName(groupName); channel.writeAndFlush(bean); }
服务端处理:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map
9.2 查看群组
客户端请求:
private void listGroup(Scanner scanner,Channel channel){ GroupListReqBean bean=new GroupListReqBean(); bean.setType("list"); channel.writeAndFlush(bean);}
服务端处理:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map
9.3 加入群组
客户端请求:
private void addGroup(Scanner scanner,Channel channel){ System.out.println("请输入加入的群组ID"); int groupId=scanner.nextInt(); Integer userId=(Integer) channel.attr(AttributeKey.valueOf("userid")).get(); GroupAddReqBean bean=new GroupAddReqBean(); bean.setUserId(userId); bean.setGroupId(groupId); channel.writeAndFlush(bean);}
服务端处理:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map
9.4 退出群组
客户端请求:
private void quitGroup(Scanner scanner,Channel channel){ System.out.println("请输入退出的群组ID"); int groupId=scanner.nextInt(); Integer userId=(Integer) channel.attr(AttributeKey.valueOf("userid")).get(); GroupQuitReqBean bean=new GroupQuitReqBean(); bean.setUserId(userId); bean.setGroupId(groupId); channel.writeAndFlush(bean);}
服务端处理:
public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map 9.5 查看群组成员 客户端请求: private void listMembers(Scanner scanner,Channel channel){ System.out.println("请输入群组ID:"); int groupId=scanner.nextInt(); GroupMemberReqBean bean=new GroupMemberReqBean(); bean.setGroupId(groupId); channel.writeAndFlush(bean);} 服务端处理: public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map 9.6 群发消息 客户端请求: private void sendMsgToGroup(Scanner scanner,Channel channel){ System.out.println("请输入群组ID:"); int groupId=scanner.nextInt(); System.out.println("请输入发送消息内容:"); String msg=scanner.next(); Integer userId=(Integer) channel.attr(AttributeKey.valueOf("userid")).get(); GroupSendMsgReqBean bean=new GroupSendMsgReqBean(); bean.setFromuserid(userId); bean.setTogroupid(groupId); bean.setMsg(msg); channel.writeAndFlush(bean);} 服务端处理: public class ServerChatGroupHandler extends ChannelInboundHandlerAdapter { private static Map 10、本篇小结 本篇中涉及的功能点稍微有点多,主要是实现了群聊的几个核心功能,分别是:创建群组、查看群组列表、加入群组、退出群组、查看成员列表、群发消息。 这些功能经过拆解,看起来就不是那么复杂了,希望大家都可以亲自动手实现一遍,加深理解,提高学习效果。 实际上,真正的产品级IM中,群聊涉及的技术细节是非常多的,有兴趣可以详读下面这几篇: 11、参考资料 [1] 手把手教你用Netty实现心跳机制、断线重连机制 [2] 自已开发IM很难?手把手教你撸一个Andriod版IM [3] 基于Netty,从零开发一个IM服务端 [4] 拿起键盘就是干,教你徒手开发一套分布式IM系统 [5] 正确理解IM长连接、心跳及重连机制,并动手实现 [6] 手把手教你用Go快速搭建高性能、可扩展的IM系统 [7] 手把手教你用WebSocket打造Web端IM聊天 [8] 万字长文,手把手教你用Netty打造IM聊天 [9] 基于Netty实现一套分布式IM系统 [10] 基于Netty,搭建高性能IM集群(含技术思路+源码) [11] SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM聊天功能 学习交流: - 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此) (本文已同步发布于:http://52im.net/thread-3981-1-1.html)
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~