Java Netty 实战指南:从 NIO 线程模型到 TCP 编解码和心跳机制

简介

Netty 是 Java 生态里很常见的高性能网络通信框架。

它基于 NIO 和事件驱动模型,把原生 NIO 里比较繁琐的 SelectorChannelByteBuffer、事件循环、半包处理、资源释放这些细节封装起来。

一句话概括:

text 复制代码
Netty 适合开发高并发、长连接、自定义协议、网关、RPC、IM、游戏服务、物联网接入这类网络应用。

常见应用场景:

场景 说明
RPC 框架 服务之间走自定义 TCP 协议
IM 聊天 长连接、消息推送、在线状态
游戏服务器 低延迟、长连接、频繁消息交互
网关 TCP 网关、HTTP 网关、协议转换
物联网 设备接入、心跳、指令下发
WebSocket 实时通知、行情推送、在线客服

Netty 本身不是业务框架,它更像网络通信底座。HTTP、WebSocket、私有 TCP 协议、二进制协议,都可以在它上面实现。

为什么不直接用 Java Socket

最早的阻塞 Socket 写法大概是:

text 复制代码
一个连接
  -> 一个线程
      -> 阻塞读取
          -> 处理业务

连接少时,这种写法很直观。

连接多了以后,线程数量、上下文切换、阻塞等待、异常处理都会变复杂。

Java NIO 解决了阻塞问题,但原生 API 使用起来偏底层:

text 复制代码
Selector
SelectionKey
SocketChannel
ByteBuffer
OP_ACCEPT
OP_READ
OP_WRITE

Netty 在 NIO 之上做了一层工程化封装:

text 复制代码
连接接入
  -> EventLoop 处理 I/O 事件
      -> ChannelPipeline 编排处理器
          -> ChannelHandler 写业务逻辑

这样业务代码通常只需要关注消息怎么解码、怎么处理、怎么响应。

Maven 依赖

普通 Maven 项目可以引入 netty-all

xml 复制代码
<properties>
    <netty.version>4.1.136.Final</netty.version>
</properties>

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>${netty.version}</version>
</dependency>

netty-all 方便学习和 Demo。

生产项目也可以按模块引入,比如:

xml 复制代码
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport</artifactId>
    <version>${netty.version}</version>
</dependency>

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-codec</artifactId>
    <version>${netty.version}</version>
</dependency>

如果项目已经通过 Spring Cloud Gateway、Reactor Netty、gRPC 等框架间接使用 Netty,版本通常交给上层框架管理。

核心组件

Netty 的核心组件不算多,但关系要理清楚。

组件 作用
Bootstrap 客户端启动器
ServerBootstrap 服务端启动器
EventLoopGroup 事件循环线程组
EventLoop 事件循环线程,处理 I/O 事件和任务
Channel 一个网络连接的抽象
ChannelPipeline 处理器链
ChannelHandler 处理入站、出站事件
ByteBuf Netty 自己的字节缓冲区
ChannelFuture 异步操作结果

服务端整体流程:

text 复制代码
ServerBootstrap
  -> bossGroup 接收连接
      -> workerGroup 处理读写
          -> Channel
              -> ChannelPipeline
                  -> Decoder
                  -> Encoder
                  -> BusinessHandler

bossGroup 负责接收连接。

workerGroup 负责处理已经建立连接的读写事件。

每个连接都会对应一个 Channel,每个 Channel 都有自己的 ChannelPipeline

EventLoop 线程模型

Netty 很重要的一个设计是:

text 复制代码
一个 Channel 注册到一个 EventLoop
一个 EventLoop 绑定一个线程
一个 EventLoop 可以管理多个 Channel

这带来一个好处:同一个连接上的 I/O 事件通常由同一个线程处理,很多连接内状态不用到处加锁。

但也带来一个要求:ChannelHandler 里不适合执行耗时操作。

比如下面这些操作都容易卡住 EventLoop:

text 复制代码
慢 SQL
远程 HTTP 调用
大文件读写
复杂 CPU 计算
Thread.sleep

EventLoop 被卡住后,它负责的多个连接都会受影响。

耗时任务可以交给业务线程池处理,再把结果写回 Channel。

