关于即时通讯:基于Netty从零开发IM三编码实践篇群聊功能

3次阅读

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

本文由作者“大白菜”分享,有较多订正和改变。留神:本系列是给 IM 初学者的文章,IM 老油条们还望海涵,勿喷!

1、引言

接上两篇《IM 零碎设计篇》、《编码实际篇(单聊性能)》,本篇次要解说的是通过实战编码实现 IM 的群聊性能,内容波及群聊技术实现原理、编码实际等常识。

学习交换:- 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》- 开源 IM 框架源码:https://github.com/JackJiang2…(备用地址点此)
(本文已同步公布于:http://www.52im.net/thread-39…)

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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();}// 组和成员列表关系实体 @Datapublic class Group implements Serializable {private String groupName;    private List<GroupMember> members=new ArrayList<GroupMember>();}// 成员和连贯通道的关系实体 public class GroupMember implements Serializable {private Integer userid;    private Channel channel;}7.2 实体和指令关系咱们筹备好相应的实体,以及实体和指令的映射关系,具体如下所示:private static Map<Byte, Class<? extends BaseBean>> map=new HashMap<Byte,Class<? extends BaseBean>>();    static{        // 登录的申请和响应实体        map.put(1, LoginReqBean.class);        map.put(2, LoginResBean.class);         // 创立群组的申请和响应实体        map.put(3, GroupCreateReqBean.class);        map.put(4, GroupCreateResBean.class);         // 查看群组的申请和响应实体        map.put(5, GroupListReqBean.class);        map.put(6, GroupListResBean.class);         // 退出群组的申请和响应实体        map.put(7,GroupAddReqBean.class);        map.put(8,GroupAddResBean.class);         // 退出群组的申请和响应实体        map.put(9,GroupQuitReqBean.class);        map.put(10,GroupQuitResBean.class);         // 查看成员列表的申请和响应实体        map.put(11,GroupMemberReqBean.class);        map.put(12,GroupMemberResBean.class);         // 发送响应的实体(发送音讯、发送响应、承受音讯)map.put(13,GroupSendMsgReqBean.class);        map.put(14,GroupSendMsgResBean.class);        map.put(15,GroupRecMsgBean.class);    }通过上面这张图,能看的更清晰一些:

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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {if(msg instanceof LoginReqBean) {// 登录            login((LoginReqBean) msg, ctx.channel());        }else if(msg instanceof GroupCreateReqBean){// 创立群组            createGroup((GroupCreateReqBean)msg,ctx.channel());        }else if(msg instanceof GroupListReqBean){// 查看群组列表            listGroup((GroupListReqBean)msg,ctx.channel());        }else if(msg instanceof GroupAddReqBean){// 退出群组            addGroup((GroupAddReqBean)msg,ctx.channel());        }else if(msg instanceof GroupQuitReqBean){// 退出群组            quitGroup((GroupQuitReqBean)msg,ctx.channel());        }else if(msg instanceof GroupMemberReqBean){// 查看成员列表            listMember((GroupMemberReqBean)msg,ctx.channel());        }else if(msg instanceof GroupSendMsgReqBean){// 音讯发送            sendMsg((GroupSendMsgReqBean) msg,ctx.channel());        }    }}

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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     private void createGroup(GroupCreateReqBean bean,Channel channel){// 定义一个响应实体        GroupCreateResBean res=new GroupCreateResBean();        // 查问 groups 是否曾经存在        Group group=groups.get(bean.getGroupId());        // 判断是否曾经存在        if(group==null){// 定义群组实体            Group g=new Group();            // 定义一个汇合,专门存储成员            List<GroupMember> members=new ArrayList<GroupMember>();            // 属性赋值            g.setGroupName(bean.getGroupName());            g.setMembers(members);            // 增加到 Map 外面            groups.put(bean.getGroupId(),g);             // 响应信息            res.setCode(0);            res.setMsg(“ 创立群组胜利 ”);        }else{res.setCode(1);            res.setMsg(“ 该群组曾经存在!”);        }        channel.writeAndFlush(res);    }}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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     private void listGroup(GroupListReqBean bean,Channel channel){if(“list”.equals(bean.getType())){// 定义一个响应实体            GroupListResBean res=new GroupListResBean();            // 定义一个汇合            List<GroupInfo> lists=new ArrayList<GroupInfo>();            // 变量 groups Map 汇合            for(Map.Entry<Integer, Group> entry : groups.entrySet()){Integer mapKey = entry.getKey();                Group mapValue = entry.getValue();                GroupInfo gi=new GroupInfo();                gi.setGroupId(mapKey);                gi.setGroupName(mapValue.getGroupName());                lists.add(gi);            }            // 把汇合增加到响应实体外面            res.setLists(lists);            // 开始写到客户端            channel.writeAndFlush(res);        }    }}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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     private void addGroup(GroupAddReqBean bean,Channel channel){GroupAddResBean res=new GroupAddResBean();        //1. 依据“群组 ID”获取对应的“组信息”Group group=groups.get(bean.getGroupId());        //2.“群组”不存在        if(group==null){res.setCode(1);            res.setMsg(“groupId=”+bean.getGroupId()+”, 不存在!”);            channel.writeAndFlush(res);            return;        }        //3.“群组”存在,则获取其底下的“成员汇合”List<GroupMember> members=group.getMembers();        boolean flag=false;        //4. 遍历汇合,判断“用户”是否曾经存在了        for(GroupMember gm:members){if(gm.getUserid()==bean.getUserId()){flag=true;                break;}        }        if(flag){res.setCode(1);            res.setMsg(“ 曾经在群组外面, 无奈再次退出!”);        }else{//1. 用户信息            GroupMember gm=new GroupMember();            gm.setUserid(bean.getUserId());            gm.setChannel(channel);             //2. 增加到汇合外面            members.add(gm);             //3. 给“群组”从新赋值            group.setMembers(members);             res.setCode(0);            res.setMsg(“ 退出群组胜利 ”);        }        channel.writeAndFlush(res);    }}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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     private void quitGroup(GroupQuitReqBean bean,Channel channel){GroupQuitResBean res=new GroupQuitResBean();         //1. 依据“群组 ID”获取对应的“组信息”Group group=groups.get(bean.getGroupId());        if(group==null){//2. 群组不存在            res.setCode(1);            res.setMsg(“groupId=”+bean.getGroupId()+”, 不存在!”);            channel.writeAndFlush(res);            return;        }        //3. 群组存在,则获取其底下“成员汇合”List<GroupMember> members=group.getMembers();        //4. 遍历汇合,找到“以后用户”在汇合的序号        int index=-1;        for(inti=0;i<members.size();i++){if(members.get(i).getUserid()==bean.getUserId()){index=i;                break;}        }        //5. 如果序号等于 -1,则示意“以后用户”不存在汇合外面        if(index==-1){res.setCode(1);            res.setMsg(“userid=”+bean.getUserId()+”, 不存在该群组外面!”);            channel.writeAndFlush(res);            return;        }        //6. 从汇合外面删除“以后用户”members.remove(index);        //7. 给“群组”的“成员列表”从新赋值        group.setMembers(members);        res.setCode(0);        res.setMsg(“ 退出群组胜利 ”);        channel.writeAndFlush(res);    }}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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     private void listMember(GroupMemberReqBean bean,Channel channel){GroupMemberResBean res=new GroupMemberResBean();        List<Integer> lists=new ArrayList<Integer>();        //1. 依据“群组 ID”获取对应的“组信息”Group group=groups.get(bean.getGroupId());        if(group==null){//2. 查问的群组不存在            res.setCode(1);            res.setMsg(“groupId=”+bean.getGroupId()+”, 不存在!”);            channel.writeAndFlush(res);        }else{//3. 群组存在,则变量其底层的成员            for(Map.Entry<Integer, Group> entry : groups.entrySet()){Group g = entry.getValue();                List<GroupMember> members=g.getMembers();                for(GroupMember gm:members){lists.add(gm.getUserid());                }            }             res.setCode(0);            res.setMsg(“ 查问胜利 ”);            res.setLists(lists);            channel.writeAndFlush(res);         }    }}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<Integer, Channel> map=new HashMap<Integer, Channel>();    private static Map<Integer, Group> groups=new HashMap<Integer, Group>();     privatevoidsendMsg(GroupSendMsgReqBean bean,Channel channel){GroupSendMsgResBean res=new GroupSendMsgResBean();         //1. 依据“群组 ID”获取对应的“组信息”Group group=groups.get(bean.getTogroupid());         //2. 给“发送人”响应,告诉其发送的音讯是否胜利        if(group==null){res.setCode(1);            res.setMsg(“groupId=”+bean.getTogroupid()+”, 不存在!”);            channel.writeAndFlush(res);            return;        }else{res.setCode(0);            res.setMsg(“ 群发音讯胜利 ”);            channel.writeAndFlush(res);        }        //3. 依据“组”上面的“成员”,变量并且一一推送音讯        List<GroupMember> members=group.getMembers();        for(GroupMember gm:members){GroupRecMsgBean rec=new GroupRecMsgBean();            rec.setFromuserid(bean.getFromuserid());            rec.setMsg(bean.getMsg());            gm.getChannel().writeAndFlush(rec);        }    }}

10、本篇小结

本篇中波及的性能点略微有点多,次要是实现了群聊的几个外围性能,别离是:创立群组、查看群组列表、退出群组、退出群组、查看成员列表、群发音讯。这些性能通过拆解,看起来就不是那么简单了,心愿大家都能够亲自动手实现一遍,加深了解,进步学习效果。实际上,真正的产品级 IM 中,群聊波及的技术细节是十分多的,有趣味能够详读上面这几篇:IM 群聊音讯如此简单,如何保障不丢不重?挪动端 IM 中大规模群音讯的推送如何保障效率、实时性?对于 IM 即时通讯群聊音讯的乱序问题探讨 IM 群聊音讯到底是存 1 份 (即扩散读) 还是存多份(即扩散写)?一套高可用、易伸缩、高并发的 IM 群聊、单聊架构方案设计实际网易云信技术分享:IM 中的万人群聊技术计划实际总结阿里电商 IM 音讯平台,在群聊、直播场景下的技术实际企业微信的 IM 架构设计揭秘:音讯模型、万人群、已读回执、音讯撤回等融云 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 聊天性能
(本文已同步公布于:http://www.52im.net/thread-39…)

正文完
 0