前言

在日常开发中经常会有消息推送的需求,例如报警消息,状态消息等,在web端通常会有几种方式实现。

  1. 短轮询,即在前端定时向后端发起请求查询是否有新的数据。这种方式很简单但是需要不断向服务器请求,并且大多数请求根本没数据浪费资源也对服务器产生了压力
  2. 长轮询,也是前端定时发出请求,所不同的是长轮询查询到没数据时不会马上断开连接,而是会等待一段时间,如果等待时有数据了就可以返回。
  3. websocket协议基于TCP,全双工通信的协议可以支持双向数据的即时发送。

实现简单的websocket端点

1. 创建服务

public class WsServer {


    private static Channel channel;

    private static EventLoopGroup boss = new NioEventLoopGroup(1);
    private static EventLoopGroup work = new NioEventLoopGroup();

    public static void startServer() {
        ServerBootstrap server = new ServerBootstrap();
        server.group(boss, work);
        server.channel(NioServerSocketChannel.class)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        ChannelPipeline pipeline = channel.pipeline();
                        pipeline.addLast(new HttpServerCodec());
                        pipeline.addLast(new HttpObjectAggregator(65535));
                        pipeline.addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
                        pipeline.addLast(new HeartbeatHandler());
                        pipeline.addLast(new WebSocketServerProtocolHandler("/netty/websocket", null,true));
                        pipeline.addLast(chatMessageHandler);
                    }
                });
        ChannelFuture channelFuture = server.bind(8088);
        try {
            System.out.println("websocket server started");
            channel = channelFuture.sync().channel();
        } catch (InterruptedException e) {
            System.out.println("websocket server start failed");
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        startServer();
    }

    public static void destroy(){
        System.out.println("websocket server closed");
        if (channel!=null){
            channel.close();
        }
        boss.shutdownGracefully();
        work.shutdownGracefully();
    }

  • 我们添加了一个LoggingHandler 可以打印出Netty的详细日志,方便我们调试
  • 我们同样需要添加Http协议的编码解码handler,因为websocket也是先通过http协议再升级提升的。
  • IdleStateHandler 是一个空闲状态的检测处理类,当连接长时间未读写就会触发事件,因此可以用于触发心跳处理
  • 我们还添加了一个心跳处理handler,在连接空闲时自动发送心跳数据保持连接的活跃状态
  • 添加WebSocketServerProtocolHandler,它实现了websocket协议的编码解码,让我们只需要专注于业务数据
  • 最后添加我们的业务处理handler。

2. 心跳handler

package com.coman404.ws;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

import java.util.Objects;

public class HeartbeatHandler extends ChannelDuplexHandler {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
        if (evt instanceof IdleStateEvent){
            IdleStateEvent e = (IdleStateEvent) evt;
            if (Objects.equals(e.state(), IdleState.READER_IDLE)){
                ctx.writeAndFlush(new TextWebSocketFrame("heart"));
            }
        }
    }
}

  • userEventTriggered 在handler处理过程中产生事件就会触发该方法,我们可以根据事件类型的不同,做出不同的处理。示例中是读空闲,即长时间未收到数据,我们便向客户端发送一个 heart的内容数据包。

3. 业务处理handler

package com.coman404.ws;

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


public class WsMessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        String channelId = ctx.channel().id().toString();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("有连接建立");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("有连接断开");
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){
            WebSocketServerProtocolHandler.HandshakeComplete complete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
            String subProtocol = complete.selectedSubprotocol();
            String uri = complete.requestUri();

        }
    }

}

  • 当握手成功后我们会收到HandshakeComplete 事件,这里我们可以获取到连接的端点uri和子协议信息,这里我们可以做一些鉴权操作。

问题

上面示例中我们实现了一个简单的websocket端点,但是我有一个需求,端点的uri不是固定的,可以像我们在spring中开发websocket一样,能够使用占位符或通配符的形式如:/netty/websocket/* 。
WebSocketServerProtocolHandler 并不能传递这样的通配符,只会精确匹配,不然会自动断开连接的

实现通配符格式的websocket端点

解决思路其实就是我们添加WebSocketServerProtocolHandler 前我们解析连接的地址,然后去匹配是否有对应的handler,如果有则使用请求地址来添加WebSocketServerProtocolHandler ,这样就可以匹配上了

1. 通配符处理handler

static class WebSocketPathValidatorHandler extends ChannelInboundHandlerAdapter {

        private final String wildcardPath; // 例如 "/ws/*"

        public WebSocketPathValidatorHandler(String wildcardPath) {
            this.wildcardPath = wildcardPath;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof FullHttpRequest) {
                FullHttpRequest request = (FullHttpRequest) msg;

                // 检查请求的 URI 是否符合通配符路径
                String uri = request.uri();
                boolean isMatch = uri.startsWith(wildcardPath.substring(0, wildcardPath.indexOf('*')-1));
                String token = request.headers().get("Sec-Websocket-Protocol");
                if (isMatch) {
                    // 路径匹配,继续 WebSocket 握手
                    // 添加 WebSocketServerProtocolHandler 到 pipeline
                    ctx.pipeline().addLast(new WebSocketServerProtocolHandler(uri, token, true));
                    ctx.pipeline().addLast(new WsMessageHandler());
                } else {
                    ctx.channel().close();
                }
                super.channelRead(ctx,msg);
            } else {
                super.channelRead(ctx, msg);
            }
        }
    }
  • 在创建handler时我们传入一个端点地址,可以是通配符形式(当然也可以是占位符格式,解决方案都是类似的
  • 当我们接收到请求后,可以从http消息中获取uri,也可以获取子协议信息(Sec-Websocket-Protocol)
  • 判断uri和子协议是否有对应匹配的,有则向pipeline中添加websocket协议的处理类和后续的业务处理,否则关闭连接

2.修改服务

在构建服务时我们就不再直接把websocket协议的handler添加到pipeline中而是使用我们自定义的WebSocketPathValidatorHandler
修改代码

server.channel(NioServerSocketChannel.class)
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        ChannelPipeline pipeline = channel.pipeline();
                        pipeline.addLast(new HttpServerCodec());
                        pipeline.addLast(new HttpObjectAggregator(65535));
                        pipeline.addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
                        pipeline.addLast(new HeartbeatHandler());
                        pipeline.addLast(new WebSocketPathValidatorHandler("/netty/websocket/*"));
//                        pipeline.addLast(new WebSocketServerProtocolHandler("/netty/websocket", null,true));
//                        pipeline.addLast(chatMessageHandler);
                    }
                });

结语

至此,服务已经可以提供正常的websocket服务了。上面提到的通配符形式的处理思路,也可以用于以后其它需要动态解决的问题上。在handler中我们仍然可以继续操作pipeline去实现更加复杂的数据流转。

posted on 2024-06-12 11:30  猿来就是尔  阅读(11)  评论(0)    收藏  举报  来源