Netty分布式聊天室 - 私聊功能实现

设计思路

        首先理清思路:

                对于私聊,有两种情况:对方在线,对方不在线

                因为我们是分布式是netty服务,彼此之间不建立通信的情况下需要使用消息队列对多有netty进行通知,让他们去寻找接收方对应的channel并发送数据。

                如果有一个服务器寻到了,并且数据发送成功,这时候其他服务器不用再执行,那么我们就需要用到redis进行服务间通信,如果服务器发现数据不在redis中,则直接跳出操作

                如果没有服务器寻到,那么数据会一直缓存在redis中,我们在此用户登录时进行数据拉取即可

                

                如此则引出一个问题:当用户登录时,有人刚好发送了消息给此用户,那么缓存在redis的数据有可能被两次发送到客户端:

                        用户登录时拉取一次、消息队列通知拉取一次

                这时候我们就需要在这两次操作前加上分布式锁,并且前端验证时针对消息的id进行去重匹配即可

        说干就干,上代码!

ChatMessage和ChatMsg改造

        ChatMessage:


/**
 * @author 14501
 */
@EqualsAndHashCode(callSuper = true)
@Data
@ToString(callSuper = true)
public class ChatRequestMessage extends Message {
    private String content;
    private Long to;
    private Long from;

    public ChatRequestMessage() {
    }

    public ChatRequestMessage(Long from, Long to, String content) {
        this.from = from;
        this.to = to;
        this.content = content;
    }

    @Override
    public int getMessageType() {
        return ChatRequestMessage;
    }
}

        ChatMsg,新增一个构造函数: 

    public ChatMsg(ChatRequestMessage chatMsg) {
        this.receiveUser = chatMsg.getTo();
        this.sendUser = chatMsg.getFrom();
        this.text = chatMsg.getContent();
    }

ChatMessageHandler设计

        负责将消息存储到数据库,然后调用消息队列的消息发布即可:

/**
 * @author 14501
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {
    @Resource
    private RabbitMqPublisher publisher;
    @Resource
    private ChatMsgService chatMsgService;
    @Resource
    private Session session;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg) throws Exception {
        Long to = msg.getTo();
        msg.setFrom(session.getUserId(ctx.channel()));
        ChatMsg chatMsg = new ChatMsg(msg);
        chatMsgService.save(chatMsg);
        //发布消息
        publisher.sendSingleMsg(to,chatMsg);
        ctx.writeAndFlush(new TextWebSocketFrame(new ResponseResult<>(Code.SUCCESS,chatMsg).toString()));
        log.info("msg:{},已提交发送",chatMsg);
    }
}

消息队列单聊设计 

        RabbitMqPublisher:

/**
 * @author 14501
 */
@Component
public class RabbitMqPublisher {

    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private RedisCache redisCache;

    public void sendSingleMsg(Long userId, ChatMsg msg){
        JSONObject json = new JSONObject();
        json.put("userId", userId);
        rabbitTemplate.convertAndSend("msg.topic","single.1", JSON.toJSONString(json));

        json = new JSONObject();
        json.put("msg",msg);
        redisCache.setCacheObject(RedisKey.userMsgKey(userId), json);
    }

}

        这里采用了topic类型的exchange,可以保证每个订阅同类型queue的netty获取到消息

        RabbitMqConsumer:


/**
 * @author 14501
 */
@Slf4j
@Configuration
public class RabbitMqConsumer {

    @Resource
    private Session session;
    @Resource
    private RedisCache redisCache;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "msg.single.queue"),
            exchange = @Exchange(name = "msg.topic",type = ExchangeTypes.TOPIC),
            key = "single.#")
    )
    public void listenerSingleMsg(String msg) {
        log.info("接收到私聊消息");
        //TODO 分布式锁 -》 消息拉取
        JSONObject json = JSON.parseObject(msg);
        long userId = Long.parseLong(json.get("userId") + "");

        Object object = redisCache.getCacheObject(RedisKey.userMsgKey(userId));

        if(object == null){
            return;
        }

        Channel channel = session.getChannel(userId);

        if(channel != null){
            channel.writeAndFlush(new TextWebSocketFrame(new ResponseResult<>(Code.SUCCESS.code,object).toString()));
            redisCache.deleteObject(RedisKey.userMsgKey(userId));
        }
    }

}

        目前只是保存一条缓存数据,后续我们会改造为redis的list结构(适配任意类型的合法消息),这里不用过多在意

LoginMessageHandler拉取离线消息

        新增一个checkMsgCache:


/**
 * @author 14501
 */
