Netty通过WebSocket编程实现服务器与客户端长连接

需求

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

运行界面

java netty实现长连接心跳 netty建立长连接_socket

WebSocketServer



package com.dance.netty.netty.websocket;

import com.dance.netty.netty.heartbeat.NettyServerHertBeat;
import com.dance.netty.netty.heartbeat.NettyServerIdleStateHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class WebSocketServer {
    private final int port;

    public WebSocketServer(int port) {
        this.port = port;
    }

    public void run() throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new LoggingHandler(LogLevel.INFO))  // 在BossGroup中增加一个日志处理器 日志级别为INFO
                    .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());
                            /*
                             * http 数据在传输过程中是分段的 http Object aggregator 就是可以将多个段聚合
                             * 这就是为什么 当浏览器发送大量数据时, 就会出现多次http请求
                             * 参数为 : 最大内容长度
                             */
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            /*
                             * 对应WebSocket 他的数据时以桢(frame) 形式传递
                             * 可以看到WebSocketFrame下面有6个子类
                             * 浏览器请求时: ws://localhost:7000/xxx 请求的url
                             * 核心功能是将http协议升级为ws协议 保持长链接
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
                            // 自定义Handler, 处理业务逻辑
                            pipeline.addLast(new WebSocketTextFrameHandler());
                        }
                    });
            System.out.println("netty server is starter......");
            ChannelFuture sync = serverBootstrap.bind(port).sync();
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

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



WebSocketTextFrameHandler



package com.dance.netty.netty.websocket;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.time.LocalDateTime;

/**
 * TextWebSocketFrame 表示一个文本桢
 */
public class WebSocketTextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("[服务器] : 收到消息 -> " + msg.text());
        // 回复浏览器
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间: " + LocalDateTime.now() + "->"+ msg.text()));
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值 LongText是唯一的
        System.out.println("handlerAdded 被调用:" + ctx.channel().id().asLongText());
        // shortText 可能会重复
        System.out.println("handlerAdded 被调用:" + ctx.channel().id().asShortText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // id 表示唯一的值 LongText是唯一的
        System.out.println("handlerRemoved 被调用:" + ctx.channel().id().asLongText());
        // shortText 可能会重复
        System.out.println("handlerRemoved 被调用:" + ctx.channel().id().asShortText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
        cause.printStackTrace();
    }
}



页面



<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    var socket;
    // 判断当前浏览器是否支持WebSocket
    if(window.WebSocket){
        socket = new WebSocket("ws://localhost:7000/hello");
        // 相当于ChannelRead ev就是消息回送
        socket.onmessage = function (ev){
            console.log(ev)
            let textArea = document.getElementById("responseText")
            textArea.value = textArea.value + "\n" + ev.data
        }
        // 相当于连接开启 ChannelAdded ev
        socket.onopen = function (ev) {
            console.log(ev)
            let textArea = document.getElementById("responseText")
            textArea.value = "连接开启了"
        }
        // 相当于连接关闭 ChannelRemove ev
        socket.onclose = function (ev) {
            console.log(ev)
            let textArea = document.getElementById("responseText")
            textArea.value = textArea.value + '\n' + "连接关闭了"
        }
    }else{
        alert("当前浏览器不支持WebSocket")
    }
    function send(msg) {
        if(!window.socket){
            return;
        }
        if(socket.readyState === WebSocket.OPEN){
            // 通过WebSocket发送消息
            socket.send(msg)
        }else{
            alert('连接没有开启')
        }
    }
</script>
<form onsubmit="return false">
    <textarea name="message" style="width: 300px; height: 300px;"></textarea>
    <input type="button" value="发送消息" onclick="send(this.form.message.value)">
    <textarea id="responseText" style="width: 300px; height: 300px;"></textarea>
    <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">

</form>
<script>


</script>

</body>
</html>



测试

启动服务器



netty server is starter......
一月 16, 2022 11:19:27 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xb0f42cfd] REGISTERED
一月 16, 2022 11:19:27 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0xb0f42cfd] BIND: 0.0.0.0/0.0.0.0:7000
一月 16, 2022 11:19:27 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xb0f42cfd, L:/0:0:0:0:0:0:0:0:7000] ACTIVE



启动页面

java netty实现长连接心跳 netty建立长连接_socket_02

可以看到连接开启了

java netty实现长连接心跳 netty建立长连接_java netty实现长连接心跳_03

可以看到浏览器发送了三个请求

第一个ws请求是和IDEA建立连接的不用管

第二个http协议是请求html文件的

java netty实现长连接心跳 netty建立长连接_js_04

我们主要看第三个请求, 这个就是我们自己的ws请求, 状态为101 Switching Protocols切换协议, 并且协议升级为ws协议

并且服务器感知,建立Channel连接

一月 16, 2022 11:20:46 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xb0f42cfd, L:/0:0:0:0:0:0:0:0:7000] READ: [id: 0x9b6bccb3, L:/0:0:0:0:0:0:0:1:7000 - R:/0:0:0:0:0:0:0:1:55639]
一月 16, 2022 11:20:46 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xb0f42cfd, L:/0:0:0:0:0:0:0:0:7000] READ COMPLETE
handlerAdded 被调用:005056fffec00008-00006534-00000002-37865b9aaa734014-9b6bccb3
handlerAdded 被调用:9b6bccb3

java netty实现长连接心跳 netty建立长连接_js_05

页面发送消息, 后端连接返回消息,并且浏览器中并没有新的请求

java netty实现长连接心跳 netty建立长连接_websocket_06

服务器



[服务器] : 收到消息 -> hi netty



关闭浏览器后服务端感知,同样的关闭服务器浏览器也会感知



handlerRemoved 被调用:005056fffec00008-00006534-00000002-37865b9aaa734014-9b6bccb3
handlerRemoved 被调用:9b6bccb3



java netty实现长连接心跳 netty建立长连接_websocket_07

并且在建立WebSocket连接时需要请求路径和后端配置路径一致

前端: ws://localhost:7000/hello

后端配置: /hello