前言
前面几章系统性的介绍过了netty的源码与原理,本章笔者将通过netty搭建一个企业级的WebSocket服务,充分展现作为一款优秀的底层通讯框架,netty的有点与普适性。
WebSocket服务
这是一个WebSocket服务示例
java
@Service
public class PushServer implements ApplicationListener<ContextRefreshedEvent> {
private static final int ON_EVENT_COUNT = 2;
@Autowired
private TextWebSocketFrameHandler bizHandler;
@Autowired
private SslContextService sslContextService;
private Runnable runnable;
private int onEventCount = 0;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
onEventCount++;
if (onEventCount == ON_EVENT_COUNT) {
start();
}
}
public void start() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(10);
final SslContext sslContext = sslContextService.load();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.SO_LINGER,1)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(sslContext.newHandler(ch.alloc()));
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws", true));
pipeline.addLast(new IdleStateHandler(60, -1, -1));
}
pipeline.addLast(bizHandler);
}
});
ChannelFuture f = bootstrap.bind(new InetSocketAddress(1111));
f.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("push server bind on port: 1111");
} else {
log.error("push server bind error: ", future.cause());
}
});
runnable = () -> {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
};
Thread shutdownHook = new Thread(runnable, "关闭推送服务");
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
public void shutdown() {
if (runnable != null) {
runnable.run();
runnable = null;
}
}
}
要搭建websocket服务,首先需要明白什么是websocket服务。如果缺少概念,可以参考什么是WebWocket服务这篇文章,这里就不再多做介绍。
PushServer这个类交由spring管理,监听容器刷新事件,我们知道spring项目启动时,spring容器和mvc容器会先后启动
ini
public void onApplicationEvent(ContextRefreshedEvent event) {
onEventCount++;
if (onEventCount == ON_EVENT_COUNT) {
start();
}
}
这段代码保证了start()方法在执行时,外部环境已经准备好。
start()方法
- 首先创建boss线程和worker线程
- 接着通过
sslContextService.load()方法加载支持wss协议需要的SslContext对象,这个对象是netty用来完成tsl/ssl加密所用的handler。详见这篇文章 - 之后为
bootstrap添加各种属性和设置参数,尤其是为连接添加各种handler,这个我们接下来会说。 - 接着通过
ChannelFuture来监听bind()方法是否执行成功 - 最后对runnable定义,优雅关闭线程组并将其提交到shutdown Hook(jvm的关闭的钩子),使得程序退出时能够优雅关闭线程池
shutdown()方法
执行runnable,暴露关闭WebSocket的能力
WebSocket服务需要的handler
new HttpServerCodec()
根据 RFC6455,WebSocket 握手建立在 HTTP/1.1 协议之上,因此服务端必须能够处理 HTTP 请求。
HttpServerCodec 是 Netty 提供的 HTTP 编解码器:
scala
public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
它内部组合了 HttpRequestDecoder 和 HttpResponseEncoder,能同时完成:
- 请求解码 :把 TCP 字节流解析成多个
HttpObject(HttpRequest、HttpContent等) - 响应编码 :将
HttpResponse转换成字节流回写给客户端
HTTP 的消息边界通过协议规则划分,例如:
- 请求行与头部使用
\r\n - 头部结束使用
\r\n\r\n - body 长度由
Content-Length或 chunked 分块大小决定
因此 HttpServerCodec 输出的是多个 HttpObject,而不是一个完整的 HTTP 请求。
new HttpObjectAggregator(64 * 1024)
HttpObjectAggregator 用于将 HttpServerCodec 拆分出的多个 HttpObject 自动聚合成一个完整的 FullHttpRequest 或 FullHttpResponse。内部工作流程为:
- 接收到
HttpRequest→ 标记消息开始 - 多次接收到
HttpContent→ 累积 body 数据 - 接收到
LastHttpContent→ 标记消息结束 - 合成一个完整的
FullHttpRequest并向下传播 - 同时会检查聚合后的消息大小是否超过限制(如 64KB),避免大包攻击
因此,使用它之后,业务 handler 可以直接处理一个完整的 HTTP 请求对象,而无需手动处理多段内容。
new WebSocketServerProtocolHandler("/ws", true)
WebSocketServerProtocolHandler 是 Netty WebSocket 服务端的核心 Handler。
它的主要职责包括:
-
处理 HTTP → WebSocket 的握手升级流程
-
在握手成功后,自动切换 pipeline 到 WebSocket Frame 模式
-
自动管理并处理 WebSocket 协议层细节:
ping / pongclose framefragmented frame(分片帧)
-
避免业务代码直接接触 WebSocket 协议细节
一句话总结:
它把"HTTP 协议世界"和"WebSocket Frame 世界"隔离开来,让业务只需要关心 WebSocket 消息本身。
handlerAdded:WebSocket pipeline 的"预部署阶段"
scss
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
ChannelPipeline cp = ctx.pipeline();
if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
ctx.pipeline().addBefore(
ctx.name(),
WebSocketServerProtocolHandshakeHandler.class.getName(),
new WebSocketServerProtocolHandshakeHandler(...)
);
}
if (cp.get(Utf8FrameValidator.class) == null) {
ctx.pipeline().addBefore(
ctx.name(),
Utf8FrameValidator.class.getName(),
new Utf8FrameValidator()
);
}
}
这一步做了什么?
当 WebSocketServerProtocolHandler 被加入 pipeline 时,Netty 会立即回调 handlerAdded()。
在这个阶段,它会 为当前 Channel 提前"布置"两个关键 Handler:
1️⃣ WebSocketServerProtocolHandshakeHandler
-
位置 :位于
WebSocketServerProtocolHandler之前 -
职责:
-
接收并解析浏览器(或客户端)发来的 HTTP Upgrade 请求
-
执行 WebSocket 握手
-
在握手成功后:
- 动态替换 HTTP 相关 handler
- 切换 pipeline 到 WebSocket Frame 编解码体系
-
可以理解为:
这是 HTTP 世界的"最后一站"
2️⃣ Utf8FrameValidator
-
位置:位于 handshake handler 之前
-
职责:
- 校验
TextWebSocketFrame的 payload 是否符合 UTF-8 编码 - 支持 fragmented frame(跨 frame 校验)
- 校验
这是 WebSocket 协议的强制要求:
RFC 6455 §5.6
WebSocket 文本消息必须是合法 UTF-8,否则必须关闭连接
这也是为什么 Netty 把 UTF-8 校验内置,而不是交给业务代码。
WebSocketServerProtocolHandshakeHandler:真正的升级入口
java
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
final FullHttpRequest req = (FullHttpRequest) msg;
注意这里的前提:
- 能拿到
FullHttpRequest - 说明 HttpServerCodec + HttpObjectAggregator 已经完成了解码与聚合
握手流程整体分为四步
① 构造 HandshakerFactory
ini
final WebSocketServerHandshakerFactory wsFactory =
new WebSocketServerHandshakerFactory(
getWebSocketLocation(ctx.pipeline(), req, websocketPath),
subprotocols,
allowExtensions,
maxFramePayloadSize,
allowMaskMismatch
);
WebSocket URL 拼装逻辑
typescript
private static String getWebSocketLocation(...) {
String protocol = "ws";
if (cp.get(SslHandler.class) != null) {
protocol = "wss";
}
return protocol + "://" + host + path;
}
这里非常关键的一点是:
- 是否使用
ws还是wss,并不是由 URL 决定 - 而是由 pipeline 中是否存在
SslHandler决定
这也是为什么 TLS 一定要放在 pipeline 最前面
② 根据请求版本创建 Handshaker
ini
CharSequence version = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_VERSION);
csharp
if (version.equals("13")) {
return new WebSocketServerHandshaker13(...);
}
- Netty 会根据请求头中的
Sec-WebSocket-Version - 创建对应版本的
WebSocketServerHandshaker - 目前主流版本是 V13(RFC 6455)
不同版本的区别主要体现在:
- 握手响应格式
- Key 的计算方式
- Frame 编解码细节
③ 执行握手:真正的"协议切换"
ini
final ChannelFuture handshakeFuture =
handshaker.handshake(ctx.channel(), req);
这是整个流程中 最关键的一步。
handshake 内部做了哪些事?
1️⃣ 构造 HTTP 101 响应
ini
FullHttpResponse response = newHandshakeResponse(req, responseHeaders);
即:
makefile
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: xxx
2️⃣ 清理 HTTP 阶段的 Handler
csharp
p.remove(HttpObjectAggregator.class);
p.remove(HttpContentCompressor.class);
原因很简单:
WebSocket 是 Frame 协议,不再需要 HTTP 聚合与压缩
3️⃣ 替换 HTTP Codec → WebSocket Codec
less
p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
这一步完成后:
- pipeline 从"HTTP 消息模型"
- 切换为"WebSocket Frame 模型"
4️⃣ 回写响应,并在回调中完成最终切换
arduino
channel.writeAndFlush(response).addListener(future -> {
if (future.isSuccess()) {
p.remove(encoderName);
promise.setSuccess();
} else {
promise.setFailure(future.cause());
}
});
⚠️ 为什么一定要在回调里 remove HttpServerCodec?
因为:
response本身 仍然需要 HTTP encoder 编码- 如果提前移除,会导致响应无法写出
这是确保协议平滑切换的关键细节。
④ 善后与事件通知
ini
ctx.fireUserEventTriggered(
ServerHandshakeStateEvent.HANDSHAKE_COMPLETE
);
Netty 会通过 UserEvent 的方式通知:
- 握手已完成
- WebSocket 连接已正式建立
业务层如果关心握手完成时机,可以监听该事件。
new IdleStateHandler(60, -1, -1)
IdleStateHandler 的作用与设计背景
IdleStateHandler 是 Netty 提供的一个 用于检测连接空闲状态的 Handler ,本质上是一个 基于时间的心跳/存活检测机制。
我们知道,TCP 协议本身并不具备应用层的心跳与存活检测能力。在以下场景中:
- 客户端断网
- 客户端进程宕机
- 浏览器页面被关闭
- 移动网络切换(IP 变化)
服务器端往往 无法立即感知连接已经失效 ,此时连接可能长期停留在 ESTABLISHED 或 CLOSE_WAIT 状态。
如果不做处理,随着时间推移,将会导致:
- 大量 无效连接堆积
CLOSE_WAIT / TIME_WAIT数量持续增长- 无效连接占用 线程、内存、FD(文件描述符)等系统资源
- 最终引发性能下降甚至服务不可用
因此,服务端必须具备检测"客户端是否还活着"的能力。
常见的心跳检测方案
在实际工程中,常见的做法是:
-
客户端
-
每隔 X 秒发送一次心跳
- WebSocket 场景:
PingWebSocketFrame - 普通 TCP 场景:自定义心跳包
- WebSocket 场景:
-
-
服务端
-
每次读到数据时,重置连接的 idle 计时器
-
如果在
readIdleTime时间内 没有收到任何数据- 判定客户端已失活
- 主动关闭连接,释放资源
-
IdleStateHandler 的核心职责
IdleStateHandler 的职责正是为上述机制提供 通用、可靠的基础能力,其主要功能包括:
-
记录连接的读 / 写时间
- 最近一次
channelRead - 最近一次
write
- 最近一次
-
向 EventLoop(EventExecutor)提交定时任务
-
定期检查连接是否发生:
- 读空闲(
READER_IDLE) - 写空闲(
WRITER_IDLE) - 读写空闲(
ALL_IDLE)
- 读空闲(
-
-
在超时发生时触发事件通知
-
通过触发
IdleStateEvent -
回调下游 Handler 的
userEventTriggered()方法 -
通常由业务 Handler 来决定:
- 发送心跳
- 或直接关闭连接
-
总结
本文通过一段 WebSocket 服务端的完整示例代码 ,展示了如何构建一个 企业级 WebSocket 服务的基础框架。如果读者从本系列第一篇文章一路阅读至此,那么本章的内容理解起来应该并不存在太大难度。至于剩余的业务处理逻辑,则需要结合具体业务场景进行具体分析;只要能够真正读懂 Netty 的源码实现,相信在实际落地过程中都可以做到游刃有余。
至此,Netty 系列文章也正式告一段落。在整个系列中,我们依次介绍了 Netty 的启动流程、线程模型、连接接入流程、网络数据包的生命周期 ,以及 Netty 的几个核心组件与关键设计思想 ,整体上帮助读者建立起了对 Netty 相对系统、深入的认知。
对于本文未能展开说明的部分,也正是 Netty 更为丰富和精妙的所在,期待各位读者在实际项目和源码阅读中持续深入探索