Netty是如何处理websoceket协议的

0 阅读5分钟

1.目标:

一直想用netty做一个基于websocket的功能,但是实战前想系统的学习下什么是websocket协议,并且netty是如何做到和websocket结合的。基于这个目的,把自己此时此刻的心路历程记录下来,或许随着时间的推移,我能有更好的理解。

2.websocket连接三部曲

  1. 首先要完成 TCP 三次握手,浏览器要和服务器端建立稳定的连接

  2. HTTP 握手(升级协议)—— 最关键的一步:

    客户端会先发一个普通的 HTTP 请求,但这个请求里藏了“暗号”。

      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
      Sec-WebSocket-Origin: http://example.com
      Sec-WebSocket-Protocol: chat, superchat
      Sec-WebSocket-Version: 13
    

    Upgrade: websocket:这个请求头的作用非常直接,它明确地告诉服务器:“我不想继续用 HTTP 协议了,我想切换到 websocket 协议。” - 在响应头中:当服务器同意升级时,它会在响应头中也带上 Upgrade: websocket,意思是:“好的,我确认,我们将要切换到 websocket 协议。”

    Connection: Upgrade:告诉服务器或者中间代理,请注意,当前这个连接需要特殊处理,不要把它当作普通的 HTTP 连接来管理,我们要在这个连接上进行协议升级。

    请求头客户端发送时代表服务器响应时代表
    Upgrade: websocket我想升级到 WebSocket 协议。我同意升级到 WebSocket 协议。
    Connection: Upgrade请在当前连接上完成这次升级。我同意在当前连接上完成升级。

    服务端响应:

      Server response:
      HTTP/1.1 101 Switching Protocols
      Upgrade: websocket
      Connection: Upgrade
      Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
      Sec-WebSocket-Protocol: chat
    

    服务器一看请求 发现客户端想搞 WebSocket,如果自己支持。它就会返回一个特殊的代码 101(意思是:Switching Protocols,切换协议)。

  3. 第三步:全双工通信(自由聊天)

    握手一旦成功,那条 TCP 连接就不再走 HTTP 协议了,而是变成了 WebSocket 协议

3.websocket一些细节

如果使用netty作为websocket服务端,那到底是谁负责完成握手和协议转换的呢?

回答:首先Netty得先按照HTTP进行解析,识别出是 WebSocket 请求后,动态替换成 WebSocket 解析器。 所以channelPipeline中需要先放入 HttpServerCodec,负责把字节流翻译成 HTTP 请求。因为 WebSocket 握手本质上是一个 HTTP 请求,所以Netty可以用这个编解码器HttpServerCodec把网卡收到的二进制数据(010101...)翻译成 Java 对象 HttpRequest

也就是说:

  1. 第一阶段:刚连接时(HTTP 模式)

    当客户端刚连上来发送握手请求时,数据流经过 Pipeline,首先遇到的是 HttpServerCodec

  • 它的作用是把二进制字节流解析成 HttpRequest
  • 这时候,数据还是 HTTP 包,根本不是 WebSocket Frame。

pipeline中会放入WebSocketServerProtocolHandler,专门用来识别websocket协议,完成握手,切换协议。

  1. 第二阶段:握手瞬间(关键变身)

当 HttpRequest 传到 WebSocketServerProtocolHandler 时,它发现这是一个握手请求(带有 Upgrade: websocket),就会进行协议升级,服务端会:

  • 它会自动帮你校验握手信息。

  • 发送握手响应(101 Switching Protocols)。

  • 高能操作: 握手一旦成功,它会自动把 Pipeline 里那些“没用”的 HTTP 编解码器(比如 HttpServerCodec)移除掉,换上 WebSocket 的编解码器。

我们在初始化pipeline的时候,经常会如下操作:

pipeline.addLast(new HttpServerCodec()); // HTTP 编解码
pipeline.addLast(new HttpObjectAggregator(65536)); // 把 HTTP 消息聚合成完整的一个
// 核心在这里!
pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); 
// 之后就是你自己的业务 Handler
pipeline.addLast(new MyBusinessHandler());

一旦 WebSocketServerProtocolHandler 完成了握手,上面的 HttpServerCodec 就失效了。此后传给 MyBusinessHandler 的不再是 HttpRequest,而是 TextWebSocketFrameBinaryWebSocketFrame

WebSocketServerProtocolHandler是怎么发挥作用的

