使用netty完成websocket聊天

使用netty完成websocket聊天

1、Netty配置类

java 复制代码
import cn.hutool.extra.spring.SpringUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import org.redisson.api.RBlockingDeque;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@ConditionalOnProperty(prefix = NettyProperties.PREFIX, name = "enable", havingValue = "true")
@ConditionalOnClass({NettyBootstrap.class, NettyProperties.class})
@EnableConfigurationProperties(NettyProperties.class)
public class NettyConfig {
    private final NettyProperties nettyProperties;
    /**
     * 主从调度
     */
    private final EventLoopGroup masterGroup;
    private final EventLoopGroup slaveGroup;
    /**
     * server
     */
    private final ServerBootstrap serverBootstrap;
    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
    private final ApplicationEventPublisher eventPublisher;


    public NettyConfig(NettyProperties nettyProperties, ThreadPoolTaskExecutor threadPoolTaskExecutor, ApplicationEventPublisher eventPublisher) {
        this.masterGroup = new NioEventLoopGroup();
        this.slaveGroup = new NioEventLoopGroup();
        this.serverBootstrap = new ServerBootstrap();

        this.nettyProperties = nettyProperties;
        this.threadPoolTaskExecutor = threadPoolTaskExecutor;
        this.eventPublisher = eventPublisher;
    }

    @Bean
    @ConditionalOnMissingBean
    public NettyBootstrap nettyBootstrap() {
        return new NettyBootstrap(nettyProperties, masterGroup, slaveGroup, serverBootstrap, eventPublisher);
    }
}

2、Netty属性类

java 复制代码
package com.chat.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * xx模块 - NettyProperties
 *
 * @author 李子耀
 * @version 1.0.0
 * @className NettyProperties
 * @description NettyProperties
 * @date 2023-8-17 14:39
 */
@Data
@ConfigurationProperties(prefix = NettyProperties.PREFIX)
public class NettyProperties {
    public static final String PREFIX = "netty";
    /**
     * true 是否激活
     */
    private Boolean enable;
    /**
     * 10.10.10.147
     */
    private String ip;
    /**
     * 9002
     */
    private Integer port;
    /**
     * jdz-china-ws
     */
    private String serverName;

    /**
     * dev
     */
    private String namespace;
    /**
     * 192.168.1.208:8848
     */
    private String serverAddr;

    /**
     * 默认参数 请求路径
     */
    private String path = "/";
    /**
     * 默认参数 是否需要认证
     */
    private Boolean auth = false;
    /**
     * 默认参数认证 key
     */
    private String authKey = "token";
    /**
     * 是否启用 评价功能 默认不启动吧,这个是延时队列
     */
    private Boolean enableRating = false;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
}

3、Netty启动类

java 复制代码
package com.lzy.chat.config;

import com.alibaba.nacos.api.naming.NamingFactory;
import com.alibaba.nacos.api.naming.NamingService;
import com.lzy.chat.handler.AuthenticationHandler;
import com.lzy.chat.handler.MyMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;

import javax.annotation.PostConstruct;
import java.util.Properties;

/**
 * netty聊天模块 - NettyBootstrap
 *
 * @author 李子耀
 * @version 1.0.0
 * @className NettyBootstrap
 * @description NettyBootstrap
 * @date 2023-8-17 15:47
 */


@Slf4j
public class NettyBootstrap {
    private final NettyProperties nettyProperties;
    /**
     * 主从调度
     */
    private final EventLoopGroup masterGroup;
    private final EventLoopGroup slaveGroup;
    /**
     * server
     */
    private final ServerBootstrap serverBootstrap;

    private final ApplicationEventPublisher eventPublisher;

    public NettyBootstrap(NettyProperties nettyProperties, EventLoopGroup masterGroup, EventLoopGroup slaveGroup, ServerBootstrap serverBootstrap, ApplicationEventPublisher eventPublisher) {
        this.nettyProperties = nettyProperties;
        this.masterGroup = masterGroup;
        this.slaveGroup = slaveGroup;
        this.serverBootstrap = serverBootstrap;
        this.eventPublisher = eventPublisher;
    }

