netty对websocket协议的实现

1. websocket协议

websocket协议是对http协议的扩充, 也是使用的TCP协议可以全双工通信的应用层协议。 websocket协议允许服务端向客户端推送消息。 浏览器和服务端只需要进行一次握手,不必像http协议一样,每次连接都要新建立连接,两者之间创建持久性的连接,并进行双向的数据交互。

http/1.1 是 请求-响应设计的,后来支持了更多的传输类型 图片,但都是基于请求响应。

不足:

  • 传输数据为文本,且请求头与响应头冗长重复。
  • 请求-响应模式,只能客户端发送请求给服务端,服务端才可以发送响应数据给客户端。
1. websocket连接建立过程

在这里插入图片描述

websocket首次请求服务端建立连接,也是客户端发起的,基于http请求的。 请求头中多携带消息

GET /test HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: tFGdnEL/5fXMS9yKwBjllg==
Origin: http://example.com
Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13

首先客户端(如浏览器)发出带有特殊消息头(UpgradeConnection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。

  • Connection必须设置Upgrade,表示客户端希望连接升级。
  • Upgrade: websocket表明协议升级为websocket。
  • Sec-WebSocket-Key字段内记录着握手过程中必不可少的键值,由客户端(浏览器)生成,可以尽量避免普通HTTP请求被误认为Websocket协议。
  • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13。
  • Origin字段是必须的。如果缺少origin字段,WebSocket服务器需要回复HTTP 403 状态码(禁止访问),通过Origin可以做安全校验。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Sec-WebSocket-Protocol: v12.stomp

Sec-WebSocket-Accept的字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。

2. 消息帧

WebSocket使用二进制消息帧作为双向通信的媒介。何为消息帧?发送方将每个应用程序消息拆分为一个或多个帧,通过网络将它们传输到目的地,并重新组装解析出一个完整消息。

有别于HTTP/1.1文本消息格式(冗长的消息头和分隔符等),WebSocket消息帧规定一定的格式,以二进制传输,更加短小精悍。二者相同之处就是都是基于TCP/IP流式协议(没有规定消息边界)

在这里插入图片描述

  • FIN: 1 bit,表示该帧是否为消息的最后一帧。1-是,0-否。
  • RSV1,RSV2,RSV3: 1 bit each,预留(3位),扩展的预留标志。一般情况为0,除非协商的扩展定义为非零值。如果接收到非零值且不为协商扩展定义,接收端必须使连接失败。
  • Opcode: 4 bits,定义消息帧的操作类型,如果接收到一个未知Opcode,接收端必须使连接失败。(0x0-延续帧,0x1-文本帧,0x2-二进制帧,0x8-关闭帧,0x9-PING帧,0xA-PONG帧(在接收到PING帧时,终端必须发送一个PONG帧响应,除非它已经接收到关闭帧),0x3-0x7保留给未来的非控制帧,0xB-F保留给未来的控制帧)
  • Mask: 1 bit,表示该帧是否为隐藏的,即被加密保护的。1-是,0-否。Mask=1时,必须传一个Masking-key,用于解除隐藏(客户端发送消息给服务器端,Mask必须为1)。
  • Payload length: 7 bits, 7+16 bits, or 7+64 bits,有效载荷数据的长度(扩展数据长度+应用数据长度,扩展数据长度可以为0)。
  • Masking-key: 0 or 4 bytes,用于解除帧隐藏(加密)的key,Mask=1时不为空,Mask=0时不用传。
  • Payload data: (x+y) bytes,有效载荷数据包括扩展数据(x bytes)和应用数据(y bytes)。有效载荷数据是用户真正要传输的数据。

这样的二进制消息帧设计,与HTTP协议相比,WebSocket协议可以提供约500:1的流量减少和3:1的延迟减少。

3. 关闭连接

挥手相对于握手要简单很多,客户端和服务器端任何一方都可以通过发送关闭帧来发起挥手请求。发送关闭帧的一方,之后不再发送任何数据给对方;接收到关闭帧的一方,如果之前没有发送过关闭帧,则必须发送一个关闭帧作为响应。关闭帧中可以携带关闭原因。

在发送和接收一个关闭帧消息之后,就认为WebSocket连接已关闭,且必须关闭底层TCP连接。

除了通过关闭握手来关闭连接外,WebSocket连接也可能在另一方离开或底层TCP连接关闭时突然关闭。

协议介绍与图片来自https://blog.csdn.net/weixin_36586120/article/details/120025498

4. 优点
  • 较少的控制开销。在连接建立后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对于HTTP请求每次都要携带完整的头部,显著减少。
  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。
  • 保持连接状态。与HTTP不同的是,Websocket需要先建立连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  • 支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。
  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著提高压缩率。
2. netty对websocket协议的实现
1.过程
  1. 初始handler需要添加http协议的编解码器。

    添加 HttpServerCodec 转为HttpRequest,添加HttpObjectAggregator将http消息聚合为一个FullHttpRequest,因为websocket的协议handler的channerRead接受的是该参数。

  2. 添加WebSocketServerProtocolHandler,这个是websocket协议的处理器,会处理首次请求的握手操作,并升级协议,更换处理器。

  3. 当客户端连接时,WebSocketServerProtocolHandler 触发 handlerAdded()回调,会立刻为这个channel注册一个握手处理器。 握手处理器位置是先于websocket处理器 但是晚于http协议处理器。

    if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
                // Add the WebSocketHandshakeHandler before this one.
                ctx.pipeline().addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
                        new WebSocketServerProtocolHandshakeHandler(websocketPath, subprotocols,
                                allowExtensions, maxFramePayloadLength, allowMaskMismatch, checkStartsWith));
            }
    
    
  4. 当客户端发送第一个http握手请求,请求升级为websocket协议。先是经过http协议处理器,将消息转为了FullHttpMessgae,之后在 握手处理的read方法中。

    //1. 根据ws协议版本号创建了一个指定版本的处理器。
    final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                        getWebSocketLocation(ctx.pipeline(), req, websocketPath), subprotocols,
                                allowExtensions, maxFramePayloadSize, allowMaskMismatch);
    final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
    
    //2. 指定版本的处理器 处理握手请求
    final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
    
    //3.握手处理器,替换为新的处理器, 这个处理器对非ws请求拒绝处理。
    ctx.pipeline().replace(this, "WS403Responder",
                       WebSocketServerProtocolHandler.forbiddenHttpRequestResponder());
    
    
  5. 4的第二步 将处理都交到指定ws协议WebSocketServerHandshaker手中,这个处理是先创建一个响应,告诉客户端服务升级。 之后将http相关的处理器都移除掉, 添加ws协议相关的处理器。

    // 1. 首先创建服务端确定升级ws的响应
    FullHttpResponse response = newHandshakeResponse(req, responseHeaders);
    
    //2.移除http协议处理器
    ChannelPipeline p = channel.pipeline();
    if (p.get(HttpObjectAggregator.class) != null) {
        p.remove(HttpObjectAggregator.class);
    }
    if (p.get(HttpContentCompressor.class) != null) {
        p.remove(HttpContentCompressor.class);
    }
    
    //3. 添加ws协议的编解码器 ,也是handler
    p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
    p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
    
    
  6. 此时客户端收到服务端响应,就升级为ws协议了,之后的用户自定义的处理器,就能处理了。

