springboot +netty 整合netty基本运用 实现好友之间的互相通讯和消息推送

springboot +netty 整合netty基本运用 实现好友之间的互相通讯和消息推送

不懂的都感觉netty是很高深的技术 ,但是如果只是拿来用,不深刻理解底层原理,还是很简单的。

正题开始

核心配置:

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

设置主从线程和通讯端口以及启动netty

下面用到了 @ComponentScan 这个注解 已经注释了 不懂的自行百度

mport 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.springframework.context.annotation.ComponentScan;


//设置自动加载
@ComponentScan
public class WsServer {
    private static class SingletionWsServer {
        static final WsServer instance = new WsServer();
    }

    public static WsServer getInstance() {
        return SingletionWsServer.instance;
    }

    private EventLoopGroup mainGroup;
    private EventLoopGroup workGroup;
    private ServerBootstrap serverBootstrap;
    private ChannelFuture channelFuture;

    //设置两个线程的主从关系
    
    public WsServer() {
    //设置两个线程组 一个处理连接用户 一个处理连接之后的操作
        mainGroup = new NioEventLoopGroup();
        workGroup = new NioEventLoopGroup();
        serverBootstrap = new ServerBootstrap();
        //设置两个线程组
        serverBootstrap.group(mainGroup, workGroup)//设置主从线程
                .channel(NioServerSocketChannel.class)//设置NIO双向通讯
                .childHandler(new WsServerInitializer());//子处理器 用于处理workeGroup线程
    }

    //启动
    public void start() {
        this.channelFuture = serverBootstrap.bind(8088);
        System.err.println("netty websocket server 启动成功");
    }


}

编写子处理器

下面把心跳的机制也写到里面了
channelPipeline.addLast(new IdleStateHandler(4,8,12));第一个4是读空闲4秒,第二个是写空闲8秒
第三个是最重要的读写空闲设置了12秒(一般设置60秒,这个是为了方便测试)。设置这个参数是控制关闭无用的channel 。当读写空闲超过12秒。我们自动把这个通道(channel)删除.提供服务器性能和节约资源空间。

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;

public class WsServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {


        //通过SocketChannel 获取对应的管道
        ChannelPipeline channelPipeline= channel.pipeline();

        //webSocket 是基于HTTP的 所以要有http解码器
        channelPipeline.addLast(new HttpServerCodec());
        //对大数据流的支持
        channelPipeline.addLast(new ChunkedWriteHandler());
        //对HttpMessage进行聚合
        //几乎在netty编程中,都会使用到handler
        channelPipeline.addLast(new HttpObjectAggregator(1024 *64));

        /**
         * 增加心跳支持
         *针对客户端  读空闲或者写空闲不出来 只对读写空闲请求超过60秒 则主动断开
         */
        channelPipeline.addLast(new IdleStateHandler(4,8,12));
        //自定义空闲状态监测
        channelPipeline.addLast(new HeartBeatHandler());


        /**
         * webSocket处理的协议,用于指定用户访问的路由:/ws
         * 本handler会帮你处理一些繁琐的事
         * 会帮你处理握手动作
         */
        channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

        //添加自定义助手类
        channelPipeline.addLast(new ChatHandler());
    }
}

下面是自定义助手类的代码
基本的处理逻辑都在里面。

import com.im.immuxin.enums.MsgActionEnum;
import com.im.immuxin.server.UsersServer;
import com.im.immuxin.utils.JsonUtils;
import com.im.immuxin.utils.SpringUtil;
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 java.util.ArrayList;
import java.util.List;

