草庐IT

netty对websocket协议的实现

小么嘛小二郎 2024-05-24 原文

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>

有关netty对websocket协议的实现的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  5. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  6. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  7. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  8. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

  9. ruby - Faye WebSocket,关闭处理程序被触发后重新连接到套接字 - 2

    我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d

  10. ruby - 实现k最近邻需要哪些数据? - 2

    我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项

随机推荐