2. netty编程ws协议服务端
package eWebscoket;

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.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder;
import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * netty对websocket协议实现测试:
 * 1.  netty 提供了{@link WebSocketServerProtocolHandler} handler用于处理websocket协议。
 * handler中有ws的握手支持,ping、pong响应,close请求这些支持, 同时将Text  与 binary 数据传递
 * 给之后的handler进行业务处理。WebSocketServerProtocolHandler 通过WebSocketServerProtocolHandshakeHandler 首次read请求
 * 确定websocket协议版本(一般是13), 给channel绑定了不容版本协议下的WebSocket13FrameDecoder。
 *
 * <p>
 * 2.WebSocketServerProtocolHandler在有新的channel连接注册回调方法中{@link WebSocketServerProtocolHandler#handlerAdded(ChannelHandlerContext)},
 * 会在当前handler中添加一个新的handler {@link WebSocketServerProtocolHandshakeHandler}专门用于处理首次客户端请求的握手操作, 查看
 * {@link io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandshakeHandler#channelRead(ChannelHandlerContext, Object)}的接收客户端数据的处理,会为客户返回
 * 一个FullHttpResponse,响应头中,含有确认更新为websocket协议的响应。 当这个channel写会给客户端,此时这个channel的通信协议就有http转为websocket,这个握手用的handler也会
 * 自己移除了。
 * <p>
 * 3. 握手处理过程:
 * 就会将pipLine中用于握手的http的解析handler给移除掉了,
 * p.remove(HttpContentCompressor.class);
 * p.remove(HttpObjectAggregator.class);
 * 然后在 HttpServerCodec 的 handler 之前添加 websocket协议的handler:
 * p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder()); {@link WebSocket08FrameDecoder#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf, java.util.List)}
 * p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder()); {@link WebSocket08FrameEncoder#encode(io.netty.channel.ChannelHandlerContext, io.netty.handler.codec.http.websocketx.WebSocketFrame, java.util.List)}
 * 之后将response写会客户端成功后升级,将HttpServerCodec移除掉,删除代码在handshake()方法的写会回调中。
 * 在握手升级之后WebSocketServerProtocolHandshakeHandler 没有用了,就会将这个handler,替换成WebSocketServerProtocolHandler.forbiddenHttpRequestResponder()
 * 对非ws的请求拒绝处理。
 *
 * @author mahao
 * @date 2022/10/18
 */
public class ServerWebSocket {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //添加http消息的转换,将socket数据流 转换为 HttpRequest, websocket协议添加这个是为了
                        //握手时候使用。 HttpServerCodec 与 HttpObjectAggregator会在第一次http请求后,被移除掉,握手结束了
                        //协议升级就会只用websocket协议了。
                        pipeline.addLast(new HttpServerCodec());
                        //未知
                        pipeline.addLast(new ChunkedWriteHandler());
                        //将拆分的http消息聚合成一个消息。
                        pipeline.addLast(new HttpObjectAggregator(8096));

                        //用户websocket协议的 握手,ping pang处理, close处理,对于二进制或者文件数据,直接交付给下层
                        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

                        pipeline.addLast("myHandler", new WebSocketHandler());

                    }
                });
        ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();
        channelFuture.channel().closeFuture().sync();

        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();

    }
}
3. html 客户端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webSock客户端</title>
</head>
<body>
<script type="text/javascript">
    var socket;
    if (window.WebSocket){
        socket= new WebSocket("ws://localhost:9999/ws");

        socket.onmessage = function (ev) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n" + ev.data;
        }
        socket.onopen = function (ev) {
            var ta = document.getElementById("responseText");
            ta.value = "连接开启";
        }
        socket.onclose = function (ev) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n" + "连接关闭";
        }
    } else {
        alert("浏览器不支持websocket");
    }

    function send(message) {
        alert(123);
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN){
            alert(123);
            socket.send(message);
        }

    }

</script>

<form onsubmit="return false;">
    <textarea name="message" style="width: 400px;height: 200px"></textarea>
    <input type="button" value="发送消息" onclick="send(this.form.message.value);">

    <h3>服务器端输出</h3>
    <textarea id="responseText" style="width: 400px;height: 300px"></textarea>
    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空内容">

</form>
</body>
</html>

  • 10
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值