Netty之WebSocket协议应用开发

时间:2022-07-24
本文章向大家介绍Netty之WebSocket协议应用开发,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上一篇聊了一下使用Netty进行HTTP协议的应用开发,今儿就来说一下HTTP协议的弊端以及WebSocket协议的开发使用。

1、HTTP协议的弊端

(1)半双工模式:同一时刻,只能有一个方向的数据传输,不能同时传输。

(2)消息冗长复杂:包括了一大堆的消息头、消息体等,相比于其它二进制通信传输,冗长而繁琐。

2、WebSocket协议

WebSocket提供了一种浏览器与服务器间进行全双工通信的网络技术,浏览器与服务器之间只需要做一个握手动作,之后就形成了一条快速通道,两者可以互相传输数据。WebSocket是基于TCP全双工进行消息传递,相比于HTTP半双工,性能得到很大的提升。

特点:

(1)单一的TCP连接,采用全双工模式通信

(2)无头部信息、cookie和身份认证

(3)服务端可以主动传递消息给客户端,不需要客户端轮询

(4)通过“PING/PONG”保持心跳检测

WebSocket进行握手的请求还是HTTP请求,只是在请求头上多了几个标识表明此请求是WebSocket握手请求:

其中Upgrade:websocket就是表明此请求为WebSocket握手请求。

3、Netty之WebSocket协议开发使用

这边我们开发一个WebSocket服务端,服务端在接收到客户端请求之后,发送当前时间给客户端的示例。需要处理的是HTTP握手请求以及消息接受处理。

服务端程序

public class WSServer {

    public static void main(String[] args) {
        new WSServer().start();
    }

    private void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap server = new ServerBootstrap();
            server.group(boss,worker)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,128)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //将请求和应答消息编码或解码成http消息
                            pipeline.addLast(new HttpServerCodec());
                            pipeline.addLast(new HttpObjectAggregator(65536));
                            //支持异步发送大的码流
                            pipeline.addLast(new ChunkedWriteHandler());
                            pipeline.addLast(new WSServerHandler());
                        }

                    })
                    .childOption(ChannelOption.SO_KEEPALIVE,true);

            ChannelFuture sync = server.bind(8888).sync();

            System.out.println("WebSocket服务端已启动,端口号为8888");

            sync.channel().closeFuture().sync();

        } catch (Exception e) {

        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

服务端添加了几个处理器,分别是

(1)HttpServerCodec:将请求和应答消息编码或解码成HTTP消息。 (2)HttpObjectAggregator:将HTTP消息的多个部分组合成一条完整的HTTP消息

(3)ChunkedWriteHandler:支持异步发送大的码流。

接下来看一下自定义的处理器:

public class WSServerHandler extends SimpleChannelInboundHandler<Object> {


    private static final Log logger = LogFactory.getLog(WSServerHandler.class);


    private WebSocketServerHandshaker handshaker;

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
        //如果是握手请求
        if (msg instanceof FullHttpRequest) {
            handlerHttpRequest(ctx,(FullHttpRequest)msg);
        } else if (msg instanceof WebSocketFrame) {//如果是WebSocket消息接受
            handlerWebSocketFrame(ctx,(WebSocketFrame)msg);
        }
    }

    private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {

        //判断是否是关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return ;
        }

        //判断是否是ping指令
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }

        //只支持文本信息
        if (!(frame instanceof TextWebSocketFrame)) {
            throw new UnsupportedOperationException(frame.getClass().getName()+" frame type not support ");
        }


        String text = ((TextWebSocketFrame) frame).text();
        logger.info("服务端接收到客户端信息为:"+text);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        ctx.writeAndFlush(new TextWebSocketFrame("欢迎使用Netty WebSocket服务,目前时间为:"+ format.format(new Date())));
    }

    private void handlerHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {


        //如果http解码失败,返回http异常
        //判断是否是WebSocket握手请求
        if (!req.decoderResult().isSuccess()
                || (!"websocket".equals(req.headers().get("Upgrade")))) {
            sentHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }

        //构造握手响应返回
        WebSocketServerHandshakerFactory wsFactory =
                new WebSocketServerHandshakerFactory("ws://localhost:8888", null, false);
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(),req);
        }
    }

    private void sentHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
        if (res.status().code() != 200) {
            ByteBuf byteBuf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(byteBuf);
            byteBuf.release();
            setContentLength(res,res.content().readableBytes());
        }

        //如果是非keep-alive连接,关闭连接
        ChannelFuture future = ctx.channel().writeAndFlush(res);
        if (!isKeepAlive(req) || res.status().code()!=200) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

}

(1)首先在messageReceived方法中判断是HTTP握手请求还是WebSocket接入。

(2)如果是HTTP握手请求,则判断是否是WebSocket的握手请求,判断方法是请求头中是否有Upgrade:websocket这个消息,如果是WebSocket握手请求,则构建握手响应返回。

(3)如果是WebSocket接入,判断是关闭指令还是ping指令,也可以判断消息是否是文本消息,然后构建TextWebSocketFrame对象返回给客户端。

测试

这里推荐使用网上已有的WebSocket测试工具,不推荐自己写前端代码测试,因为麻烦。

启动WebSocket服务端:

在测试工具中输入ws://localhost:8888进行连接:

连接成功后,就可以发信息了: