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

 

posted @ 2021-03-25 21:28  安详的苦丁茶  阅读(558)  评论(0编辑  收藏  举报