惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Tor Project blog
AWS News Blog
AWS News Blog
量子位
博客园 - 聂微东
L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
小众软件
小众软件
人人都是产品经理
人人都是产品经理
A
About on SuperTechFans
L
Lohrmann on Cybersecurity
Know Your Adversary
Know Your Adversary
V
Visual Studio Blog
P
Privacy International News Feed
K
Kaspersky official blog
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
GRAHAM CLULEY
S
Securelist
Last Week in AI
Last Week in AI
Latest news
Latest news
B
Blog
T
Tenable Blog
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
T
The Exploit Database - CXSecurity.com
N
Netflix TechBlog - Medium
A
Arctic Wolf
SecWiki News
SecWiki News
C
Cisco Blogs
月光博客
月光博客
PCI Perspectives
PCI Perspectives
Cloudbric
Cloudbric
H
Hacker News: Front Page
F
Full Disclosure
TaoSecurity Blog
TaoSecurity Blog
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
S
Schneier on Security
罗磊的独立博客
S
Security Affairs
Scott Helme
Scott Helme
P
Proofpoint News Feed
GbyAI
GbyAI
Google Online Security Blog
Google Online Security Blog
The Register - Security
The Register - Security
W
WeLiveSecurity
Hugging Face - Blog
Hugging Face - Blog
博客园 - 叶小钗

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
基于netty的websocket服务优化
无所事事O_o · 2026-05-09 · via 博客园_首页

前言

  • 本文默认读者对netty有一定的使用经验,对于一些netty的基础概念不会说明
  • 本文主要探讨对于netty实现的websocket服务端推送数据的一些优化点,如果存在错误的地方欢迎指出,并且文中有说明不清晰的地方也欢迎留言讨论

开始喽

  • 通常我们使用如下的方式,创建一个基于netty的websocket服务,然后再此基础上进行业务开发。
// ai生成的demo
public class WebSocketServer {
    private final int port;
    public WebSocketServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.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 编解码器
                     pipeline.addLast(new HttpServerCodec());
                     // HTTP 消息聚合器
                     pipeline.addLast(new HttpObjectAggregator(65536));
                     // WebSocket 协议处理器
                     pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                     // 自定义 WebSocket 处理器
                     pipeline.addLast(new WebSocketFrameHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)
             .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 绑定端口并启动服务器
            ChannelFuture f = b.bind(port).sync();
            System.out.println("WebSocket 服务器启动,监听端口 " + port);

            // 等待服务器 socket 关闭
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        new WebSocketServer(port).run();
    }
}
  • 此时的websocket服务仅仅是最基础的版本,虽然可以用,但是在成本,性能,健壮性上仍存在一些优化空间,以下是一些优化点的原理介绍

快速订阅优化

如下图是客户端正常订阅websocket流程(不包括TLS)

交互流程

  • 在这个流程中客户端从建立连接到收到消息一共要经过3个rtt时间
  • 如果我们将协议升级和发送订阅请求合并,则可以减少1个rtt事件

快速订阅流程

在这里插入图片描述

  • 将协议升级和订阅请求合并后只需要2个rtt事件即可
  • 协议升级本质上是一次get请求,所以get请求中就可以携带参数
  • 握手成功后netty会触发一个userEventTriggered方法,传递HandshakeComplete事件,事件中会包含请求的uri,header,协议等数据,所以我们只需要实现一个userEventTriggered方法来处理这个事件中的数据,将订阅参数处理即可
  • 在网络延迟较大的客户端中,节省一个rtt的时间可能就有几十或者上百的毫秒节省
  • 下面这个代码是netty中WebSocketServerProtocolHandshakeHandler读接口实现,包含了上面提到的发送HandshakeComplete事件的逻辑,感兴趣的可以自己debug一下
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
        final HttpObject httpObject = (HttpObject) msg;

        if (httpObject instanceof HttpRequest) {
            final HttpRequest req = (HttpRequest) httpObject;
            isWebSocketPath = isWebSocketPath(req);
            if (!isWebSocketPath) {
                ctx.fireChannelRead(msg);
                return;
            }

            try {
                final WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                        getWebSocketLocation(ctx.pipeline(), req, serverConfig.websocketPath()),
                        serverConfig.subprotocols(), serverConfig.decoderConfig());
                final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
                final ChannelPromise localHandshakePromise = handshakePromise;
                if (handshaker == null) {
                    WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
                } else {
                    WebSocketServerProtocolHandler.setHandshaker(ctx.channel(), handshaker);
                    ctx.pipeline().remove(this);

                    final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req);
                    handshakeFuture.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) {
                            if (!future.isSuccess()) {
                                localHandshakePromise.tryFailure(future.cause());
                                ctx.fireExceptionCaught(future.cause());
                            } else {
                                localHandshakePromise.trySuccess();
                                ctx.fireUserEventTriggered(
                                        WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
                                // 发送握手成功事件
                                ctx.fireUserEventTriggered(
                                        new WebSocketServerProtocolHandler.HandshakeComplete(
                                                req.uri(), req.headers(), handshaker.selectedSubprotocol()));
                            }
                        }
                    });
                    applyHandshakeTimeout();
                }
            } finally {
                ReferenceCountUtil.release(req);
            }
        } else if (!isWebSocketPath) {
            ctx.fireChannelRead(msg);
        } else {
            ReferenceCountUtil.release(msg);
        }
    }
  • 除了上面这个快速订阅的过程可以节省1个rtt事件外,还可以和TCP Fast Open优化同时使用,可以再次节省一个rtt时间。这样可以将3个rtt压缩为1个rtt时间。
  • 不过这个优化暂时并没有应用到生产上,因此不过多介绍,感兴趣的可以自行查阅实现