第一个 TCP Demo:按行收发消息

下面实现一个最小 TCP 服务:

text 复制代码
客户端发送一行文本
服务端收到后回一行文本

协议约定:

text 复制代码
每条消息以 \n 结尾

这种协议可以用 LineBasedFrameDecoder 解决 TCP 粘包和半包问题。

服务端 Handler

java 复制代码
package com.example.netty.line;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class LineServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("客户端连接:" + ctx.channel().remoteAddress());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        String content = msg.trim();
        System.out.println("服务端收到:" + content);

        String response = "服务端已收到:" + content + "\n";
        ctx.writeAndFlush(response);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        System.out.println("客户端断开:" + ctx.channel().remoteAddress());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

这里继承 SimpleChannelInboundHandler<String>

它会在 channelRead0 执行完后自动释放入站消息,适合处理已经解码后的对象。

服务端启动类

java 复制代码
package com.example.netty.line;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class LineNettyServer {

    private final int port;

    public LineNettyServer(int port) {
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new LineBasedFrameDecoder(1024))
                                    .addLast(new StringDecoder(CharsetUtil.UTF_8))
                                    .addLast(new StringEncoder(CharsetUtil.UTF_8))
                                    .addLast(new LineServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(port).sync();
            System.out.println("Netty TCP 服务启动,端口:" + port);

            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new LineNettyServer(8888).start();
    }
}

Pipeline 顺序很关键:

text 复制代码
ByteBuf
  -> LineBasedFrameDecoder 按换行切包
      -> StringDecoder 转成 String
          -> LineServerHandler 处理业务

出站时方向相反:

text 复制代码
String
  -> StringEncoder 转成 ByteBuf
      -> 写回客户端

客户端 Handler

java 复制代码
package com.example.netty.line;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class LineClientHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush("Hello Netty\n");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        System.out.println("客户端收到:" + msg.trim());
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端启动类

java 复制代码
package com.example.netty.line;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class LineNettyClient {

    private final String host;
    private final int port;

    public LineNettyClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new LineBasedFrameDecoder(1024))
                                    .addLast(new StringDecoder(CharsetUtil.UTF_8))
                                    .addLast(new StringEncoder(CharsetUtil.UTF_8))
                                    .addLast(new LineClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect(host, port).sync();
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new LineNettyClient("127.0.0.1", 8888).start();
    }
}

运行顺序:

text 复制代码
先启动 LineNettyServer
再启动 LineNettyClient

服务端输出:

text 复制代码
客户端连接:/127.0.0.1:xxxxx
服务端收到:Hello Netty
客户端断开:/127.0.0.1:xxxxx

客户端输出:

text 复制代码
客户端收到:服务端已收到:Hello Netty

TCP 粘包和半包

TCP 是流协议,没有天然的消息边界。

应用层发送两条消息:

text 复制代码
hello
world

接收端可能看到:

text 复制代码
helloworld

也可能看到:

text 复制代码
hel
lowor
ld

这就是常说的粘包和半包。

解决办法是在应用协议里定义消息边界。

方案 Netty 解码器 适合场景
按换行切分 LineBasedFrameDecoder 命令行协议、简单文本协议
按分隔符切分 DelimiterBasedFrameDecoder 自定义分隔符文本协议
固定长度 FixedLengthFrameDecoder 每条消息长度固定
长度字段 LengthFieldBasedFrameDecoder RPC、自定义二进制协议

实际项目里,自定义协议最常见的是长度字段方案。

长度字段协议 Demo

定义一个简单协议:

text 复制代码
+----------------+----------------+
| 4 字节消息长度  | 消息内容 UTF-8  |
+----------------+----------------+

比如消息内容是 ping,长度就是 4

Pipeline 可以这样配置:

java 复制代码
ch.pipeline()
        .addLast(new LengthFieldBasedFrameDecoder(
                1024 * 1024,
                0,
                4,
                0,
                4
        ))
        .addLast(new LengthFieldPrepender(4))
        .addLast(new StringDecoder(CharsetUtil.UTF_8))
        .addLast(new StringEncoder(CharsetUtil.UTF_8))
        .addLast(new LengthFieldServerHandler());

参数含义:

参数 说明
maxFrameLength 1024 * 1024 单条消息最大 1MB
lengthFieldOffset 0 长度字段从第 0 个字节开始
lengthFieldLength 4 长度字段占 4 字节
lengthAdjustment 0 长度值只表示消息体长度
initialBytesToStrip 4 解码后去掉长度字段,只保留消息体

LengthFieldPrepender(4) 会在出站消息前自动加 4 字节长度字段。

服务端 Handler:

java 复制代码
package com.example.netty.length;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class LengthFieldServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        System.out.println("收到长度字段消息:" + msg);
        ctx.writeAndFlush("pong: " + msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

完整服务端:

java 复制代码
package com.example.netty.length;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class LengthFieldNettyServer {

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

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4))
                                    .addLast(new LengthFieldPrepender(4))
                                    .addLast(new StringDecoder(CharsetUtil.UTF_8))
                                    .addLast(new StringEncoder(CharsetUtil.UTF_8))
                                    .addLast(new LengthFieldServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(8899).sync();
            System.out.println("长度字段协议服务启动,端口:8899");
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

这个 Demo 适合继续扩展成:

text 复制代码
4 字节长度
1 字节消息类型
8 字节请求 ID
N 字节 JSON / Protobuf 内容

RPC 和内部 TCP 协议通常都会有类似的包头设计。

ByteBuf 怎么理解

ByteBuf 是 Netty 的字节缓冲区。

它比 JDK ByteBuffer 更适合网络编程。

对比点 ByteBuffer ByteBuf
读写指针 一个 position,需要 flip readerIndex 和 writerIndex 分开
扩容 使用不方便 可以自动扩容
池化 原生支持弱 Netty 有池化分配器
引用计数 有引用计数

一个 ByteBuf 大概可以理解成:

text 复制代码
0                readerIndex        writerIndex              capacity
| 已读区域        | 可读区域           | 可写区域                 |

读取:

java 复制代码
int value = byteBuf.readInt();

写入:

java 复制代码
byteBuf.writeInt(100);
byteBuf.writeBytes("hello".getBytes(StandardCharsets.UTF_8));

ByteBuf 释放

Netty 里很多 ByteBuf 是引用计数对象。

如果直接继承 ChannelInboundHandlerAdapter 并处理入站 ByteBuf,通常需要释放。

java 复制代码
package com.example.netty.buffer;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

public class RawByteBufHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            ByteBuf byteBuf = (ByteBuf) msg;
            System.out.println("收到字节数:" + byteBuf.readableBytes());
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
}

如果使用 SimpleChannelInboundHandler<T>,入站消息默认会在处理后自动释放。

java 复制代码
public class TextHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        System.out.println(msg);
    }
}

常见判断:

写法 释放方式
ChannelInboundHandlerAdapter 直接消费 ByteBuf 手动 release
ctx.fireChannelRead(msg) 继续传下去 当前 Handler 不释放
ctx.writeAndFlush(msg) 把原消息写出去 写出流程完成后释放
SimpleChannelInboundHandler<T> 默认自动释放

资源释放是 Netty 开发里很重要的一块。

ChannelFuture:异步结果

Netty 的 I/O 操作都是异步的。

java 复制代码
ChannelFuture future = ctx.writeAndFlush("hello");

这行代码返回时,数据不一定已经真正写到 socket。

如果要在写完后关闭连接,需要加监听器:

java 复制代码
ctx.writeAndFlush("bye\n")
        .addListener(future -> {
            if (future.isSuccess()) {
                future.channel().close();
            } else {
                future.cause().printStackTrace();
                future.channel().close();
            }
        });

也可以使用内置监听器:

java 复制代码
ctx.writeAndFlush("bye\n")
        .addListener(ChannelFutureListener.CLOSE);

心跳和空闲检测

长连接场景里,需要识别空闲连接。

Netty 提供 IdleStateHandler

java 复制代码
ch.pipeline()
        .addLast(new IdleStateHandler(60, 0, 0))
        .addLast(new HeartbeatServerHandler());

含义:

参数 含义
第一个 60 60 秒没有读事件,触发读空闲
第二个 0 不检测写空闲
第三个 0 不检测读写空闲

处理空闲事件:

java 复制代码
package com.example.netty.heartbeat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent event && event.state() == IdleState.READER_IDLE) {
            System.out.println("连接读空闲,关闭:" + ctx.channel().remoteAddress());
            ctx.close();
            return;
        }

        ctx.fireUserEventTriggered(evt);
    }
}

实际协议里通常会定义心跳消息:

text 复制代码
PING
PONG

客户端定时发送 PING,服务端回复 PONG。如果一段时间没有收到任何数据,服务端关闭连接。

耗时业务怎么处理

业务 Handler 里遇到慢任务,可以交给业务线程池。

java 复制代码
package com.example.netty.biz;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BusinessHandler extends SimpleChannelInboundHandler<String> {

    private final ExecutorService businessExecutor = Executors.newFixedThreadPool(16);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        businessExecutor.submit(() -> {
            String result = handleBusiness(msg);
            ctx.executor().execute(() -> ctx.writeAndFlush(result + "\n"));
        });
    }

    private String handleBusiness(String msg) {
        return "result: " + msg;
    }
}

业务线程池处理完成后,通过 ctx.executor().execute(...) 回到当前 Channel 对应的 EventLoop,再写回结果。

这样可以减少并发问题,也能避免业务线程直接和 I/O 线程混在一起。

HTTP Server Demo

Netty 也能直接写 HTTP 服务。

Pipeline:

java 复制代码
ch.pipeline()
        .addLast(new HttpServerCodec())
        .addLast(new HttpObjectAggregator(1024 * 1024))
        .addLast(new SimpleHttpServerHandler());

Handler:

java 复制代码
package com.example.netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.CharsetUtil;

public class SimpleHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
        ByteBuf content = Unpooled.copiedBuffer("Hello Netty HTTP", CharsetUtil.UTF_8);

        FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                content
        );

        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}

启动类和 TCP 服务类似,只是 Pipeline 换成 HTTP 编解码器。

如果目标是普通 Web API,Spring MVC、WebFlux、Spring Boot 内置服务器更省心。

Netty HTTP 更适合网关、协议转换、定制代理、底层通信组件这类场景。

Spring Boot 管理 Netty 生命周期

在 Spring Boot 项目里,可以把 Netty 服务做成一个 Spring Bean,让应用启动时打开端口,关闭时释放资源。

java 复制代码
package com.example.netty.spring;

import com.example.netty.line.LineServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class NettyServerLifecycle {

    private EventLoopGroup bossGroup;

    private EventLoopGroup workerGroup;

    private Channel serverChannel;

    @PostConstruct
    public void start() throws InterruptedException {
        bossGroup = new NioEventLoopGroup(1);
        workerGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline()
                                .addLast(new LineBasedFrameDecoder(1024))
                                .addLast(new StringDecoder(CharsetUtil.UTF_8))
                                .addLast(new StringEncoder(CharsetUtil.UTF_8))
                                .addLast(new LineServerHandler());
                    }
                });

        serverChannel = bootstrap.bind(8888).sync().channel();
        System.out.println("Netty 服务已启动,端口:8888");
    }

    @PreDestroy
    public void stop() {
        if (serverChannel != null) {
            serverChannel.close();
        }
        if (workerGroup != null) {
            workerGroup.shutdownGracefully();
        }
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
        }
    }
}

这种方式适合在 Spring Boot 应用里额外暴露一个 TCP 端口,比如设备接入端口、内部协议端口。

常见 ChannelOption

服务端启动时经常会看到 optionchildOption

java 复制代码
bootstrap.option(ChannelOption.SO_BACKLOG, 1024)
        .childOption(ChannelOption.SO_KEEPALIVE, true)
        .childOption(ChannelOption.TCP_NODELAY, true);

区别如下:

方法 作用对象
option 服务端 ServerChannel,比如监听端口
childOption 已接入的客户端连接 Channel

常见选项:

选项 说明
SO_BACKLOG 服务端等待连接队列大小
SO_KEEPALIVE TCP keepalive
TCP_NODELAY 关闭 Nagle 算法,降低小包延迟
SO_REUSEADDR 地址复用
CONNECT_TIMEOUT_MILLIS 客户端连接超时时间

Handler 共享