    @PostConstruct
    public void start() {
        // 注册到 Nacos 服务中
        registerNamingService();
        log.info("netty 注册nacos 成功");

        // 启动 Netty 服务器
        startServer();
        log.info("netty 聊天服务9002 加载成功");
        // 等待服务器关闭
        // 这里是等待服务器关闭的代码,可以根据您的实际情况进行编写
        // ...
    }

    /**
     * 注册到 Nacos 服务中
     */
    private void registerNamingService() {
        try {

            Properties properties = new Properties();
            properties.put("namespace", nettyProperties.getNamespace());
            properties.put("serverAddr", nettyProperties.getServerAddr());
            properties.put("username", nettyProperties.getUsername());
            properties.put("password", nettyProperties.getPassword());
            NamingService namingService = NamingFactory.createNamingService(properties);
            namingService.registerInstance(
                    nettyProperties.getServerName(),
                    nettyProperties.getIp(),
                    nettyProperties.getPort());
            log.info("当前netty注册Ip{},port{}", nettyProperties.getIp(), nettyProperties.getPort());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 启动服务
     */
    public void startServer() {
        serverBootstrap.group(masterGroup, slaveGroup).channel(NioServerSocketChannel.class)
                .childHandler(getChannelInitializer())
                .bind(nettyProperties.getPort());

        log.info("netty服务启动完成:{}", nettyProperties.getPort());
    }


    /**
     * 得到通道初始化
     *
     * @return {@link ChannelInitializer}<{@link SocketChannel}>
     */
    private ChannelInitializer<SocketChannel> getChannelInitializer() {
        return new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) {
                ChannelPipeline channelPipeline = ch.pipeline();
                // 添加HTTP编解码器
                channelPipeline.addLast(new HttpServerCodec());
                channelPipeline.addLast(new ChunkedWriteHandler());
                // 添加HTTP对象聚合器,将HTTP消息的多个部分合并成一个完整的HTTP消息
                channelPipeline.addLast(new HttpObjectAggregator(1024 * 64));
                //增加授权配置,设置频道userId并转换路由
                channelPipeline.addLast(new AuthenticationHandler(nettyProperties));
                //增加路由配置。
                channelPipeline.addLast(new WebSocketServerProtocolHandler(nettyProperties.getPath()));
                //业务处理器
                channelPipeline.addLast(new MyMessageHandler(eventPublisher));
            }
        };
    }
}

4、消息处理

java 复制代码
package com.chat.handler;

import com.alibaba.fastjson.JSON;
import com.chat.constant.ChatType;
import com.chat.event.ChatRatingEvent;
import com.chat.pojo.dto.ChannelDTO;
import com.chat.socket.*;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;

import java.util.Map;

/**
 * xx模块 - MessageHandler
 *
 * @author 李子耀
 * @version 1.0.0
 * @className MessageHandler
 * @description MessageHandler
 * @date 2023-8-14 17:15
 */

