Netty+SpringBoot+FastDFS+Html5实现聊天App,项目介绍。
Netty+SpringBoot+FastDFS+Html5实现聊天App, 项目github链接 。
本章完整代码链接 。
本章主要讲的是聊天App_PigChat中关于聊天功能的实现。
在ChatHandler中重写其移除channel的方法handlerRemoved,以及处理异常的方法exceptionCaught。
@Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { String channelId = ctx.channel().id().asShortText(); System.out.println("客户端被移除,channelId为:" + channelId); // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel users.remove(ctx.channel()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除 ctx.channel().close(); users.remove(ctx.channel()); }
public class ChatMsg implements Serializable { private static final long serialVersionUID = 3611169682695799175L; private String senderId; // 发送者的用户id private String receiverId; // 接受者的用户id private String msg; // 聊天内容 private String msgId; // 用于消息的签收 public String getSenderId() { return senderId; } public void setSenderId(String senderId) { this.senderId = senderId; } public String getReceiverId() { return receiverId; } public void setReceiverId(String receiverId) { this.receiverId = receiverId; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getMsgId() { return msgId; } public void setMsgId(String msgId) { this.msgId = msgId; } }
对实体类再做一层包装
public class DataContent implements Serializable { private static final long serialVersionUID = 8021381444738260454L; private Integer action; // 动作类型 private ChatMsg chatMsg; // 用户的聊天内容entity private String extand; // 扩展字段 public Integer getAction() { return action; } public void setAction(Integer action) { this.action = action; } public ChatMsg getChatMsg() { return chatMsg; } public void setChatMsg(ChatMsg chatMsg) { this.chatMsg = chatMsg; } public String getExtand() { return extand; } public void setExtand(String extand) { this.extand = extand; } }
public enum MsgActionEnum { CONNECT(1, "第一次(或重连)初始化连接"), CHAT(2, "聊天消息"), SIGNED(3, "消息签收"), KEEPALIVE(4, "客户端保持心跳"), PULL_FRIEND(5, "拉取好友"); public final Integer type; public final String content; MsgActionEnum(Integer type, String content){ this.type = type; this.content = content; } public Integer getType() { return type; } }
/** * @Description: 用户id和channel的关联关系处理 */ public class UserChannelRel { private static HashMap<String, Channel> manager = new HashMap<>(); public static void put(String senderId, Channel channel) { manager.put(senderId, channel); } public static Channel get(String senderId) { return manager.get(senderId); } public static void output() { for (HashMap.Entry<String, Channel> entry : manager.entrySet()) { System.out.println("UserId: " + entry.getKey() + ", ChannelId: " + entry.getValue().id().asLongText()); } } }
重写ChatHandler读取消息的channelRead0方法。
具体步骤如下:
(1)获取客户端发来的消息;
(2)判断消息类型,根据不同的类型来处理不同的业务;
(2.1)当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来;
(2.2)聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收];
然后实现消息的发送,首先从全局用户Channel关系中获取接受方的channel,然后当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在,若用户在线,则使用writeAndFlush方法向其发送消息;
(2.3)签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收];
(2.4)心跳类型的消息
// 用于记录和管理所有客户端的channle public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("read.........."); // 获取客户端传输过来的消息 String content = msg.text(); Channel currentChannel = ctx.channel(); // 1. 获取客户端发来的消息 DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class); Integer action = dataContent.getAction(); // 2. 判断消息类型,根据不同的类型来处理不同的业务 if (action == MsgActionEnum.CONNECT.type) { // 2.1 当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来 String senderId = dataContent.getChatMsg().getSenderId(); UserChannelRel.put(senderId, currentChannel); // 测试 for (Channel c : users) { System.out.println(c.id().asLongText()); } UserChannelRel.output(); } else if (action == MsgActionEnum.CHAT.type) { // 2.2 聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收] ChatMsg chatMsg = dataContent.getChatMsg(); String msgText = chatMsg.getMsg(); String receiverId = chatMsg.getReceiverId(); String senderId = chatMsg.getSenderId(); // 保存消息到数据库,并且标记为 未签收 UserService userService = (UserService)SpringUtil.getBean("userServiceImpl"); String msgId = userService.saveMsg(chatMsg); chatMsg.setMsgId(msgId); DataContent dataContentMsg = new DataContent(); dataContentMsg.setChatMsg(chatMsg); // 发送消息 // 从全局用户Channel关系中获取接受方的channel Channel receiverChannel = UserChannelRel.get(receiverId); if (receiverChannel == null) { // TODO channel为空代表用户离线,推送消息(JPush,个推,小米推送) } else { // 当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在 Channel findChannel = users.find(receiverChannel.id()); if (findChannel != null) { // 用户在线 receiverChannel.writeAndFlush( new TextWebSocketFrame( JsonUtils.objectToJson(dataContentMsg))); } else { // 用户离线 TODO 推送消息 } } } else if (action == MsgActionEnum.SIGNED.type) { // 2.3 签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收] UserService userService = (UserService)SpringUtil.getBean("userServiceImpl"); // 扩展字段在signed类型的消息中,代表需要去签收的消息id,逗号间隔 String msgIdsStr = dataContent.getExtand(); String msgIds[] = msgIdsStr.split(","); List<String> msgIdList = new ArrayList<>(); for (String mid : msgIds) { if (StringUtils.isNotBlank(mid)) { msgIdList.add(mid); } } System.out.println(msgIdList.toString()); if (msgIdList != null && !msgIdList.isEmpty() && msgIdList.size() > 0) { // 批量签收 userService.updateMsgSigned(msgIdList); } } else if (action == MsgActionEnum.KEEPALIVE.type) { // 2.4 心跳类型的消息 System.out.println("收到来自channel为[" + currentChannel + "]的心跳包..."); } }
在controller中添加获取未签收的消息列表的接口getUnReadMsgList。
/** * * @Description: 用户手机端获取未签收的消息列表 */ @PostMapping("/getUnReadMsgList") public IMoocJSONResult getUnReadMsgList(String acceptUserId) { // 0. userId 判断不能为空 if (StringUtils.isBlank(acceptUserId)) { return IMoocJSONResult.errorMsg(""); } // 查询列表 List<com.imooc.pojo.ChatMsg> unreadMsgList = userService.getUnReadMsgList(acceptUserId); return IMoocJSONResult.ok(unreadMsgList); }