无状态 Handler 可以标记 @ChannelHandler.Sharable,多个 Channel 共用同一个实例。

java 复制代码
package com.example.netty.handler;

import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

@ChannelHandler.Sharable
public class LoggingTextHandler extends SimpleChannelInboundHandler<String> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        System.out.println("收到:" + msg);
        ctx.fireChannelRead(msg);
    }
}

如果 Handler 内部有连接级别状态,比如当前用户、登录状态、未完成请求 Map,就不适合共享。

Netty 适合和不适合的场景

场景 是否适合
自定义 TCP 协议 适合
长连接推送 适合
网关和代理 适合
RPC 通信层 适合
普通后台 CRUD 接口 通常 Spring MVC 更合适
简单 HTTP JSON API 通常 Spring Boot 内置服务器更省心

Netty 的优势在底层通信和协议处理。

如果需求只是写几个 REST API,直接使用 Spring MVC 或 WebFlux 就够了。

常见问题

服务端收不到完整消息

常见原因是 TCP 没有消息边界。

处理方式是定义应用层协议,并配置对应解码器。

java 复制代码
new LineBasedFrameDecoder(1024)
new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4)

Handler 里业务处理很慢

EventLoop 被慢任务阻塞后,多个连接都会受影响。

处理方式是把慢任务交给业务线程池,处理完成后再回到 EventLoop 写响应。

内存持续上涨

常见原因是 ByteBuf 没有释放。

处理方式:

场景 建议
手动消费 ByteBuf 使用 ReferenceCountUtil.release(msg)
解码后只处理对象 使用 SimpleChannelInboundHandler
继续向后传递 使用 ctx.fireChannelRead(msg)
打开泄漏检测 设置 -Dio.netty.leakDetection.level=advanced

客户端断开后服务端还持有状态

可以在 channelInactive 中清理连接状态。

java 复制代码
@Override
public void channelInactive(ChannelHandlerContext ctx) {
    onlineChannels.remove(ctx.channel().id());
}

用户在线表、设备连接表、请求等待表,都适合在断开时清理。

write 后马上 close 导致消息没发完

writeAndFlush 是异步操作。

写完后关闭连接,要等 ChannelFuture 完成:

java 复制代码
ctx.writeAndFlush(response)
        .addListener(ChannelFutureListener.CLOSE);

实践建议

场景 建议
学习和 Demo 使用 netty-all
生产项目 按模块引入或交给上层框架管理版本
TCP 协议 先设计消息边界
二进制协议 优先考虑长度字段
长连接 加心跳和空闲检测
EventLoop 避免执行慢任务
ByteBuf 明确释放责任
Handler 有状态不共享,无状态可共享
关闭应用 调用 shutdownGracefully()
性能排查 关注 EventLoop 阻塞、内存泄漏、消息堆积

小结

Netty 的核心是异步事件驱动网络编程。

EventLoopGroup 管线程,Channel 表示连接,ChannelPipeline 编排处理链,ChannelHandler 承载业务处理,ByteBuf 负责高效读写字节数据。

写 Netty 程序时,最关键的不是背 API,而是抓住几个工程点:协议边界、线程模型、资源释放、心跳检测、异步结果处理。

把这些点处理好,Netty 就能成为稳定的 TCP、WebSocket、RPC、网关和设备接入通信底座。

相关推荐
CoderYanger6 小时前
A.每日一题:1967题+1331题
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
言乐67 小时前
Python实现基本搜索引擎
java·linux·开发语言·python·搜索引擎
萧瑟余晖7 小时前
JDK 15 新特性详解
java
L-影7 小时前
springboot启动流程
java·spring boot·spring
ch.ju7 小时前
Java Programming Chapter 4——lambda expression
java·开发语言
布朗克1687 小时前
Go 入门到精通-18-文件操作
java·数据库·golang
罗超驿8 小时前
10.Java对象比较完全指南:从equals到Comparable与Comparator
java·开发语言
LiaoWL1238 小时前
【JUC合集-01】JUC并发编程复习总结
java
Tisfy9 小时前
LeetCode 1979.找出数组的最大公约数:模拟(附手动gcd)
java·数学·算法·leetcode·题解·最大公约数