public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    //用于记录和管理所有的channel
    public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    @Override
    protected void channelRead0(ChannelHandlerContext cxt, TextWebSocketFrame msg) throws Exception {
        //获取Channel
        Channel currentChannel =cxt.channel();

        //获取客户端传输过来的消息
        String conter= msg.text();
           //把传输过来的消息转换成我们定义的类型 使用jsonToPojo()方法
            DataContert dataContert = JsonUtils.jsonToPojo(conter,DataContert.class);
            //获取动作类型
            Integer action = dataContert.getAction();
            //判断消息类型,根据不同的消息类型来处理不同的业务
            if(action== MsgActionEnum.CONNECT.type){
                //当websocket 第一次open的时候,初始化channel,把用的channel和userid关联起来
                   //获取用户ID
                   String senderId = dataContert.getChatMsg().getSenderId();
                   //绑定关系
                   UserChannelRel.put(senderId,currentChannel);

                   //测试
                for(Channel c :users){
                    System.out.println(c.id().asLongText());
                }
                UserChannelRel.output();

            }else if(action==MsgActionEnum.CHAT.type){
                //聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]
                    //获取消息类
                    ChatMsg chatMsg =dataContert.getChatMsg();
                    //获取消息
                    String msgText =chatMsg.getMsg();
                    //接收者ID
                    String recaiverId =chatMsg.getRecaiverId();
                    //发送者ID
                    String senderId =chatMsg.getSenderId();

                    DataContert dataContertMsg =new DataContert();
                    dataContertMsg.setChatMsg(chatMsg);
                    //把消息保存到数据库 设置签收状态
                        //通过springUtil获取里面的bean
                      UsersServer usersServer=(UsersServer) SpringUtil.getBean("usersImpl");
                      String msgId =usersServer.saveMsg(chatMsg);
                      chatMsg.setMsgId(msgId);
                   //获取对应的Channel
                Channel receiverChannel=UserChannelRel.get(recaiverId);
                if (receiverChannel==null){
                    //用户离线

                }else {
                    //当receiverChannel(Channer)不为空时 去ChannelGroup(users)里面去找对应的channel是否存在
                    Channel findChannel =users.find(receiverChannel.id());
                    if(findChannel !=null ){
                        //用户在线
                        receiverChannel.writeAndFlush(
                                new TextWebSocketFrame(
                                        JsonUtils.objectToJson(dataContertMsg)));
                    }else {
                        //用户离线
                    }
                }

            }else if(action==MsgActionEnum.SIGNED.type){
                //消息签收类型,针对具体的消息进行签收,修改数据库中对应消息的签收状态[已签收]
                UsersServer usersServer=(UsersServer) SpringUtil.getBean("usersImpl");
                //获取扩展字段 扩展字段在signed类型消息中,代表需要签收的消息的id,逗号隔开
               String msgIdsStr = dataContert.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.size()>0){
                   usersServer.updateMsgSigned(msgIdList);
               }
            }else if(action==MsgActionEnum.KEEPALIVE.type){
                //心跳类型的消息
                System.out.println("收到来自channel为[" + currentChannel + "]的心跳包...");

            }else if(action==MsgActionEnum.PULL_FRIEND.type){
                //拉取好友的消息
            }

//        //发送消息
//        for(Channel channel :users){
//            channel.writeAndFlush(new TextWebSocketFrame("[服务器接收到消息]:"+new Date()+"消息为:"+conter));
//        }

       // users.writeAndFlush(new TextWebSocketFrame("[服务器接收到消息]:"+new Date()+"消息为:"+conter));
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
       // super.handlerAdded(ctx);
        users.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //super.handlerRemoved(ctx);
        users.remove(ctx.channel());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //super.exceptionCaught(ctx, cause);
        //连接异常打印错误
        cause.printStackTrace();
        //发生异常之后关闭连接(关闭channel),随后从ChannelGroup 中移除
        ctx.channel().close();
        users.remove(ctx.channel());

    }
}

上面的代码中 我自己定义了一些枚举。便于管理前端传送过来的消息。(注意:前端传送过来的消息体结构必须和我们后端定义的一致。便于我们解析。)
重点:
这个类继承了SimpleChannelInboundHandler这个netty的一个方法。
继承这个方法必须要实现channelRead0()这个方法。
通过 Channel currentChannel =cxt.channel(); 可以获取的第一次连接上的Channel。每次断开连接分配的Channel都是不一样的。netty就是根据每个Channel来发送消息的。那重点就来了。
怎么根据Channel给指定的朋友发送消息呢?
其实很简单。每次连接的时候。会自动分配一个Channel.我们只需要把我们的ID和这个Channel绑定。就可以实现给指定的人发送消息。Map<String,Channel> manager=new Map<>();
如下图:
在这里插入图片描述
通过一个Map即可绑定每个连接上netty的用户。这样我们只需通过指定的KEY即可找到当前在线的朋友。即可发送消息。
下面给出绑定Channel的代码

