Java Netty 服务端向客户端发送消息

说到netty通讯,回忆了下,还是18年的时候,学了了netty,至今就学习的时候写过一个项目。最近闲生,被要求做一个netty通讯的项目,顺手,总结一下,之前写的项目。

当时是写了一款访微信聊天的软件,所以用到了netty通讯,废话不过说,我们来直接上代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import org.springframework.stereotype.Component;
 
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 lombok.extern.slf4j.Slf4j;
 
@Slf4j 
@Component
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 subGroup;
    private ServerBootstrap server;
    private ChannelFuture future;
 
    public WSServer() {
        // 主线程组
        mainGroup = new NioEventLoopGroup();
 
        // 子线程组
        subGroup = new NioEventLoopGroup();
        // netty服务器的创建,ServerBootstrap是一个启动类
        server = new ServerBootstrap();
        server.group(mainGroup, subGroup)// 设置主从线程组
                .channel(NioServerSocketChannel.class)// 设置nio双向通道
                .childHandler(new WSServerInitializer());// 子处理器,用于处理subGroup
    }
 
    /**
     * 启动
     */
    public void start() {
        this.future = server.bind(9700);
        System.err.println("netty websocket server 启动完毕...");
        log.info("netty websocket server 启动完毕...");
    }
 
}

这个类用于创建netty的服务端链接,下面我们来配置启动的一些配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 lombok.extern.slf4j.Slf4j;
@Slf4j
public class WSServerInitializer extends ChannelInitializer<SocketChannel> {
 
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        System.out.println("初始化 SocketChannel");
        log.info("初始化 SocketChannel");
        ChannelPipeline pipeline = ch.pipeline();
 
        //websocket 基于http协议,所以要http编解码器
        pipeline.addLast(new HttpServerCodec());
         
        //对写大数据流的支持
        pipeline.addLast(new ChunkedWriteHandler());
         
        //对httpMessage进行聚合,聚合成FullHttpRequest或FullHttpResponse
        //几乎在netty中的编程,都会使用到此hanler
        pipeline.addLast(new HttpObjectAggregator(1024*64));
         
        //========================以上是用于支持http协议========================
         
         
        //========================增加心跳支持 start    ========================
         
        //针对客户端,如果在1分钟时没有想服务端发送写心跳(ALL),则主动断开
        //如果是读空闲或者写空闲,不处理
        pipeline.addLast(new IdleStateHandler(8, 10, 12));
         
        //自定义的空闲检测
        pipeline.addLast(new HeartBeatHandler());
        //========================增加心跳支持 end      ========================
         
         
        /**
         * websocket服务器处理的协议,用于指定给客户端连接访问的路由: /ws
         * 本Handler会帮助你处理一些繁重的复杂的事
         * 会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳
         * 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
         */
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
         
        //自定义hanler
        pipeline.addLast(new ChatHandler());
    }
 
}

自定义空闲检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
 
/**
 * 用于检测channel的心跳的handler 继承 ChannelInboundHandlerAdapter 从而不需要实现 channelRead0方法
 *
 * @author wb0024
 *
 */
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
 
    @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) {
                System.out.println("进入读空闲...");
            } else if (event.state() == IdleState.WRITER_IDLE) {
                System.out.println("进入写空闲...");
            } else if (event.state() == IdleState.ALL_IDLE) {
                System.out.println("channel关闭前users数量为:"+ChatHandler.users.size());
                System.out.println("进入读写空闲...");
                Channel channel = ctx.channel();
                //关闭无用的channel,以防资源浪费
                channel.close();
                System.out.println("channel关闭后users数量为:"+ChatHandler.users.size());
            }
        }
    }
 
}

自定义hanler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import com.imooc.enums.MsgActionEnum;
import com.imooc.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;
 
/**
 * 处理消息的handler
 *
 * @author wb0024 TextWebSocketFrame:在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
 */
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
 
    // 用于记录和管理所有客户端的channel
    public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
 
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
 
        // 获取客户端传输过来的消息
        String content = msg.text();
 
        System.out.println("处理消息的handler:" + content);
        Channel currentChannel = ctx.channel();
 
        // 1. 获取客户端发送的消息
        DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
 
        Integer action = dataContent.getAction();
        System.out.println("action:" + action);
        // 2. 判断消息类型,根据不同的类型来处理不同的业务
        if (action == MsgActionEnum.CONNECT.type) {
            // 2.1 当websocket 第一次open的时候,初始化channel,把用户的channel和userId关联起来
            String senderId = dataContent.getChatMsg().getSenderId();
            UserChannelRel.put(senderId, currentChannel);
 
            // 测试
            for (Channel channel : users) {
                System.out.println(channel.id().asLongText());
            }
            UserChannelRel.output();
 
        } else if (action == MsgActionEnum.CHAT.type) {
            // 2.2 聊天类型的消息,把聊天记录保存到数据库,同时标记消息的签收状态[未签收]
            ChatMsg chatMsg = dataContent.getChatMsg();
            String receiverId = chatMsg.getReceiverId();
 
            DataContent dataContentMsg = new DataContent();
            dataContentMsg.setChatMsg(chatMsg);
 
            // 发送消息
            // 从全局用户channel关系中获取接受方的channel
            Channel receiverChannel = UserChannelRel.get(receiverId);
            if (receiverChannel != null) {
                // 当receiverChannel不为空的时候,从 ChannelGroup 去查找对应的channel是否存在
                Channel findChannel = users.find(receiverChannel.id());
                if (findChannel != null) {
                    // 用户在线
                    receiverChannel.writeAndFlush(new TextWebSocketFrame(JsonUtils.objectToJson(dataContentMsg)));
                }
            }
        } else if (action == MsgActionEnum.KEEPALIVE.type) {
            // 2.4 心跳类型的消息
            System.out.println("收到来自channel为[" + currentChannel + "]的心跳包");
        }
 
    }
 
    /**
     * 当客户连接服务端之后(打开链接) 获取客户端的channel,并且放到ChannelGroup中去进行管理
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        users.add(ctx.channel());
    }
 
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
 
        String channelId = ctx.channel().id().asLongText();
        System.out.println("客户端被移除,channelId为:" + channelId);
 
        // 当触发handlerRemoved,ChannelGroup会自动移除对应的客户端channel
        users.remove(ctx.channel());
    }
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        // 发生异常之后关键channel。随后从ChannelGroup 中移除
        ctx.channel().close();
        users.remove(ctx.channel());
    }
 
}

  

定义channel管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.HashMap;
 
import io.netty.channel.Channel;
 
/**
 * 用户id和channel的关联关系处理
 *
 * @author wb0024
 *
 */
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());
        }
    }
}

其他类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.Serializable;
 
import lombok.Data;
 
@Data
public class ChatMsg implements Serializable {
 
    /**
     *
     */
    private static final long serialVersionUID = 1L;
 
    private String senderId; // 发送者的用户id
    private String receiverId; // 接受者的用户id
    private String msg; // 聊天内容
    private String msgId; // 用于消息的签收
 
}

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.Serializable;
 
import lombok.Data;
 
@Data
public class DataContent implements Serializable {
 
    /**
     *
     */
    private static final long serialVersionUID = 1L;
 
    private Integer action; // 动作类型
    private ChatMsg chatMsg; // 用户的聊天内容对象
    private String extand; // 扩展字段
 
}

 好了,到了这里一个简单的netty通讯就做好了

posted @   徐徐图之  阅读(14250)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
点击右上角即可分享
微信分享提示