@Slf4j
public class MyMessageHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    private final ApplicationEventPublisher applicationEventPublisher;

    public MyMessageHandler(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * 消息处理程序
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
        if (frame instanceof TextWebSocketFrame) {
            // 处理文本帧
            TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
            String message = textFrame.text();
            // 处理接收到的文本消息
            processTextMessage(ctx, message);
        } else {
            // 处理其他类型的帧,如二进制帧等
            // ...
            log.info("处理其他类型的帧,如二进制帧等");
        }
    }

    /**
     * 处理文本消息
     *
     * @param ctx     ctx
     * @param message 消息
     */
    private void processTextMessage(ChannelHandlerContext ctx, String message) {
        DefaultMessage msg = JSON.parseObject(message, DefaultMessage.class);
        String channelId = ctx.channel().id().asLongText();
        msg.setChannelId(channelId);
        log.info("当前的chanelID:{}", channelId);
        msg.setSenderId(ctx.channel().attr(Attributes.USER_ID).get());
        String chatType = msg.getChatType();
        if (ChatType.GROUP.equals(chatType)) {
            this.groupChat(msg);
        } else if (ChatType.PRIVATE.equals(chatType)) {
            String messageType = msg.getMessageType();
            if ("10".equals(messageType)) {
                ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(msg)));
            } else {
                //有真实聊天记录
                MessageUtil.sendMsgAndInsertDB(msg);
            }
        }
        Map<String, MessageExtHandler> map = SpringContextHolder.getBeansOfType(MessageExtHandler.class);
        map.forEach((k, v) -> v.handle(ctx, msg));
        ctx.fireChannelRead(msg);
    }

    private void groupChat(Message msg) {
        log.info("群聊消息msg:{}", msg);
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        ChannelGroupManager.add(ctx.channel());
        String channelId = ctx.channel().id().asLongText();
        log.info("客户端已连接,ID是:" + channelId);
        //做一个频道id 鉴权。
        //京东,每次都是一个频道都是 新 的 聊天。
        //新建一个对话记录。 这个 如果有客户聊了天,就是可以评价的.增加评价
        //发布事件,放入延时队列 就一个channelId
        applicationEventPublisher.publishEvent(new ChatRatingEvent(channelId));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ChannelGroupManager.remove(ctx.channel());
        ctx.channel().close();
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        ChannelGroupManager.remove(ctx.channel());
        String channelId = ctx.channel().id().asLongText();
        log.info("客户端已断开连接,ID是:" + channelId);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
//            log.info("握手完成,调用回调方法,可以发送消息");
            String channelId = ctx.channel().id().asLongText();
            ctx.channel().writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(new ChannelDTO(channelId))));
        } else {
//            log.info("没有握手完成,继续触发用户事件");
            super.userEventTriggered(ctx, evt);
        }
    }
}

5、消息处理类

java 复制代码
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.DefaultEventExecutor;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;


public class ChannelGroupManager {
    private static ChannelGroup group = new DefaultChannelGroup(new DefaultEventExecutor());

    /**
     * 添加
     *
     * @param channel
     * @return
     */
    public static boolean add(Channel channel) {
        if (null == channel) {
            return false;
        }
        return group.add(channel);
    }

    /**
     * 移除
     *
     * @param channel
     * @return
     */
    public static boolean remove(Channel channel) {
        if (null == channel) {
            return false;
        }
        return group.remove(channel);
    }

    /**
     * 根据用户Id 获取channel
     * @param userId
     * @return
     */
    public static List<Channel> channel(String userId) {
        if (StringUtils.isEmpty(userId)) {
            return null;
        }
        List<Channel> reference = new ArrayList<>();
        group.forEach(channel -> {
          String id =  channel.attr(Attributes.USER_ID).get();
          if(userId.equals(id)){
              reference.add(channel);
              return;
          }
        });
        return reference;
    }
}

6、相关的基础类

java 复制代码
public interface Message {
    /**
     * 聊天类型 群里:1 私聊:0
     * @return
     */
    String getChatType();

    /**
     * 消息类型 心跳消息:10 文件消息:20 emoji表情:30 HTML文本:40
     * @return
     */
    String getMessageType();

    /**
     * 消息Id,后端自动填充
     * @return
     */
    String getId();

    /**
     * 消息内容,具体格式前端定义就行
     * @return
     */
    String getContent();

    /**
     * 接受者 如果是群聊则是房间id,需要根据房间号找到房间里的人 转换一下
     * @return
     */
    String getReceiverId();

    /**
     * 签收状态 已签收:1 0未签收:0
     * @return
     */
    String getSignState();
}



import io.netty.channel.ChannelHandlerContext;
// 消息扩展处理接口
public interface MessageExtHandler {
    void handle(ChannelHandlerContext ctx, Message message);
}


import lombok.Data;
@Data
public abstract class AbstractMessage implements Message {

    /**
     * @ignore
     */
    private String id;

    /**
     *
     * 消息内容 具体格式前端定义就好
     */
    private String content;

    /**
     * 聊天类型   群里 1 私聊 0
     */
    private String chatType;

    /**
     * 消息类型 心跳消息 10 文件消息 20 emoji 表情 30 HTML文本 40
     */
    private String messageType;

    /**
     * 消息发送者,发送方可以不设值,后端自动设值
     */
    private String senderId;

    /**
     * 消息接受者,如果是群聊则是房间号,需要根据房间号找到房间里的人 转换一下
     * @required
     */
    private String receiverId;

    /**
     * 签收状态 1:签收  0:签收
     */
    private String signState;
}

//消息类型
public abstract class AbstractMessageType {

