以下是 Netty 客户端的详解及代码示例,结合核心概念、开发步骤和实战代码,帮助您快速掌握 Netty 客户端开发。
一、Netty 客户端核心概念
-
Bootstrap
- 客户端的启动引导类,用于配置线程模型、通道类型和处理逻辑。
- 与服务端的
ServerBootstrap
对应,不区分 boss/worker 线程组。
-
NioSocketChannel
- 客户端的通道实现类(对应 Java NIO 的
SocketChannel
)。 - 需指定为
.channel(NioSocketChannel.class)
。
- 客户端的通道实现类(对应 Java NIO 的
-
ChannelPipeline
- 数据处理流水线,通过添加 Handler 实现编解码和业务逻辑。
- 典型链式结构:
LengthFieldDecoder → StringDecoder → 自定义Handler
。
二、客户端开发步骤详解
-
创建 EventLoopGroup
负责处理 I/O 操作和任务调度:EventLoopGroup group = new NioEventLoopGroup();
-
配置 Bootstrap
设置线程组、通道类型、处理器链:Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4)) // 解决粘包 .addLast(new StringDecoder(CharsetUtil.UTF_8)) .addLast(new EchoClientHandler()); // 业务处理器 } });
-
发起连接与重连机制
- 异步连接并监听结果:
ChannelFuture future = bootstrap.connect("127.0.0.1", 8080).addListener(f -> { if (!f.isSuccess()) { // 指数退避重连 int delay = 1 << retryCount; group.schedule(() -> connect(bootstrap, retryCount + 1), delay, SECONDS); } });
- 重连策略:首次失败后延迟 1 秒,后续按 2^n 递增。
- 异步连接并监听结果:
-
业务处理器示例
处理数据接收和异常:public class EchoClientHandler extends SimpleChannelInboundHandler<String> { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println("收到服务端响应: " + msg); } @Override public void channelActive(ChannelHandlerContext ctx) { ctx.writeAndFlush("Hello Server!"); // 连接建立后发送数据 } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
三、完整客户端代码示例
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.util.CharsetUtil;
public class NettyClient {
private static final int MAX_RETRY = 3; // 最大重连次数
public static void main(String[] args) 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 LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4))
.addLast(new StringDecoder(CharsetUtil.UTF_8))
.addLast(new EchoClientHandler());
}
});
// 发起连接(含重试逻辑)
connect(bootstrap, "127.0.0.1", 8080, MAX_RETRY);
} finally {
group.shutdownGracefully();
}
}
private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
bootstrap.connect(host, port).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else if (retry == 0) {
System.err.println("重试次数耗尽,连接失败");
} else {
int delay = 1 << (MAX_RETRY - retry);
bootstrap.config().group().schedule(
() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit.SECONDS
);
}
});
}
}
四、高级特性与注意事项
-
粘包/半包解决方案
-
LengthFieldBasedFrameDecoder
:通过长度字段标识数据包边界(需与服务端协议一致)。 -
示例配置:
.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4))
-
public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast)
-
byteOrder:表示字节流表示的数据是大端还是小端,用于长度域的读取;
-
maxFrameLength:表示发送的数据帧的最大长度。
-
lengthFieldOffset:长度字段在发送的字节数组中的下标位置。也就是说,从字节数组的这个下标开始,是表示长度的字段。
-
lengthFieldLength:长度字段的字节数。
-
lengthAdjustment:长度调整值,用于修正消息长度的计算。例如,如果整个消息包括消息头和消息体,那么这个值可以用来调整只计算消息体的长度。
-
initialBytesToStrip:接收到的数据包开始的这么多字节将被跳过。这可以用来去除消息头部分。
-
failFast:如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException,而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。
-
-
连接管理
- 心跳机制:通过
IdleStateHandler
检测空闲连接并发送心跳包。 - 资源释放:确保在关闭时调用
group.shutdownGracefully()
。
- 心跳机制:通过
-
性能调优
ChannelOption
参数:bootstrap.option(ChannelOption.SO_KEEPALIVE, true) // TCP保活 .option(ChannelOption.TCP_NODELAY, true); // 禁用Nagle算法
- I/O 线程数:根据 CPU 核心数设置
NioEventLoopGroup
线程数。
五、常见问题排查
-
连接失败不重连
检查connect()
是否添加了监听器(Listener)并实现重试逻辑。 -
数据接收不完整
确认 Pipeline 中已添加拆包器(如LengthFieldBasedFrameDecoder
)。 -
内存泄漏
- 确保 ByteBuf 引用计数释放(调用
release()
或使用SimpleChannelInboundHandler
自动释放)。 - 使用 Netty 提供的
ResourceLeakDetector
检测泄漏。
- 确保 ByteBuf 引用计数释放(调用
netty客户端示例
package com.netty.client;
import com.netty.client.config.TcpClientConfig;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
@Component
public class NettyClient {
private final TcpClientConfig config;
private Channel channel;
private CompletableFuture<String> currentFuture;
private final Object lock = new Object();
private EventLoopGroup workerGroup;
private Bootstrap bootstrap;
// 连接状态标志
private final AtomicBoolean isConnected = new AtomicBoolean(false);
private final AtomicBoolean reconnecting = new AtomicBoolean(false);
public NettyClient(TcpClientConfig config) {
this.config = config;
initializeNettyResources();
}
private void initializeNettyResources() {
workerGroup = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, config.getTimeout())
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldDecoder());
pipeline.addLast(new LengthFieldEncoder());
pipeline.addLast(new ClientHandler());
}
});
}
// @Scheduled(cron = "${unisic.automate.mes.cron:*/5 * * * * ?}")
public void connect() {
if (reconnecting.compareAndSet(false, true)) {
try {
// 检查当前连接状态
if (isConnected.get() && channel != null && channel.isActive()) {
log.info("Already connected to {}:{}", config.getHost(), config.getPort());
return;
}
// 关闭旧连接(如果存在)
if (channel != null) {
channel.close().sync();
}
// 使用同一个bootstrap创建新连接
ChannelFuture future = bootstrap.connect(config.getHost(), config.getPort());
future.addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) {
channel = f.channel();
isConnected.set(true);
log.info("Connected to {}:{}", config.getHost(), config.getPort());
} else {
log.error("Connection to {}:{} failed", config.getHost(), config.getPort(), f.cause());
scheduleReconnect();
}
});
// 等待连接完成
future.sync();
} catch (Exception e) {
log.error("Connection attempt failed", e);
scheduleReconnect();
} finally {
reconnecting.set(false);
}
}
}
private void scheduleReconnect() {
if (workerGroup != null && !workerGroup.isShutdown()) {
Integer reconnectDelay = Optional.ofNullable(config).map(TcpClientConfig::getReconnectDelay).orElse(2);
workerGroup.schedule(this::connect, reconnectDelay, TimeUnit.SECONDS);
log.warn("Scheduled reconnect in {} seconds", reconnectDelay);
}
}
public String sendAndReadSync(String message)
throws InterruptedException, TimeoutException {
synchronized (lock) {
// 检查连接状态
if (!isConnected.get() || channel == null || !channel.isActive()) {
throw new IllegalStateException("Not connected to server");
}
if (currentFuture != null) {
throw new IllegalStateException("Another request is in progress");
}
currentFuture = new CompletableFuture<>();
channel.writeAndFlush(message).addListener(f -> {
if (!f.isSuccess() && currentFuture != null) {
currentFuture.completeExceptionally(f.cause());
}
});
try {
Integer requestTimeout = Optional.ofNullable(config).map(TcpClientConfig::getRequestTimeout).orElse(3);;
return currentFuture.get(requestTimeout, TimeUnit.MILLISECONDS);
} catch (Exception e) {
currentFuture = null;
if (e instanceof TimeoutException) {
throw (TimeoutException) e;
}
throw new RuntimeException("Request failed", e);
} finally {
currentFuture = null;
}
}
}
public void shutdown() {
if (channel != null) {
channel.close().awaitUninterruptibly();
isConnected.set(false);
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
// 获取当前连接状态
public boolean isConnected() {
return isConnected.get() && channel != null && channel.isActive();
}
static class LengthFieldDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) return;
in.markReaderIndex();
int length = in.readIntLE();
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
ByteBuf payload = in.readBytes(length);
out.add(payload.toString(StandardCharsets.UTF_8));
}
}
static class LengthFieldEncoder extends MessageToByteEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {
byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
out.writeIntLE(bytes.length);
out.writeBytes(bytes);
}
}
class ClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
synchronized (lock) {
if (currentFuture != null) {
currentFuture.complete(msg);
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("Channel exception: ", cause);
synchronized (lock) {
if (currentFuture != null) {
currentFuture.completeExceptionally(cause);
}
}
isConnected.set(false);
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
log.warn("Connection lost to {}:{}", config.getHost(), config.getPort());
isConnected.set(false);
if (!reconnecting.get()) {
scheduleReconnect();
}
}
}
}
netty多线程
package com.netty.client;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.base.exception.ServiceException;
import com.netty.client.config.TcpClientConfig;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
public class NettyTcpClient {
private final GenericObjectPool<Channel> connectionPool;
private final TcpClientConfig config;
private final NioEventLoopGroup workerGroup;
private final Timer timer = new HashedWheelTimer();
// 线程安全的请求ID生成器
private final AtomicInteger requestIdCounter = new AtomicInteger(0);
public NettyTcpClient(TcpClientConfig config) {
this.config = config;
this.workerGroup = new NioEventLoopGroup(config.getThreadCount());
this.connectionPool = new GenericObjectPool<>(
new ChannelFactory(config, workerGroup),
buildPoolConfig(config.getPool())
);
preWarmPool();
}
private void preWarmPool() {
int minIdle = config.getPool().getMinIdle();
for (int i = 0; i < minIdle; i++) {
workerGroup.submit(() -> {
try {
connectionPool.addObject();
} catch (Exception e) {
log.warn("Pre-warm connection failed", e);
}
});
}
}
private GenericObjectPoolConfig<Channel> buildPoolConfig(TcpClientConfig.Pool poolConfig) {
GenericObjectPoolConfig<Channel> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(poolConfig.getMaxTotal());
config.setMaxIdle(poolConfig.getMaxIdle());
config.setMinIdle(poolConfig.getMinIdle());
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
return config;
}
public String sendSync(String message) {
Channel channel = null;
try {
channel = connectionPool.borrowObject();
// 确保消息有唯一的TransactionID
JSONObject jsonObject = ensureTransactionId(message);
String correlationId = jsonObject.getJSONObject("Header").getString("TransactionID");
// 获取处理器并准备响应
ClientHandler handler = channel.pipeline().get(ClientHandler.class);
if (handler == null) {
throw new ServiceException("ClientHandler not found in pipeline");
}
CompletableFuture<String> future = handler.prepareResponse(correlationId);
ChannelFuture writeFuture = channel.writeAndFlush(message);
// 等待消息发送完成
if (!writeFuture.await(config.getWriteTimeoutMillis())) {
throw new ServiceException("Write operation timed out");
}
if (!writeFuture.isSuccess()) {
throw new ServiceException("Write failed", writeFuture.cause());
}
return future.get(config.getRequestTimeoutMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new ServiceException("Request timed out");
} catch (Exception e) {
throw new ServiceException("Netty communication failed");
} finally {
if (channel != null) {
try {
connectionPool.returnObject(channel);
} catch (Exception e) {
log.warn("Return channel to pool failed", e);
invalidateChannel(channel);
}
}
}
}
private JSONObject ensureTransactionId(String message) {
JSONObject jsonObject = JSON.parseObject(message);
JSONObject header = jsonObject.getJSONObject("Header");
if (!header.containsKey("TransactionID")) {
header.put("TransactionID", generateTransactionId());
return jsonObject;
}
return jsonObject;
}
private String generateTransactionId() {
return String.format("%s-%d-%d",
config.getClientId(),
System.currentTimeMillis(),
requestIdCounter.incrementAndGet());
}
private void invalidateChannel(Channel channel) {
try {
connectionPool.invalidateObject(channel);
} catch (Exception ex) {
log.warn("Invalidate channel failed", ex);
}
}
@PreDestroy
public void shutdown() {
try {
connectionPool.close();
} catch (Exception e) {
log.warn("Connection pool close failed", e);
}
if (workerGroup != null) {
workerGroup.shutdownGracefully(
config.getShutdownQuietPeriodMillis(),
config.getShutdownTimeoutMillis(),
TimeUnit.MILLISECONDS
).syncUninterruptibly();
}
timer.stop();
}
private static class ChannelFactory extends BasePooledObjectFactory<Channel> {
private final Bootstrap bootstrap;
private final TcpClientConfig config;
public ChannelFactory(TcpClientConfig config, EventLoopGroup workerGroup) {
this.config = config;
this.bootstrap = new Bootstrap()
.group(workerGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, config.getConnectTimeoutMillis())
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.remoteAddress(config.getHost(), config.getPort());
}
@Override
public Channel create() throws Exception {
CompletableFuture<Channel> channelFuture = new CompletableFuture<>();
ClientHandler handler = new ClientHandler();
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldDecoder());
pipeline.addLast(new LengthFieldEncoder());
pipeline.addLast(handler);
}
});
ChannelFuture future = bootstrap.connect();
future.addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) {
channelFuture.complete(f.channel());
} else {
channelFuture.completeExceptionally(f.cause());
}
});
return channelFuture.get(config.getConnectTimeoutMillis(), TimeUnit.MILLISECONDS);
}
@Override
public PooledObject<Channel> wrap(Channel channel) {
return new DefaultPooledObject<>(channel);
}
@Override
public boolean validateObject(PooledObject<Channel> p) {
Channel channel = p.getObject();
return channel.isActive() && channel.isWritable();
}
@Override
public void destroyObject(PooledObject<Channel> p) {
Channel channel = p.getObject();
if (channel.isActive()) {
channel.close().awaitUninterruptibly();
}
}
}
private static class LengthFieldDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) return;
in.markReaderIndex();
int length = in.readIntLE();
// 长度校验
if (length <= 0 || length > 10 * 1024 * 1024) { // 10MB max
in.resetReaderIndex();
throw new CorruptedFrameException("Invalid frame length: " + length);
}
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
ByteBuf payload = in.readBytes(length);
out.add(payload.toString(StandardCharsets.UTF_8));
}
}
private static class LengthFieldEncoder extends MessageToByteEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {
byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
out.writeIntLE(bytes.length);
out.writeBytes(bytes);
}
}
private class ClientHandler extends SimpleChannelInboundHandler<String> {
private final Map<String, CompletableFuture<String>> pendingRequests = new ConcurrentHashMap<>();
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
try {
JSONObject jsonObject = JSON.parseObject(msg);
JSONObject header = jsonObject.getJSONObject("Header");
String correlationId = header.getString("TransactionID");
CompletableFuture<String> future = pendingRequests.remove(correlationId);
if (future != null) {
future.complete(msg);
} else {
log.warn("Received response for unknown TransactionID: {}", correlationId);
}
} catch (Exception e) {
log.error("Message processing failed: {}", e.getMessage());
log.debug("Original message: {}", msg);
}
}
public CompletableFuture<String> prepareResponse(String correlationId) {
CompletableFuture<String> future = new CompletableFuture<>();
pendingRequests.put(correlationId, future);
// 使用Netty的HashedWheelTimer进行超时管理
Timeout timeout = timer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
if (pendingRequests.remove(correlationId) != null) {
future.completeExceptionally(new TimeoutException("Response timeout"));
}
}
}, config.getRequestTimeoutMillis(), TimeUnit.MILLISECONDS);
future.whenComplete((r, t) -> {
timeout.cancel();
pendingRequests.remove(correlationId);
});
return future;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("Channel error: {}", cause.getMessage(), cause);
// 失败所有pending请求
pendingRequests.values().forEach(f ->
f.completeExceptionally(new ServiceException("Channel closed due to error")));
pendingRequests.clear();
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
log.warn("Channel inactive: {}", ctx.channel());
// 失败所有pending请求
pendingRequests.values().forEach(f ->
f.completeExceptionally(new ServiceException("Channel closed")));
pendingRequests.clear();
}
}
}