📣 📣 📣 📢📢📢
☀️☀️你好啊!小伙伴,我是小冷。是一个兴趣驱动自学练习两年半的的Java工程师。
📒 一位十分喜欢将知识分享出来的Java博主⭐️⭐️⭐️,擅长使用Java技术开发web项目和工具
📒 文章内容丰富:覆盖大部分java必学技术栈,前端,计算机基础,容器等方面的文章
📒 如果你也对Java感兴趣,关注小冷吧,一起探索Java技术的生态与进步,一起讨论Java技术的使用与学习
✏️高质量技术专栏专栏链接:微服务,数据结构,netty,单点登录,SSM,SpringCloudAlibaba等
😝公众号😝:想全栈的小冷,分享一些技术上的文章,以及解决问题的经验
⏩当前专栏:Netty 实战系列
⏩专栏代码地址:Netty练手项目仓库地址
IM 即时通讯系统
复用 web-im 开源项目的前端代码
地址: /javanf/web-im
使用时 安装node 启动服务端
我们重写的时候只需要修改app.vue
中的 WebSocket 的连接地址即可
修改之后 用npm run dev
启动项目即可,同时也将服务端启动
此时我们只需要发送一个信息
就可以看到前端传给我们的数据格式了
数据分析
此时我们将 websocket demo整合到了springboot中 确保客户端和服务端可以正常的通信分析客户端的数据结构 根据不同的逻辑返回对应的数据 “ 数据是启动项目的第一步”创建昵称登录登陆后可以查看在线用户 和与已存在的群组可以和其他用户一对一聊天可以创建群组和加入群组 让后发送消息 可以一对多聊天当前回传的功能分析
处理方式区别
按照处理方式的不同 可以分为操作类别 (操作用户 操作群组等) 消息类别 (一对一 一对多)
请求逻辑划分
可以分为
【用户登录】(创造链接)、【用户注销】(断开连接)
【创建群组】【加入群组】
【发送消息】(消息内部划分 私聊 &群聊)
用户 : 昵称 nickname和 id群组 : 群组 id 群组名称 name 用户列表消息(可以设计单独模型)数据模型设计
此时接续分析我们客户端发送给我们的 msg
msg:{"uid":"web_im_1650432464367","type":1,"nickname":"1","bridge":[],"groupId":""}
bridge
: 【uid , otheruid】 不为空代表一对一消息 uid 发送给 other Uid 的消息,
为空代表一对多消息 需要groupId
此时我们还需要考虑连接类型 从客户端 server/index.js 中就可以发现
// 创建连接case 1:......// 注销case 2:......// 创建群case 10:.......// 加入群case 20:......// 发送消息default:......
由此可以分析出 消息type状态码的几种类型
type
类型 :
1 : 创建连接
2 : 断开连接
10 : 创建群
20:加入群
默认(100):发消息
接口设计
数据模型
只展示字段 GETTER/SETTER 等自行添加
用户模型
public class UserModel {private String uid;private String nickName;//状态 1 在线 0 离线private int status;}
群组模型
public class GroupModel {private String id;private String name;//用户群组private List<UserModel> users;}
请求模型
@Datapublic class ReqModel {//请求类型private int type;//用户 idprivate String uid;//用户昵称private String nickname;//群组idprivate String groupId;//群组名称private String groupName;//消息内容private String msg;//群发列表private List<String> bridge;}
请求结果
@Datapublic class RespModel {//响应类型private int type;//日期private String date;//用户 idprivate String uid;//用户昵称private String nickname;//状态private int status;//返回联系人和群组列表private List<UserModel> users;private List<GroupModel> groups;private String groupId;//消息内容private String msg;//群发列表private List<String> bridge;}
列表存储
//存放本地数据public class LocalData {//存储连接的通道 分发消息使用public static final List<UserModel> userlist = new ArrayList<>();//存储通道和用户id的映射关系 用来获取消息通知的通道public static final Map<String, Channel> channelUserUrl = new HashMap<>();//在线用户列表public static final List<GroupModel> grouplist = new ArrayList<>();//群组列表public static final ChannelGroup channellist = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);//拿到存储连接通道列表中的通道public static List<Channel> getAllChannels() {List<Channel> channels = new ArrayList<>();Iterator<Channel> iterator = channellist.iterator();while (iterator.hasNext()) {Channel channel = iterator.next();channels.add(channel);}return channels;}//通过群组id 取出对应的群组public static GroupModel getGroupById(String id) {for (GroupModel groupModel : grouplist) {if (groupModel.getId().equals(id)) {return groupModel;}}return null;}}
用户上线 下线逻辑
根据前端客户端传回来的格式 我们可以设计出四个模型和定义请求响应逻辑
{"uid":"web_im_1650112539438","type":1,"nickname":"冷环渊","bridge":[],"groupId":""}
设置枚举处理类型
请求类型
@Getterpublic enum ReqType {//枚举内容CONN(1, "建立连接"),CANCEL(2, "断开连接"),ADD_GROUP(10, "创建群组"),JOIN_GROUP(20, "加入群组"),SEND_MSG(100, "发送消息");//编号private int num;//信息private String desc;ReqType(int num, String desc) {this.num = num;this.desc = desc;}// 增加一个根据数值遍历枚举类型public static ReqType getTypeByNum(int num) {ReqType[] reqTypes = ReqType.values();for (ReqType reqType : reqTypes) {if (num == reqType.getNum()) {return reqType;}}return ReqType.SEND_MSG;}}
响应类型
@Getterpublic enum RespType {//处理分类OPERA(1, "操作类处理"),MSG(2, "消息类处理");//编号private int num;//信息private String desc;RespType(int num, String desc) {this.num = num;this.desc = desc;}}
service 与 实现类
接口
public interface ChatService {//新增用户void addUser(ReqModel reqModel, RespModel respModel);//用户下线void delUser(ReqModel reqModel, RespModel respModel);//新增群组void addGroup(ReqModel reqModel, RespModel respModel);//加入群组void joinGroup(ReqModel reqModel, RespModel respModel);//发送群组消息void sendGroupMsg(ReqModel reqModel, RespModel respModel);//细聊void sendPrivateMsg(ReqModel reqModel, RespModel respModel);}
用户上线提示
@Overridepublic void addUser(ReqModel reqModel, RespModel respModel) {respModel.setMsg(reqModel.getNickname() + " : 加入聊天室");UserModel userModel = new UserModel(reqModel.getUid(), reqModel.getNickname(), 1);LocalData.userlist.add(userModel);respModel.setUsers(LocalData.userlist);respModel.setGroups(LocalData.grouplist);}
用户下线提示
@Overridepublic void delUser(ReqModel reqModel, RespModel respModel) {respModel.setMsg(reqModel.getNickname() + " : 离开聊天室");UserModel userModel = null;//遍历在线用户的列表for (int i = 0; i < LocalData.userlist.size(); i++) {UserModel temp = LocalData.userlist.get(i);if (temp.getUid().equals(reqModel.getUid())) {userModel = temp;break;}}//在用户列表删除要下线的用户LocalData.userlist.remove(userModel);respModel.setUsers(LocalData.userlist);respModel.setGroups(LocalData.grouplist);}
通道处理器
@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {System.out.println("msg : " + msg.text());// 获取请求数据 解析json形式ReqModel model = new Gson().fromJson(msg.text(), ReqModel.class);RespModel respModel = new RespModel();//获取当前时间LocalDateTime now = LocalDateTime.now();String date = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));respModel.setDate(date);respModel.setUid(model.getUid());respModel.setNickname(model.getNickname());//色湖之bridge 初始值 要默认空值List<String> defaultList = new ArrayList<>();respModel.setBridge(defaultList);// 默认类型respModel.setType(RespType.OPERA.getNum());//判断请求类型ReqType type = ReqType.getTypeByNum(model.getType());switch (type) {case CONN:System.out.println(model.getNickname() + ": 用户上线了");//记录并返回在线用户列表 以及已经创建的群组列表chatService.addUser(model, respModel);break;case CANCEL:System.out.println(model.getNickname() + ": 用户下线了");chatService.delUser(model, respModel);break;case ADD_GROUP:break;case JOIN_GROUP:break;case SEND_MSG:respModel.setType(RespType.MSG.getNum());break;default:}System.out.println(new Gson().toJson(respModel));List<Channel> channels = LocalData.getAllChannels();notifyChannels(channels, respModel);}//分发信息private void notifyChannels(List<Channel> channels, RespModel respModel) {for (Channel channel : channels) {TextWebSocketFrame resp = new TextWebSocketFrame(new Gson().toJson(respModel));channel.writeAndFlush(resp);}}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {//下线LocalData.channellist.remove(ctx.channel());}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//上线LocalData.channellist.add(ctx.channel());}
响应效果
msg : {"uid":"web_im_1650112539438","type":1,"nickname":"冷环渊","bridge":[],"groupId":""}冷环渊: 用户上线了{"type":1,"date":"-04-22 23:36:35","uid":"web_im_1650112539438","nickname":"冷环渊","status":0,"users":[{"uid":"web_im_1650619948362","nickName":"222","status":1},{"uid":"web_im_1650112539438","nickName":"冷环渊","status":1}],"groups":[],"msg":"冷环渊 : 加入聊天室","bridge":[]}
创建群聊
msg : {"uid":"web_im_1650112539438","type":10,"nickname":"冷环渊","groupName":"就将计就计","bridge":[]}
case ADD_GROUP:System.out.println(model.getNickname() + "用户创建了群组" + model.getGroupName());chatService.addGroup(model, respModel);break;
当时判断到枚举类型到创建群组的时候,我们就需要在 localdata的群组集合中加入一个新建的群组并且将创建的用户加入到群组中
实现chatService接口中的新增群组方法
@Overridepublic void addGroup(ReqModel reqModel, RespModel respModel) {respModel.setMsg(reqModel.getNickname() + ":创建了群 :" + reqModel.getGroupName());// 把创建者加入到群组的成员列表中UserModel self = new UserModel(reqModel.getUid(), reqModel.getNickname());List<UserModel> users = new ArrayList<>();users.add(self);//此处 开源项目设计的是 UID由客户端创建 GroupID由服务端创建。思考 一般来说id都是服务端创建的String groupId = "group_" + reqModel.getUid() + "_" + reqModel.getGroupName();GroupModel groupModel = new GroupModel(groupId, reqModel.getGroupName(), users);LocalData.grouplist.add(groupModel);respModel.setGroups(LocalData.grouplist);}
在之后 点击创建群组就可以发现 群组新增成功
群聊操作
加入群聊
加入群聊的思路也是类似的
case JOIN_GROUP:System.out.println(model.getNickname() + "用户加入了群组" + model.getGroupName());chatService.joinGroup(model, respModel);break;
当请求的类型是加入群组的时候 我们需要将当前用户加入到对应的群组users中
实现chatService接口中的新增群组方法
@Overridepublic void joinGroup(ReqModel reqModel, RespModel respModel) {respModel.setMsg(reqModel.getNickname() + ":加入了群 :" + reqModel.getGroupName());for (GroupModel groupModel : LocalData.grouplist) {if (groupModel.getId().equals(reqModel.getGroupId())) {UserModel self = new UserModel(reqModel.getUid(), reqModel.getNickname());groupModel.getUsers().add(self);}}respModel.setGroups(LocalData.grouplist);}
信息发送处理
思路
首先根据请求的类型我们发现,客户端发起信息处理请求的时候 是依靠bridge
和Groupid
来判断是群聊还是私聊的
case SEND_MSG://识别响应类型 消息类型是更改respModel.setType(RespType.MSG.getNum());//判断一对一消息还是一对多消息if (model.getBridge().size() == 0) {// 一对多chatService.sendGroupMsg(model, respModel);} else {chatService.sendPrivateMsg(model, respModel);}break;
实现对应的接口方法
//发送群组消息@Overridepublic void sendGroupMsg(ReqModel reqModel, RespModel respModel) {respModel.setMsg(reqModel.getMsg());respModel.setGroupId(reqModel.getGroupId());respModel.setStatus(1);}//发送一对一消息@Overridepublic void sendPrivateMsg(ReqModel reqModel, RespModel respModel) {respModel.setMsg(reqModel.getMsg());respModel.setBridge(reqModel.getBridge());respModel.setGroupId("");respModel.setStatus(1);}
发送通道处理
一对一
一对一的时候bridge数组的 第0位 就是我我们自身 第一位 就是我们需要发送消息的人,
接下来只需要根据用户的id来获取到对应的通道,之后创建集合 使用分发方法
// 根据一对一 或者一对多的类型来找到接受通知的用户if (model.getBridge().size() > 0) {// 代表一对一,只需要通知自身和需要接受消息的用户String selfId = model.getBridge().get(0);Channel selfChannel = LocalData.channelUserUrl.get(selfId);//接受信息的通道String otherId = model.getBridge().get(1);Channel otherChannel = LocalData.channelUserUrl.get(otherId);List<Channel> channels = new ArrayList<>();channels.add(selfChannel);channels.add(otherChannel);notifyChannels(channels, respModel);return;}
一对多
通过群id来获取群对象 之后遍历群的user表 根据用户id 来获取通道 分发
// 一对多群组消息List<Channel> channels = new ArrayList<>();// 通过群id来找到群对象 获取用户列表 根据列表uid 获取对应的通道GroupModel groupModel = LocalData.getGroupById(model.getGroupId());for (UserModel userModel : groupModel.getUsers()) {Channel channel = LocalData.channelUserUrl.get(userModel.getUid());channels.add(channel);}notifyChannels(channels, respModel);
可以改经的点
使用bridge作为一对一或者一对多的判断比较繁琐,可以通过状态码来判断:
type 200 代表一对多
type 100 代表私聊
WebSocket协议处理器(最后整合的部分)
// 泛型 代表的是处理数据的单位// TextWebSocketFrame 是文本信息帧@Component@ChannelHandler.Sharablepublic class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {@Autowiredprivate ChatService chatService;@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {System.out.println("msg : " + msg.text());// 获取请求数据 解析json形式ReqModel model = new Gson().fromJson(msg.text(), ReqModel.class);RespModel respModel = new RespModel();//设置用户信息respModel.setUid(model.getUid());respModel.setNickname(model.getNickname());//获取当前时间LocalDateTime now = LocalDateTime.now();String date = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));respModel.setDate(date);//bridge 初始值 要默认空值List<String> defaultList = new ArrayList<>();respModel.setBridge(defaultList);// 默认类型respModel.setType(RespType.OPERA.getNum());//判断请求类型ReqType type = ReqType.getTypeByNum(model.getType());switch (type) {case CONN:System.out.println(model.getNickname() + ": 用户上线了");//记录并返回在线用户列表 以及已经创建的群组列表//记录用户和通道的关联关系LocalData.channelUserUrl.put(model.getUid(), ctx.channel());chatService.addUser(model, respModel);break;case CANCEL:System.out.println(model.getNickname() + ": 用户下线了");LocalData.channelUserUrl.remove(LocalData.channelUserUrl.get(model.getUid()));chatService.delUser(model, respModel);break;case ADD_GROUP:System.out.println(model.getNickname() + "用户创建了群组" + model.getGroupName());chatService.addGroup(model, respModel);break;case JOIN_GROUP:System.out.println(model.getNickname() + "用户加入了群组" + model.getGroupName());chatService.joinGroup(model, respModel);break;case SEND_MSG://识别响应类型 消息类型是更改respModel.setType(RespType.MSG.getNum());//判断一对一消息还是一对多消息if (model.getBridge().size() == 0) {// 一对多chatService.sendGroupMsg(model, respModel);} else {chatService.sendPrivateMsg(model, respModel);}break;default:}System.out.println(new Gson().toJson(respModel));if (respModel.getType() == RespType.OPERA.getNum()) {List<Channel> channels = LocalData.getAllChannels();notifyChannels(channels, respModel);return;}// 根据一对一 或者一对多的类型来找到接受通知的用户if (model.getBridge().size() > 0) {// 代表一对一,只需要通知自身和需要接受消息的用户String selfId = model.getBridge().get(0);Channel selfChannel = LocalData.channelUserUrl.get(selfId);//接受信息的通道String otherId = model.getBridge().get(1);Channel otherChannel = LocalData.channelUserUrl.get(otherId);List<Channel> channels = new ArrayList<>();channels.add(selfChannel);channels.add(otherChannel);notifyChannels(channels, respModel);return;}// 一对多群组消息List<Channel> channels = new ArrayList<>();// 通过群id来找到群对象 获取用户列表 根据列表uid 获取对应的通道GroupModel groupModel = LocalData.getGroupById(model.getGroupId());for (UserModel userModel : groupModel.getUsers()) {Channel channel = LocalData.channelUserUrl.get(userModel.getUid());channels.add(channel);}notifyChannels(channels, respModel);}private void notifyChannels(List<Channel> channels, RespModel respModel) {for (Channel channel : channels) {TextWebSocketFrame resp = new TextWebSocketFrame(new Gson().toJson(respModel));channel.writeAndFlush(resp);}}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {//下线LocalData.channellist.remove(ctx.channel());}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {//上线LocalData.channellist.add(ctx.channel());}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {}}