    /**
     * 心跳消息
     */
    public static final String HEART = "10";

    /**
     * 文件消息
     */
    public static final String FILE = "20";

    /**
     * emoji表情消息
     */
    public static final String EMOJI = "30";

    /**
     * html 文本内容消息
     */
    public static final String HTML = "40";
}



import io.netty.util.AttributeKey;
public class Attributes {

 public static final AttributeKey<String> USER_ID = AttributeKey.newInstance("userId");

 public static final AttributeKey<String> CODE = AttributeKey.newInstance("code");
}

import lombok.Data;
@Data
public class DefaultMessage extends AbstractMessage {
    private String channelId;
}

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.chat.socket.AbstractMessage;
import lombok.Data;

import java.util.Date;

@Data
@TableName(chat_msg")
public class ChatMsgPO extends AbstractMessage {

    private String channelId;
    /**
     * 创建日期
     */
    @TableField(value = "create_date", fill = FieldFill.INSERT)
    protected Date createDate;


    /**
     * 修改操作日期
     */
    @TableField(value = "update_date", fill = FieldFill.UPDATE)
    protected Date updateDate;

    @TableLogic()
    @TableField(fill = FieldFill.INSERT)
    private String enabled;
}


public abstract class ChatType {
    /**
     * 群聊
     */
    public static final String GROUP = "1";

    /**
     * 私聊
     */
    public static final String PRIVATE = "0";
}

public abstract class SignState {
    /**
     * 签收
     */
    public static final String SIGNED = "Y";

    /**
     * 未签收
     */
    public static final String UNSIGNED = "N";
}

7、结果结果导向

java 复制代码
import com.alibaba.fastjson.JSON;
import com.chat.pojo.po.ChatMsgPO;
import com.chat.service.ChatMsgService;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.util.StringUtils;

import java.util.List;

public class MessageUtil {
    /**
     * 日志对象
     */
    protected static Logger logger = LoggerFactory.getLogger(MessageUtil.class);

    static ChatMsgService chatMsgService = SpringContextHolder.getBean(ChatMsgService.class);

    /**
     * 发送消息并且存库
     * @param msg
     * @return
     */
    public static boolean sendMsgAndInsertDB(Message msg) {
        checkInit();
        ChatMsgPO chatMsg = new ChatMsgPO();
        BeanUtils.copyProperties(msg,chatMsg);
        chatMsg.setSignState(SignState.UNSIGNED);
        chatMsgService.saveChatMsg(chatMsg);
        return pushMsg(msg);
    }

    /**
     * 检测
     */
    private static void checkInit() {
        if (chatMsgService == null) {
            chatMsgService = SpringContextHolder.getBean(ChatMsgService.class);
        }
    }

    /**
     * 发送聊天消息
     */
    public static boolean pushMsg(Message msg) {
        String receiverId = msg.getReceiverId();
        if (StringUtils.isEmpty(receiverId)) {
            logger.warn("接受者用户Id为空,信息丢弃:{}", JSON.toJSONString(msg));
            return false;
        }
        List<Channel> channels = ChannelGroupManager.channel(receiverId);
        if (channels.isEmpty()) {
            logger.warn("消息接受者不在线:{}", JSON.toJSONString(msg));
            return false;
        }
        channels.forEach(item -> item.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(msg))));
        return true;
    }
}
相关推荐
桀桀桀桀桀桀7 分钟前
数据库中的用户管理和权限管理
数据库·mysql
瓜牛_gn5 小时前
mysql特性
数据库·mysql
Yaml410 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
追风林10 小时前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
Hsu_kk12 小时前
MySQL 批量删除海量数据的几种方法
数据库·mysql
编程学无止境12 小时前
第02章 MySQL环境搭建
数据库·mysql
knight-n12 小时前
MYSQL库的操作
数据库·mysql
eternal__day14 小时前
MySQL_聚合函数&分组查询
数据库·mysql
咕哧普拉啦15 小时前
乐尚代驾十订单支付seata、rabbitmq异步消息、redisson延迟队列
java·spring boot·mysql·spring·maven·乐尚代驾·java最新项目
春哥的魔法书16 小时前
数据库基础(5) . DCL
数据库·mysql