netty 与 webSocket

蹲街弑〆低调 提交于 2021-01-01 07:41:11

netty 与 webSocket

起因

有个需求需要用到webSocket ,然后最近又正好在学netty,然后合起来走一波。写篇文章记录一下,做一个念想。

协议格式

0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+
 
 具体每一bit的意思
FIN      1bit 表示信息的最后一帧
RSV 1-3  1bit each 以后备用的 默认都为 0
Opcode   4bit 帧类型,稍后细说
Mask     1bit 掩码,是否加密数据,默认必须置为1
Payload  7bit 数据的长度
Masking-key      1 or 4 bit 掩码
Payload data     (x + y) bytes 数据
Extension data   x bytes  扩展数据
Application data y bytes  程序数据
OPCODE:4位
解释PayloadData,如果接收到未知的opcode,接收端必须关闭连接。
0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留

开始

我们先写一个什么都不加的 service 热热手,话不多说,代码如下

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author Sean Wu
 */
public class ServiceMain {


    public static void main(String[] args) throws Exception {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {

                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new StringEncoder()).addLast(new StringDecoder()).addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                super.channelRead(ctx, msg);
                                System.out.println(msg.toString());
                            }
                        });
                    }
                });
        ChannelFuture f = b.bind(8866).sync();
        f.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }

}

常规的netty入门示例,加了个String的编码和解码器,还加了一个打印消息的 Handler,并不是什么太复杂的代码。

添加Http的支持

websocket 协议作为 http 协议的一种升级,最好么我们先顺手添加一下对 Http 协议的支持。首先我们写一个 HTTPRequestHandler,话不多说,代码如下

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;

/**
 * @author Sean Wu
 */
public class HTTPRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

        // 创建要返回的内容
        byte[] retBytes = "this a simple http response".getBytes();
        ByteBuf byteBuf = Unpooled.copiedBuffer(retBytes);
        // 由于http并不是我们关心的重点,我们就直接返回好了
        DefaultHttpResponse response = new DefaultHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, retBytes.length);
        ctx.writeAndFlush(response);
        ctx.writeAndFlush(byteBuf);

    }
}

这个 Handler 对 http 协议做了一个最简单的支持,就是不管客户端传啥都返回一个 this a simple http response。什么keep-alive,Expect:100-Continue都先不管好了,跟我们这次要讲的websocket 并没有什么关系的说。然后我们改一下我们上面的 ServiceMain 这个类,在Channel里添加对http的支持。代码如下。


import com.jiuyan.xisha.websocket.handler.HTTPRequestHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @author Sean Wu
 */
public class ServiceMain {
    public static void main(String[] args) throws Exception {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new HttpServerCodec())
                                .addLast(new HttpObjectAggregator(65536))
                                .addLast(new HTTPRequestHandler());
                    }
                });
        ChannelFuture f = b.bind(8866).sync();
        f.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

可以看到也非常的简单,介绍下我们这里用到的几个Handler

ChannelHandler 作用
HttpServerCodec 对字节码根据http协议进行编码和解码,
HttpObjectAggregator 将一个 HttpMessage 和跟随它的多个 HttpContent 聚合
为单个 FullHttpRequest 或者 FullHttpResponse (取
决于它是被用来处理请求还是响应)。安装了这个之后,
ChannelPipeline 中的下一个 ChannelHandler 将只会
收到完整的 HTTP 请求或响应
HTTPRequestHandler 处理 HttpObjectAggregator 送过来的 FullHttpRequest 请求

然后我们运行一下 ServiceMain 然后用浏览器访问一下,正常的话,如图所示。 正常返回

添加对 websocket 的支持

首先,我们在刚才的 HTTPRequestHandler 的 channelRead0 方法里添加对 websocket 接口的特殊处理。修改后的代码如下

/**
 * @author Sean Wu
 */
public class HTTPRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {



    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {

        System.out.println(msg.uri());
        // 如果后缀为 ws 的请求,则增加引用计数,将他传给下一个 ChannelInboundHandler
        if ("/ws".equalsIgnoreCase(msg.uri())) {
            ctx.fireChannelRead(msg.retain());
            return;
        }

        // 之前的代码

    }
}

然后我们要加一个处理 websocket 协议的 handler 根据WebSocket 协议,netty 定义了如下六种帧

帧类型 秒速
BinaryWebSocketFrame 充满了二进制数据流的一个帧,大多是多媒体文件
TextWebSocketFrame 充满了文本的一个帧
CloseWebSocketFrame 用来关闭websocket的帧
PingWebSocketFrame 用来探活的的一个帧
PongWebSocketFrame 用来表示自己还活着的一个帧

Netty 里提供了一个叫 WebSocketServerProtocolHandler 的类,他会帮你处理 Ping,Pong,Close之类的服务状态的帧。这里我们只需要简单的用下TextWebSocketFramce就好了。

/**
 * @author Sean Wu
 */
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {


    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
            ctx.writeAndFlush(new TextWebSocketFrame("client " + ctx.channel() + "join"));
        }
        super.userEventTriggered(ctx, evt);
    }

    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println(msg.text());
        ctx.writeAndFlush(new TextWebSocketFrame("hello" + msg.text()));
    }
}

这里我们的例子非常的简单,可以说是网上所有 netty-websocket 的例子里最简单的了。我们只是在收到了客户端的消息之后打印了一下然后原封不动的加个 hello 返回回去。

再然后,我们要改一下我们之前的 ChannelPipeline。添加对 websocket 的支持。改完之后的代码如下

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap b = new ServerBootstrap();
        b.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline p = ch.pipeline();
                        p.addLast(new HttpServerCodec())
                                .addLast(new HttpObjectAggregator(65536))
                                .addLast(new HTTPRequestHandler())
                                .addLast(new WebSocketServerProtocolHandler("/ws"))
                                .addLast(new TextWebSocketFrameHandler());
                    }
                });
        ChannelFuture f = b.bind(8866).sync();
        f.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }

运行示例

首先,启动我们的服务器。然后打开刚才的那个页面(http://127.0.0.1:8866/),打开调试模式(f12)。 然后输入如下 js 代码

var ws = new WebSocket("ws://127.0.0.1:8866/ws");
ws.onopen = function(evt) {
    console.log("链接建立了 ...");
};
ws.onmessage = function(evt) {
    console.log( "收到了消息: " + evt.data);
};

image 可以看到,很完美。然后我们再试着用 ws.send("xisha") 发些消息看。发消息的js代码和结果如下。 image 我们也可以打开网络面板查看我们的消息内容。 image 可以看到,只有一个链接。

总结

还有很多没讲到,恩。。。问题不大。下次有机会再说。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!