第十一章 Netty 小应用

本文介绍了使用Netty实现群聊系统,包括服务器端和客户端代码,实现了用户上线、离线及消息转发功能。接着展示了心跳检测机制,当服务器超过特定时间无读写操作时,触发相应提示。最后,通过WebSocket搭建了全双工的长连接服务器,客户端可以实时接收和发送消息。
摘要由CSDN通过智能技术生成

目录

一、群聊

服务端代码

客户端代码

测试结果

二、心跳检测

代码

结果

三、通过WebSocket实现长连接

服务端代码

客户端代码

结果


一、群聊

需求

  1. 编写一个 Netty 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
  2. 实现多人群聊
  3. 服务器端:可以监测用户上线,离线,并实现消息转发功能
  4. 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(由服务器转发得到)
  5. 目的:进一步理解Netty非阻塞网络编程机制

服务端代码

ChatGroupServerHandler.java

public class ChatGroupServerHandler extends SimpleChannelInboundHandler<String> {

    /**
     * 定义一个 channelGroup ,管理所有的channel
     * GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
     */
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 表示Channel 处于活动状态,提示 XX 上线
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + "-上线了~");
    }

    /**
     * 表示Channel 处于不活动状态,提示 XX 离线
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + "-离线了~");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 关闭通道
        ctx.close();
    }

    /**
     * 表示连接建立,一旦建立连接,第一个执行该方法
     * 并且将当前 channel 加入到channelGroup
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        // 将该客户端加入聊天的信息,发送给其他客户端
        /**
         * 该方法将会 循环遍历ChannelGroup中所有的Channel,并发送消息,我们不需要自己遍历
         */
        channelGroup.writeAndFlush(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm:ss")
                .format(LocalDateTime.now()) + ": 【" + channel.remoteAddress() + "】-加入聊天");
        channelGroup.add(channel);
    }

    // 断开连接,将XX客户端离开的消息推送给当前在线的客户端
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm:ss")
                .format(LocalDateTime.now()) + ": 【" + channel.remoteAddress() + "】-离开聊天");
        System.out.println("GroupChannel-Size:" + channelGroup.size());
    }

    // 读取数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        // 读取道当前的Channel
        Channel channel = ctx.channel();
        // 这时我们遍历ChannelGroup,根据不同的情况,回送不同的消息
        channelGroup.forEach(ch -> {
            if (channel != ch) {
                // 不是自己
                ch.writeAndFlush("【" + channel.remoteAddress() + "】发送消息:" + msg + "\n");
            } else {
                // 回显
                ch.writeAndFlush("【自己】:" + msg + "\n");
            }
        });

    }
}

ChatGroupServer.java

/**
 * 群聊服务端
 */
public class ChatGroupServer {
    private final int PORT;

    public ChatGroupServer(int PORT) {
        this.PORT = PORT;
    }

    public void run() {
        // 创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            // 向pipeline 加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            // 向pipeline 加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            // 加入自己的业务处理的handler
                            pipeline.addLast(new ChatGroupServerHandler());
                        }
                    });
            System.out.println("群聊服务器启动~~~");

            ChannelFuture channelFuture = serverBootstrap.bind(PORT).sync();
            // 监听关闭
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) {
        new ChatGroupServer(7000).run();
    }
}

客户端代码

ChatGroupClientHandler.java

public class ChatGroupClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg.trim());
    }
}

ChatGroupClient.java

public class ChatGroupClient {

    private final int PORT;
    private final String HOST;

    public ChatGroupClient(String host, int port) {
        PORT = port;
        HOST = host;
    }

    private void run() {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            // 向pipeline 加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            // 向pipeline 加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            // 加入自己的业务处理的handler
                            pipeline.addLast(new ChatGroupClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(HOST, PORT).sync();
            // 得到Channel
            Channel channel = channelFuture.channel();
            System.out.println("-----" + channel.localAddress() + "-----");
            // 客户端需要输入信息,
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                // 通过channel 发送到服务端
                channel.writeAndFlush(msg + "\r\n");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new ChatGroupClient("127.0.0.1", 7000).run();
    }
}

测试结果

  image.png                  image.png  

                 服务端                                                                    客户端1 

 image.png      image.png

                客户端2                                                                    客户端3                                                                               

二、心跳检测

需求:  

  1. 编写一个 Netty心跳检测机制案例, 当服务器超过3秒没有读时,就提示读空闲
  2. 当服务器超过5秒没有写操作时,就提示写空闲
  3. 实现当服务器超过7秒没有读或者写操作时,就提示读写空闲

代码

HeartBeatHandler.java

public class HeartBeatHandler extends ChannelInboundHandlerAdapter {

