zoukankan      html  css  js  c++  java
  • Netty整合WebSocket的使用

    初学Netty,记录下学习的过程,各位请多多关照哦!

     Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点就是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性

    netty主要分为如下几大部分

    • 构建Netty 服务端

    • 构建Netty 客户端

    • 利用protobuf定义消息格式

    • 服务端空闲检测

    • 客户端发送心跳包与断线重连

    写代码之前呢我们需要先引进依赖

    <dependency>
                <groupId>io.netty</groupId>
                <artifactId>netty-all</artifactId>
                <version>4.1.42.Final</version>
            </dependency>

    如有需要请前往gitee下载

    1.构建netty服务

    package com.serene.im.config;

    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    /**
    * netty 服务
    *
    * @author serene
    * @date 2021/3/18 15:55
    */
    @Component
    public class NettyServer {

    private static final Logger log = LoggerFactory.getLogger(NettyServer.class);

    /**
    * netty端口
    */
    private static int port = 8080;

    private static class SingletionWSServer {
    static final NettyServer instance = new NettyServer();
    }

    public static NettyServer getInstance() {
    return SingletionWSServer.instance;
    }

    private EventLoopGroup mainGroup;
    private EventLoopGroup subGroup;
    private ServerBootstrap server;
    private ChannelFuture future;


    /**
    * 处理客户端请求
    */
    public NettyServer() {
    System.out.println("2.进入NettyServer");
    mainGroup = new NioEventLoopGroup();
    subGroup = new NioEventLoopGroup();
    server = new ServerBootstrap();
    server.group(mainGroup, subGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new NettyChannelInitializer());
    }

    /**
    * 启动netty
    */
    public void start() {
    this.future = server.bind(port);
    log.info("netty server server 启动完毕... port = " + port);
    System.out.println("future-> " + future.isSuccess());
    System.out.println("3.启动netty服务");
    }

    }

    2.管道初始化(netty初始化器)

    package com.serene.im.config;
    
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelPipeline;
    import io.netty.channel.socket.SocketChannel;
    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 io.netty.handler.timeout.IdleStateHandler;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * 管道初始化
     */
    
    public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        private static final Logger log = LoggerFactory.getLogger(NettyChannelInitializer.class);
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            System.out.println("4.管道初始化");
            log.info(" 管道初始化...... ");
            ChannelPipeline pipeline = ch.pipeline();
            // websocket 基于http协议,所以要有http 编解码器
            pipeline.addLast("HttpServerCodec", new HttpServerCodec());
    
            // 对写大数据流的支持
            pipeline.addLast(new ChunkedWriteHandler());
    
            // 对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse
            // 几乎在netty中的编程,都会使用到此hanler
            pipeline.addLast(new HttpObjectAggregator(1024 * 64));
    
            // 增加心跳支持 start
            // 针对客户端,如果在1分钟时没有向服务端发送读写心跳(ALL),则主动断开
            // 如果是读空闲或者写空闲,不处理
            pipeline.addLast(new IdleStateHandler(8, 10, 12));
    
            // 自定义的空闲状态检测 处理消息的handler
            //pipeline.addLast(new NettyHeartBeatHandler());
    
            /**
             * websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
             * 本handler会帮你处理一些繁重的复杂的事
             * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
             * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
             */
            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
            System.out.println("5.进入websocket");
    
            // 自定义的wshandler 处理消息的handler
            pipeline.addLast(new NettyWsChannelInboundHandler());
            System.out.println("7.进入处理消息的handler类");
    
            // 自定义 http
            pipeline.addLast(new NettyHttpChannelInboundHandler());
            System.out.println("自定义http 的Handler");
        }
    
    }


    3.然后你就根据自己的业务逻辑写自己需要的handler,下面是处理聊天消息的handler

    package com.serene.im.config;
    
    
    import com.serene.im.SpringBeanUtil;
    import com.serene.im.entity.ChatMsg;
    import com.serene.im.entity.DataContent;
    import com.serene.im.enums.MsgActionEnum;
    import com.serene.im.pojo.im.ImChatMsgLogs;
    import com.serene.im.service.im.ImChatMsgLogsService;
    import com.serene.im.utils.JsonUtils;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.channel.group.DefaultChannelGroup;
    import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
    import io.netty.util.concurrent.GlobalEventExecutor;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    /**
     * 处理消息的handler
     * TextWebSocketFrame: 在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
     * SimpleChannelInboundHandler:    对于请求来讲 ,相当于 【入站,入境】
     *
     * @author serene
     * @date 2021/3/18 15:55
     */
    
    public class NettyWsChannelInboundHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    
        private static final Logger log = LoggerFactory.getLogger(NettyWsChannelInboundHandler.class);
    
        /**
         * 用于记录和管理所有客户端的channle
         */
        public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    
    
        /**
         * 从channel缓冲区读数据
         *
         * @param ctx
         * @param msg
         * @throws Exception
         */
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            System.out.println("8.从channel缓冲区读数据");
            // 获得 channel
            Channel currentChannel = ctx.channel();
            //获取客户端所传输的消息
            String content = msg.text();
            log.info("接收消息: {} ", content);
            dealWithToChatWith(currentChannel, content);
        }
    
        /**
         * 处理聊天数据
         */
        protected void dealWithToChatWith(Channel currentChannel, String content) throws Exception {
            System.out.println("9.进入获取客户端发来的消息");
            // 1. 获取客户端发来的消息
            DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
            Integer action = dataContent.getAction();
    
            /**
             * 2. 判断消息类型,根据不同的类型来处理不同的业务
             */
            //type=1:第一次(或重连)初始化连接
            if (action == MsgActionEnum.CONNECT.type) {
                System.out.println("进入初始化连接");
                // 2.1  当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来
                String sendId = dataContent.getChatMsg().getSendId();
                UserChannelRel.put(sendId, currentChannel);
                UserChannelRel.output();
    
                //type=2:聊天消息
            } else if (action == MsgActionEnum.CHAT.type) {
                System.out.println("进入好友聊天消息  type=" + MsgActionEnum.CHAT.type);
                //  2.2  聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]
                ImChatMsgLogsService imChatMsgLogsService = (ImChatMsgLogsService) SpringBeanUtil.getBean("imChatMsgLogsServiceImpl");
    
                ChatMsg chatMsg = dataContent.getChatMsg();
                // 保存消息到数据库,并且标记为 未签收
                ImChatMsgLogs logs = new ImChatMsgLogs();
                logs.setMainUserId(chatMsg.getMainUserId());
                logs.setUserId(chatMsg.getUserId());
                logs.setSendId(chatMsg.getSendId());
                logs.setReceiveId(chatMsg.getReceiveId());
                logs.setMsgContent(chatMsg.getMsgContent());
                logs.setToType(1);
                Integer msgId = imChatMsgLogsService.saveWebMsgLogs(logs);
                chatMsg.setMsgId(msgId.toString());
                // 消息发送时间
                chatMsg.setSendTime(new Date());
                DataContent dataContentMsg = new DataContent();
                dataContentMsg.setChatMsg(chatMsg);
                // 给自己发送成功消息
                Channel sendIdChannel = UserChannelRel.get(chatMsg.getSendId());
                sendIdChannel.writeAndFlush(new TextWebSocketFrame(JsonUtils.objectToJson(dataContent)));
    
                // 发送消息 从全局用户Channel关系中获取接受方的channel
                Channel receiverChannel = UserChannelRel.get(chatMsg.getReceiveId());
    
                if (receiverChannel == null) {
                    // TODO channel为空代表用户离线,推送消息(JPush,个推,小米推送)   添加离线消息记录
                    log.info(" 用户离线1 ... receiverChannel 是  null");
                    imChatMsgLogsService.updateOfflineStatusTwo(msgId);
                } else {
                    // 当receiverChannel不为空的时候,从ChannelGroup去查找对应的channel是否存在
                    Channel findChannel = users.find(receiverChannel.id());
                    if (findChannel != null) {
                        // 用户在线
                        receiverChannel.writeAndFlush(new TextWebSocketFrame(JsonUtils.objectToJson(dataContent)));
                    } else {
                        // 用户离线 TODO 推送消息     添加离线消息记录
                        log.info(" 用户离线2 ... findChannel 是  null");
                        imChatMsgLogsService.updateOfflineStatusTwo(msgId);
                    }
                }
                //type=3:消息签收
            } else if (action == MsgActionEnum.SIGNED.type) {
                System.out.println("进入消息签收");
                log.info(" 消息通知.....  ");
    
                // 扩展字段在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);
                    }
                }
                if (msgIdList != null && !msgIdList.isEmpty() && msgIdList.size() > 0) {
                    //  2.3  签收消息类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收]
                    // 批量签收
                    ImChatMsgLogsService imChatMsgLogsService = (ImChatMsgLogsService) SpringBeanUtil.getBean("imChatMsgLogsServiceImpl");
                    imChatMsgLogsService.updateMsgReadStatusOne(msgIdList);
                }
    
                //type=4:客户端保持心跳
            } else if (action == MsgActionEnum.KEEPALIVE.type) {
                System.out.println("客户端保持心跳");
    
                //  2.4  心跳类型的消息
                log.info("收到来自channel为[" + currentChannel + "]的心跳包...");
    
                //type=6:好友申请
            } else if (action == MsgActionEnum.FRIEND_REQUEST.type) {
                // 好友申请
                ChatMsg chatMsg = dataContent.getChatMsg();
                String sendId = chatMsg.getSendId();
                String receiveId = chatMsg.getReceiveId();
                log.info("sendId = " + sendId + ".... 好友申请.... receiveId =" + receiveId);
    
                //type=7:群消息
            } else if (action == MsgActionEnum.GROUP_MSG.type) {
                System.out.println("进入群消息");
                //群消息发送
                ImChatMsgLogsService imChatMsgLogsService = (ImChatMsgLogsService) SpringBeanUtil.getBean("imChatMsgLogsServiceImpl");
    
                ChatMsg chatMsg = dataContent.getChatMsg();
                // 保存消息到数据库,并且标记为 未签收
                ImChatMsgLogs logs = new ImChatMsgLogs();
                logs.setMainUserId(chatMsg.getMainUserId());
                logs.setUserId(chatMsg.getUserId());
                logs.setSendId(chatMsg.getSendId());
                logs.setGroupInfoId(1);
                logs.setMsgContent(chatMsg.getMsgContent());
                logs.setToType(2);
                Integer msgId = imChatMsgLogsService.saveWebMsgLogs(logs);
                chatMsg.setMsgId(msgId.toString());
                //消息发送时间
                chatMsg.setSendTime(new Date());
    
                DataContent dataContentMsg = new DataContent();
                dataContentMsg.setChatMsg(chatMsg);
                // 给所有在线的 im用户 发送信息
                for (Channel c : users) {
                    System.out.println("给所有在线的 im用户 发送信息");
                    c.writeAndFlush(new TextWebSocketFrame(JsonUtils.objectToJson(dataContent)));
                }
                // 更新消息状态为已读
                log.info(" 群消息发送... users.size = " + users.size());
            }
        }
    
        /**
         * 当客户端连接服务端之后(打开连接)
         * 获取客户端的channle,并且放到ChannelGroup中去进行管理
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            users.add(ctx.channel());
            log.info(" netty 获得连接.....");
            System.out.println("6.netty获取连接");
        }
    
        /**
         * 移除
         *
         * @param ctx
         * @throws Exception
         */
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            String channelId = ctx.channel().id().asShortText();
            // 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel
            users.remove(ctx.channel());
            log.info("客户端被移除,channelId为:" + channelId);
        }
    
        /**
         * 连接发送异常
         *
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除
            ctx.channel().close();
            users.remove(ctx.channel());
            log.info(" netty 异常了...... ");
        }
    
    }
    用户id和channel的关联关系处理
    package com.serene.im.config;
    
    import io.netty.channel.Channel;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.HashMap;
    
    /**
     * 用户id和channel的关联关系处理
     *
     * @author serene
     * @date 2021/3/18 15:55
     */
    public class UserChannelRel {
    
        private static final Logger log = LoggerFactory.getLogger(UserChannelRel.class);
    
        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()) {
                log.info(" imChat获得连接:  UserId: " + entry.getKey() + ", ChannelId: " + entry.getValue().id().asLongText());
                System.out.println("-------imChat获得连接------");
            }
        }
    
    }

    4.用于检测channel的心跳handler

    package com.serene.im.config;
    
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import io.netty.channel.group.ChannelGroup;
    import io.netty.handler.timeout.IdleState;
    import io.netty.handler.timeout.IdleStateEvent;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * 用于检测channel的心跳handler
     * 继承ChannelInboundHandlerAdapter,从而不需要重写channelRead0方法
     *
     * @author serene
     * @date 2021/3/18 15:55
     */
    
    public class NettyHeartBeatHandler extends ChannelInboundHandlerAdapter {
    
        private static final Logger log = LoggerFactory.getLogger(NettyHeartBeatHandler.class);
    
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    
            // 判断evt是否是IdleStateEvent(用于触发用户事件,包含 读空闲/写空闲/读写空闲 )
            if (evt instanceof IdleStateEvent) {
                // 强制类型转换
                IdleStateEvent event = (IdleStateEvent) evt;
                if (event.state() == IdleState.READER_IDLE) {
                    log.info("进入读空闲...");
                } else if (event.state() == IdleState.WRITER_IDLE) {
                    log.info("进入写空闲...");
                } else if (event.state() == IdleState.ALL_IDLE) {
                    log.info("所有的空闲...");
                    ChannelGroup users = NettyWsChannelInboundHandler.users;
                    log.info("channel关闭前,users的数量为:" + users.size());
                    Channel channel = ctx.channel();
                    // 关闭无用的channel,以防资源浪费
                    channel.close();
                    log.info("channel关闭后,users的数量为:" + users.size());
                }
            }
    
        }
    
    }

    NettyBoot类

    package com.serene.im;
    
    import com.serene.im.config.NettyServer;
    import org.springframework.context.ApplicationListener;
    import org.springframework.context.event.ContextRefreshedEvent;
    import org.springframework.stereotype.Component;
    
    
    @Component
    public class NettyBooter implements ApplicationListener<ContextRefreshedEvent> {
    
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            System.out.println("1.进入NettyBooter");
            if (event.getApplicationContext().getParent() == null) {
                try {
                    NettyServer.getInstance().start();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
    }

    netty启动成功后进行登录就跟socket连接上了

    可以群聊和好友的个人聊天,实现实时监听。

    gitee项目地址:https://gitee.com/ckfeng/serene_im

  • 相关阅读:
    2021软件工程-个人阅读作业
    OO第四单元——基于UML的UML解析器总结&OO课程总结
    OO第三单元——基于JML的社交网络总结
    OO第二单元——电梯作业总结
    SQL拼接字符串
    SQL查询列表中每种类型的第一条
    JS获取当前时间,设置不可用以前的时间
    JavaScript中的函数使用
    .Net软件开发面试技巧
    .Net小白的第一篇博客
  • 原文地址:https://www.cnblogs.com/ckfeng/p/14579412.html
Copyright © 2011-2022 走看看