import io.netty.channel.Channel;

import java.util.HashMap;

/**
 * 用户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());
        }
    }
}

只有理解了这步,基本就可以实现给指定的朋友发送消息了。当然群发也是一样。
最后给出心跳的处理代码
其实这段代码也可以写在我们自定义的助手类里面。但是为了好看。我提取出来了。

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

public class HeartBeatHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        //super.userEventTriggered(ctx, evt);
        //判断 evt是否是 IdleStateEvent(用于触发用户事件 ,包含 读空闲/写空闲/读写空闲)
        if (evt instanceof IdleStateEvent){
                //对evt进行强转
            IdleStateEvent event =(IdleStateEvent) evt;
            if (event.state()== IdleState.READER_IDLE){
                System.out.println("今入读空闲请求·····");
            }else if(event.state()==IdleState.WRITER_IDLE){
                System.out.println("进入写空闲请求······");
            }else if (event.state()==IdleState.ALL_IDLE){
                System.out.println("进入读写空闲请求·····");
                System.out.println("关闭Channel数量前:"+ChatHandler.users.size());
                //读写空闲请求需要关闭无用的Channel
               Channel channel= ctx.channel();
               //关闭无用的channel 以防资源浪费
               channel.close();
                System.out.println("关闭Channel数量后:"+ChatHandler.users.size());
            }
        }
    }
}

用到的枚举
代码如下

import java.io.Serializable;

public class DataContert implements Serializable {


    private static final long serialVersionUID = -6726890501587734885L;

    private Integer action;     //动作类型
    private ChatMsg chatMsg;   //用于用户聊天的entiey
    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;
    }
}

ChatMsg:

import java.io.Serializable;

public class ChatMsg implements Serializable {

    private static final long serialVersionUID = -4641427694954507726L;
    private String senderId;        //发送者用户ID
    private String recaiverId;      //接受者用户ID
    private String msg;             //聊天内容
    private String msgId;           //用于消息的签收

    public String getSenderId() {
        return senderId;
    }

    public void setSenderId(String senderId) {
        this.senderId = senderId;
    }

    public String getRecaiverId() {
        return recaiverId;
    }

    public void setRecaiverId(String recaiverId) {
        this.recaiverId = recaiverId;
    }

    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;
    }
}

下面是我写的解析前端传输过来的数据。转换成我们要的类型。(只适用这代码里面的数据格式)
在这里插入图片描述
这个方法代码如下:

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.List;
import java.util.Map;

/**
 * 自定义转换类
 */

public class JsonUtils {

    //定义jackson对象
    private static final ObjectMapper MAPPER =new ObjectMapper();

    /**
     * 将对象转换成JSON字符串
     */
    public static String objectToJson(Object data){
        try {
            String string =MAPPER.writeValueAsString(data);
            return string;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json结果集转化为对象
     */
    public static <T> T jsonToPojo(String jsonDate,Class<T> beanType){
        try {
            T t= MAPPER.readValue(jsonDate,beanType);
            return t;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json数据装换成pojo对象list
     */

    public static <T>List<T> jsonToList(String jsonDate,Class<T> beanType){
        JavaType javaType=MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
        try {
            List<T> list =MAPPER.readValue(jsonDate,javaType);
            return list;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

}

以上就是基本代码了。

总结一下:
当我们定义好主从线程。设置好端口号后。最主要的就是那个我们写的自定义助手类(ChatHandler)
基本的处理逻辑都在里面。只有懂得了。每次连接成功后他都会生成一个唯一的Channel。我们需要通过这个来发送消息。只要我们把这个和ID绑定。就可以通过ID来找到这个Channel。找到了要发送的Channel。我们就可以通过
channel.writeAndFlush(
new TextWebSocketFrame(
JsonUtils.objectToJson(dataContertMsg)));
//dataContertMsg是要发送的消息。
来直接发送消息。这样就可以达到基本通信。 消息推送也是一样。
前端代码就不写了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值