数据压缩优化

压缩扩展

  • 如果推送数据较多时,通过压缩数据可以节省大量的网络流量。那么如何压缩数据就有多种选择,首先可以在业务层手动将数据压缩后发送出去。但是这种方式导致数据在业务层不可读,比如浏览器中会发现消息是乱码,无法查看。因此我们更推荐在传输层进行压缩
  • websocket协议中本身支持permessage-deflate扩展,permessage-deflate扩展主要用于将推送数据进行压缩处理。通过此扩展即可在发送后由框架自动将数据压缩
  • 具体实现只需要将WebSocketServerProtocolHandler处理器前面加上WebSocketServerCompressionHandler处理器,并且将WebSocketServerProtocolHandler处理器中的支持扩展参数配置为true,即可支持压缩功能
  • 下面是WebSocketServerProtocolHandler的其中一个构造函数,其中第3个参数allowExtensions配置为true则代表开启扩展功能
public WebSocketServerProtocolHandler(String websocketPath, String subprotocols,
                                          boolean allowExtensions, int maxFrameSize, boolean allowMaskMismatch,
                                          boolean checkStartsWith, boolean dropPongFrames) {
        this(websocketPath, subprotocols, allowExtensions, maxFrameSize, allowMaskMismatch, checkStartsWith,
             dropPongFrames, DEFAULT_HANDSHAKE_TIMEOUT_MILLIS);
    }

选择性压缩

  • 在开了压缩功能后,netty会默认将所有的BinaryWebSocketFrame和TextWebSocketFrame消息进行压缩,但是如果消息体较小,则可能额外消耗了cpu,但无法有比较好的压缩率,因此可以选择将超过一定大小的消息进行压缩
  • WebSocketExtensionFilter接口决定是否跳过扩展功能,只要实现这个接口并配置到netty中即可完成选择性压缩功能
@Component
public class CustomWebSocketExtensionFilter implements WebSocketExtensionFilter {
    @Resource
    private WebSocketConfig webSocketConfig;

    @Override
    public boolean mustSkip(WebSocketFrame frame) {
        if(frame instanceof TextWebSocketFrame || frame instanceof BinaryWebSocketFrame) {
        	// 小于一定长度的消息则跳过压缩,返回值代表是否跳过压缩
            if(frame.content().readableBytes() < webSocketConfig.getMinCompressionLength()) {
                return true;
            }
        }
        return false;
    }
}
  • 构造一个新的WebSocketServerExtensionHandler,替换掉WebSocketServerCompressionHandler即可,代码如下
@Component
public class WebSocketServerExtensionHandlerFactory {

    @Resource
    private CustomWebSocketExtensionFilter customWebSocketExtensionFilter;
    private CustomWebSocketExtensionFilterProvider customWebSocketExtensionFilterProvider = new CustomWebSocketExtensionFilterProvider();

    public WebSocketServerExtensionHandler newInstance() {
        return new WebSocketServerExtensionHandler(new PerMessageDeflateServerExtensionHandshaker(6,
                ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(),
                PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE,
                false, false,
                customWebSocketExtensionFilterProvider
        ));
    }

    private class CustomWebSocketExtensionFilterProvider implements WebSocketExtensionFilterProvider {
        @Override
        public WebSocketExtensionFilter encoderFilter() {
            return customWebSocketExtensionFilter;
        }

        @Override
        public WebSocketExtensionFilter decoderFilter() {
            return WebSocketExtensionFilter.NEVER_SKIP;
        }
    }
}
  • 需要额外关注的一点是,写入数据时如果我们手动将一条消息依次发送给多个channel,则每个channel的管道都会将消息压缩一次,但是如果通过channelGroup将消息发送给多个channel,则消息只会压缩一次

缓冲区

缓冲区打满

  • tcp连接中每个连接都有自己的读写缓冲区,如果接收方接受数据过慢,那么最终会阻塞到发送方的写缓冲区
  • 但是由于netty是事件驱动的,因此当写缓冲区打满时,消息会堆积到内存中,直到把内存打满引发fullgc问题,因此推送数据时必须处理写缓存区打满的问题
  • 这个问题处理方式比较简单,netty中提供了是否可写的方法,当不可写入的时候丢弃掉消息就不会将消息堆积到内存,但是如果不关闭channel,则已经写入缓冲区的数据无法释放,直到连接断开或接收方接受了数据
public class WritableHandler extends ChannelOutboundHandlerAdapter {
    @Resource
    private WebSocketConfig webSocketConfig;
    @Resource
    private ChannelStore channelStore;

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        if (!ctx.channel().isWritable()) {
            Channel channel = ctx.channel();
            // 释放掉消息,不进行推送
            ReferenceCountUtil.safeRelease(msg);
            if (webSocketConfig.isCloseNotWritableChannel() && ctx.channel().isOpen()) {
                // 根据配置决定是否关闭这个channel
                Monitor.counter("closeNotWritableChannel").end();
                channelStore.closeChannel(channel, CloseReason.NOT_WRITABLE);
            }

            return;
        }
        super.write(ctx, msg, promise);
    }
}

安全防护

  • 如果我们的websocket服务是对外暴露的,通常来讲需要一定的安全防护措施,避免外部用户恶意攻击我们的接口,比如如下限制
    • 相同ip的连接频率限制
    • 相同ip的最大连接数量限制
    • 同一连接接收消息频率限制
    • 同一连接最大连接时长限制
    • 违规操作过多时,禁用对方ip连接
  • 通过这些措施可以避免掉很多恶意流量,至于具体实现等后续再分享出来

结尾

如果还有其他可优化的方向,欢迎留言指点