WebSocketServerProtocolHandler 见名知意是一个websocket服务端协议的处理器。它作为一个inboud handler。在添加到pipeline的时候,会在它的前面添加一个WebSocketServerProtocolHandshakeHandler

public void handlerAdded(ChannelHandlerContext ctx) {
    ChannelPipeline cp = ctx.pipeline();
    if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
        // Add the WebSocketHandshakeHandler before this one.
        cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
                new WebSocketServerProtocolHandshakeHandler(serverConfig));
    }
    if (serverConfig.decoderConfig().withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
        // Add the UFT8 checking before this one.
        cp.addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
                new Utf8FrameValidator(serverConfig.decoderConfig().closeOnProtocolViolation()));
    }
}

WebSocketServerProtocolHandshakeHandler:它就是用来处理握手和协议转换的,底层是根据请求的不同版本,使用不同的WebSocketServerHandshaker 来处理握手。

public WebSocketServerHandshaker newHandshaker(HttpRequest req) {

    CharSequence version = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_VERSION);
    if (version != null) {
        if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) {
            // Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification).
            return new WebSocketServerHandshaker13(
                    webSocketURL, subprotocols, decoderConfig);
        } else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) {
            // Version 8 of the wire protocol - version 10 of the draft hybi specification.
            return new WebSocketServerHandshaker08(
                    webSocketURL, subprotocols, decoderConfig);
        } else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) {
            // Version 8 of the wire protocol - version 07 of the draft hybi specification.
            return new WebSocketServerHandshaker07(
                    webSocketURL, subprotocols, decoderConfig);
        } else {
            return null;
        }
    } else {
        // Assume version 00 where version header was not specified
        return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig);
    }
}

有 三个版本,现在基本都用13版本

  1. WebSocketServerHandshaker13
  2. WebSocketServerHandshaker08
  3. WebSocketServerHandshaker07

WebSocketServerHandshaker13继承自WebSocketServerHandshaker,它是一个抽象类,用来统一处理握手 handshake。

public final ChannelFuture handshake(Channel channel, FullHttpRequest req,
                                        HttpHeaders responseHeaders, final ChannelPromise promise) {

    if (logger.isDebugEnabled()) {
        logger.debug("{} WebSocket version {} server handshake", channel, version());
    }
    FullHttpResponse response = newHandshakeResponse(req, responseHeaders);
    ChannelPipeline p = channel.pipeline();
    if (p.get(HttpObjectAggregator.class) != null) {
        p.remove(HttpObjectAggregator.class);
    }
    if (p.get(HttpContentCompressor.class) != null) {
        p.remove(HttpContentCompressor.class);
    }
    ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
    final String encoderName;
    if (ctx == null) {
        // this means the user use an HttpServerCodec
        ctx = p.context(HttpServerCodec.class);
        if (ctx == null) {
            promise.setFailure(
                    new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
            response.release();
            return promise;
        }
        p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
        p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
        encoderName = ctx.name();
    } else {
        p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder());

        encoderName = p.context(HttpResponseEncoder.class).name();
        p.addBefore(encoderName, "wsencoder", newWebSocketEncoder());
    }
    channel.writeAndFlush(response).addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            if (future.isSuccess()) {
                ChannelPipeline p = future.channel().pipeline();
                p.remove(encoderName);
                promise.setSuccess();
            } else {
                promise.setFailure(future.cause());
            }
        }
    });
    return promise;
}

这里面有几个关键性的步骤:

1.FullHttpResponse response = newHandshakeResponse(req, responseHeaders); 这个交给子类来实现,构建响应来切换协议

2.在pipeline中塞入WS 解码器和WS 编码器

3.不同的开发者配置 Netty 的方式不一样(有人用 HttpServerCodec,有人分开用 HttpRequestDecoderHttpResponseEncoder),这段代码兼容这两种情况

4.发完最后那个101 握手响应之后,就可以把http编码器移除了。

这就是“过河拆桥”的艺术。

  1. 握手响应(101) 本质上还是一个 HTTP 格式的报文。
  2. 所以,我们必须保留 HttpServerCodecHttpResponseEncoder,直到这个 101 响应彻底发完
  3. 一旦发完(operationComplete),旧的 HTTP 工具类就彻底成了累赘。Netty 啪的一声把它们从 Pipeline 里踢出去(p.remove(encoderName))。等最后一份 HTTP 报文(101 响应)发出去的一瞬间,瞬间撤掉所有 HTTP 相关的组件。