@Slf4j
@Component
@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {

    @Resource
    private ChatUserService chatUserService;
    @Resource
    private Session session;
    @Resource
    private RedisCache redisCache;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
        Long userId = msg.getUserId();
        boolean login = chatUserService.canLogin(userId);

        LoginResponseMessage message;

        if (login) {
            String bind = session.bind(ctx.channel(), userId);
            log.info("登陆成功");
            message = new LoginResponseMessage(true, "登录成功");
            message.setToken(bind);
            //拉取离线消息
            checkMsgCache(ctx.channel(),userId);
        } else {
            log.warn("用户不合法或已登录");
            message = new LoginResponseMessage(false, "用户不合法或已登录");
        }

        ctx.writeAndFlush(new TextWebSocketFrame(new ResponseResult<>(Code.SUCCESS.code,message).toString()));
    }

    private void checkMsgCache(Channel channel,Long userId) {
        //TODO 分布式锁 -》 消息拉取
        JSONObject object = JSON.parseObject(JSON.toJSONString(redisCache.getCacheObject(RedisKey.userMsgKey(userId))));

        if(object == null){
            return;
        }

        channel.writeAndFlush(new TextWebSocketFrame(new ResponseResult<>(Code.SUCCESS.code,object).toString()));
        redisCache.deleteObject(RedisKey.userMsgKey(userId));
    }
}

        如此一来离线发送和在线发送都实现了

调试

        使用ApiFox保持双方登录,切记登录之后的所以消息都要加上登录成功时回调的token,如下:

        接着登录zs,userId为2,我们开始演示:

        在线发送:两人都连接到Netty集群了

        离线发送:接收方不在线

在线发送

        ccx发送给zs的服务器响应:

        zs接收到的数据:         这里其实引出一个问题,当前端发送数据时,是要等到服务器响应再渲染消息,还是直接渲染?

        如果直接渲染,实际上为了匹配到数据,我们还需要添加对聊天数据添加一个由前端来生成的唯一值(针对同一个用户即可,可以使用userId + 时间戳的形式),sql数据库中不需要保存,只用于ws服务端和客户端的交互即可,这里可以在message中新增一个tempId,然后在ChatMsg实体类中新增一个同类型tempId即可

        此项目仅为示例项目,没有采用这个方案,一切从简,思路也只是写出来供大家参考,有更好方案的同学可以在评论区留言或者私信交流一下

离线发送

        下线张三:

        此时zs已经下线,我们使用ccx这边继续给zs发送消息试试:

         可以看到消息队列已经检测到消息:

        但是redis的缓存没有被清空,说明消息没有发送出去:

         此时我们上线张三,接受到离线消息:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Netty可以通过自定义协议实现私聊功能,具体实现步骤如下: 1. 定义私聊消息的协议格式,例如:[私聊目标用户ID]消息内容。 2. 在Netty服务端的ChannelHandler中,根据协议格式解析出私聊目标用户ID和消息内容,并将消息发送给目标用户。 3. 在Netty客户端中,发送私聊消息时需要按照协议格式构造消息,并将消息发送给服务端。 4. 在Netty客户端中,接收到私聊消息时需要根据协议格式解析出发送者ID和消息内容,并将消息显示在客户端界面上。 下面是一个简单的示例代码,实现了基于Netty私聊功能: ```java // 服务端ChannelHandler public class ChatServerHandler extends SimpleChannelInboundHandler<String> { // 存储所有连接的客户端Channel private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 新客户端连接时,将其Channel加入ChannelGroup channels.add(ctx.channel()); } @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { // 解析私聊消息协议格式 if (msg.startsWith("[私聊")) { int endIndex = msg.indexOf("]"); if (endIndex != -1) { String targetUserId = msg.substring(4, endIndex); String content = msg.substring(endIndex + 1); // 遍历所有客户端Channel,找到目标用户并发送私聊消息 for (Channel channel : channels) { if (channel != ctx.channel()) { if (channel.attr(ChatConstants.USER_ID).get().equals(targetUserId)) { channel.writeAndFlush("[" + ctx.channel().attr(ChatConstants.USER_ID).get() + "私聊你]:" + content); break; } } } } } else { // 广播消息给所有客户端 for (Channel channel : channels) { if (channel != ctx.channel()) { channel.writeAndFlush("[" + ctx.channel().attr(ChatConstants.USER_ID).get() + "]:" + msg); } } } } } // 客户端发送私聊消息 private void sendPrivateMessage(String targetUserId, String content) { String message = "[私聊" + targetUserId + "]" + content; channel.writeAndFlush(message); } // 客户端接收私聊消息 @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { // 解析私聊消息协议格式 if (msg.startsWith("[私聊")) { int endIndex = msg.indexOf("]"); if (endIndex != -1) { String senderId = msg.substring(4, endIndex); String content = msg.substring(endIndex + 1); // 显示私聊消息 showMessage("[" + senderId + "私聊你]:" + content); } } else { // 显示广播消息 showMessage(msg); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱飞的男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值