    /**
     * @param ctx 上下文
     * @param evt 事件
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 将evt 向下转型,
        IdleStateEvent event = (IdleStateEvent) evt;
        switch (event.state()) {
            case READER_IDLE:
                System.out.println(System.currentTimeMillis()+":读空闲");
                break;
            case WRITER_IDLE:
                System.out.println(System.currentTimeMillis()+":写空闲");
                break;
            case ALL_IDLE:
                System.out.println(System.currentTimeMillis()+":读写空闲");
                break;
        }

        // 关闭通道
//        ctx.close();
    }
}

HeartBeatServer.java

public class HeartBeatServer {

    public static void main(String[] args) {

        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            /**
                             * 加入一个netty提供的IdleStateHandler
                             * 说明:
                             * 1. IdleStateHandler 是 netty 提供的处理空闲状态的处理器
                             * 2. long readerIdleTime: 表示经过多长时间没有读,就会发送一个心跳检测包进行连接检查
                             * 3. long writerIdleTime: 表示经过多长时间没有写,就会发送一个心跳检测包进行连接检查
                             * 4. long allIdleTime: 表示经过多长时间没有读写,就会发送一个心跳检测包进行连接检查
                             * 官方解释:
                             * Triggers an IdleStateEvent when a Channel has not performed read, write, or both operation for a while.
                             * 5. 当 IdleStateEvent 触发后,就会传递给管道的下一个handler去处理,
                             *   通过调用下一个Handler的 userEventTiggered,在该方法中处理IdleStateEvent(读空闲,写空闲,读写空闲)
                             */
                            pipeline.addLast(new IdleStateHandler(7, 10, 5, TimeUnit.SECONDS));
                            pipeline.addLast(new HeartBeatHandler());
                        }
                    });
            System.out.println(System.currentTimeMillis()+"服务器启动");
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }

    }
}

结果

image.png

三、通过WebSocket实现长连接

要求:  实现基于webSocket的长连接的全双工的交互

  1. Http协议是无状态的, 浏览器和服务器间的请求响应一次,下一次会重新创建连接.
  2. 需要改变Http协议多次请求的约束,实现长连接, 服务器可以发送消息给浏览器
  3. 客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知

 

服务端代码

  • WebSocketServerHandler.java
/**
 * TextWebSocketFrame 表示一个文本帧
 */
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("服务端收到消息:"+msg.text());
        // 回复消息
        ctx.writeAndFlush(new TextWebSocketFrame("当前时间:"+ LocalDateTime.now()+"--"+msg.text()));

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发生:" + cause.getMessage());
        ctx.close();
    }

    /**
     * 当 web 客户端连接后,触发该方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // id 是唯一值,short 不唯一,Long 是唯一的
        System.out.println("handlerAdded:"+ctx.channel().id().asShortText());
        System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved:"+ctx.channel().id().asLongText());
    }
}
  • WebSocketServer.java
/**
 * websocket 服务端
 */
public class WebSocketServer {

    public static void main(String[] args) {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.ERROR)) // 打印 netty 日志
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 因为基于Http协议,所以使用http的编解码器
                            pipeline.addLast(new HttpServerCodec());
                            // 以块方式写,添加 ChunkedWriteHandler 处理器
                            pipeline.addLast(new ChunkedWriteHandler());
                            /**
                             * 说明
                             * 1. Http再传输过程中是分段,HttpObjectAggregator 就是可以将多个段聚合,
                             * 2. 这就是为什么,当浏览器发送大数据时,会发生多次 http 请求
                             */
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            /**
                             * 说明:
                             * 1. websocket 协议,数据是以帧(frame)形式传递
                             * 2. WebSocketFrame 下面有6个子类
                             * 3. 浏览器请求时:ws:127.0.0.1/7998/hello 表示请求的uri
                             * 4. WebSocketServerProtocolHandler 核心功能是将http协议升级为ws协议,保持长连接,状态码101
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
                            // 自定义的handler,处理业务逻辑
                            pipeline.addLast(new WebSocketServerHandler());
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(7998).sync();
            System.out.println("~~~websocket 服务器已经启动~~~");
            channelFuture.channel().closeFuture().sync();
        } catch (Exception exception){
            exception.printStackTrace();
        }
        finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

客户端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>netty-websocket-client</title>
</head>
<body>

<script>
    let socket;
    // 判断当前浏览器是否支持websocket
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:7998/hello");

        // 相当于 channelRead0,读取消息
        socket.onmessage = function (ev) {
            const showText = document.getElementById('responseText');
            showText.value += ev.data + "\r\n";
        }

        // 连接开启
        socket.onopen = function (ev) {
            const showText = document.getElementById('responseText');
            showText.value = "已建立连接"+"\r\n";
        }

        // 连接关闭
        socket.onclose = function (ev) {
            const showText = document.getElementById('responseText');
            showText.value += "已关闭连接" + "\r\n";
        }

        // 发送消息到服务器
        function send(msg) {
            // 先判断socket是否创建好
            if (!socket) {
                return;
            }
            if (socket.readyState === socket.OPEN) { // 判断websocket是否以建立连接
                socket.send(msg);
                const sendBox = document.getElementById('sendBox');
                sendBox.value = '';
            } else {
                alert("未开启连接")
            }

        }

    } else {
        alert("当前浏览器不支持websocket!!!");
    }
</script>

<form onsubmit="return false">
    <textarea id = 'sendBox' name="message" style="width: 150px;height: 200px"></textarea>
    <input type="button" value="发送消息" onclick="send(this.form.message.value)">
    <textarea id="responseText" style="width: 150px;height: 200px"></textarea>
    <input type="button" value="清空内容" onclick="document.getElementById('responseText').value = ''">
</form>

</body>
</html>

结果

image.png           image.png

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值