1. 介绍
Netty 是 Java 中一个高性能的网络框架,本文将实现一个自定义的编解码器来加深对 Netty 工作流程的理解。
客户端与服务器的分工是这样的:
- 客户端:接收服务器发送的消息,并且给服务器返回接收成功的消息。
- 服务器:
- 当客户端连接建立后,打印客户端已上线,并且开启一个定时任务,每隔 3s 给客户端发送服务端当前的时间。
- 当接收到客户端的消息时,打印到控制台上。
- 当客户端连接断开后,打印客户端已离线。
2. 关注点
2.1 粘包/拆包 问题的解决方案
TCP 传输可能会出现 粘包/拆包 的问题,在 Netty 中可以使用 LengthFieldBasedFrameDecoder
来解决。对 粘包/拆包 问题不熟悉的可以看这篇文章——Netty——TCP 粘包/拆包问题。
2.2 自定义消息协议
本次自定义编解码器需要 自定义消息的协议,一般来说,业务消息包含四部分:
magic
:魔数,用于验证消息的有效性,是一个固定的整数,和 .class 文件中的魔数类似。code
:业务码,表示消息的类型,不同的业务消息对应不同的业务处理。len
:消息的长度。data
:消息的内容。
为了简化消息协议,本次自定义的消息协议只包含 len
和 data
两个字段。
2.3 @Sharable
2.3.1 是什么
在 Netty 中,ChannelHandler
是处理 I/O 事件的核心组件。一般而言,每个 Channel
都会有自己独立的 ChannelPipeline
,ChannelPipeline
里包含了一系列的 ChannelHandler
。对 Netty 的核心组件不熟悉的可以看这篇文章——Netty——核心组件。
若一个 ChannelHandler
被标记为 @Sharable
,那就意味着这个 ChannelHandler
实例能够被多个 Channel
的 ChannelPipeline
同时使用,而不用为每个 Channel
都创建新的 ChannelHandler
实例,这样可以节省系统资源。
2.3.2 使用场景
- 无状态处理:当
ChannelHandler
不维护任何与特定Channel
相关的状态时,就能使用@Sharable
注解。例如,对所有Channel
都应用相同的 日志记录、数据转换 等操作。 - 资源共享:要是
ChannelHandler
需要共享某些资源(如 数据库连接池、本地缓存 等),使用@Sharable
注解可以保证这些资源在多个Channel
间有效共享。
2.3.3 注意事项
- 状态管理:被标记为
@Sharable
的ChannelHandler
必须是无状态的,或者其状态可以安全地在多个Channel
间共享。若ChannelHandler
维护了与特定Channel
相关的状态,就可能会出现数据混乱和线程安全问题。 - 线程安全:由于
@Sharable
的ChannelHandler
可能会被多个线程同时访问,所以 要保证其所有方法都是线程安全的。 - 特殊情况:Netty 要求
ByteToMessageDecoder
(将字节流转化成业务消息的解码器) 的实现类不能标注@Sharable
,因为ByteToMessageDecoder
往往会维护一些内部状态,在解码过程中,它可能需要追踪已经读取的字节数、当前解析的位置或者中间的解析结果等,以便继续处理后续的数据。
3. 代码实现
3.1 消息协议
3.1.1 消息类 Msg
/**
* 自定义消息协议
*/
public class Msg {
/**
* 消息的长度
*/
private int len;
/**
* 消息的内容
*/
private byte[] data;
public Msg() {}
public Msg(int len, byte[] data) {
this.len = len;
this.data = data;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
3.1.2 消息编码器 MsgEncoder
/**
* 消息的编码器,将消息从 Msg 类型 编码成 字节流
* 消息编码是无状态的,可共享
*/
@ChannelHandler.Sharable
public class MsgEncoder extends MessageToByteEncoder<Msg> {
@Override
protected void encode(ChannelHandlerContext ctx, Msg msg, ByteBuf out) throws Exception {
// 先写入消息的长度
out.writeInt(msg.getLen());
// 然后再写入消息内容
out.writeBytes(msg.getData());
}
}
3.1.3 消息解码器 MsgDecoder
/**
* 消息的解码器,将消息从 字节流 解码成 Msg 类型
* 消息解码器不能共享
*/
public class MsgDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 检查可读字节数是否足够读取 消息的长度,如果不够,则直接返回,等待更多数据
if (in.readableBytes() < 4) {
return;
}
// 标记读指针,便于回滚
in.markReaderIndex();
// 读取消息的长度
int len = in.readInt();
// 检查可读字节数是否足够读取 消息的内容,如果不够,则重置读指针,等待更多数据
if (in.readableBytes() < len) {
in.resetReaderIndex();
return;
}
// 读取消息的内容
byte[] data = new byte[len];
in.readBytes(data);
// 将消息封装成 Msg 类型,交给 Pipeline 中的下一个 Handler 处理
out.add(new Msg(len, data));
}
}
3.2 服务器
3.2.1 服务器类 Server
/**
* 支持收发 Msg 类型的消息的服务器
*/
public class Server {
public static void main(String[] args) {
final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
final EventLoopGroup workerGroup = new NioEventLoopGroup();
final MsgEncoder encoder = new MsgEncoder();
final ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(4096, 0, 4))
.addLast(new MsgDecoder())
.addLast(encoder)
.addLast(new ServerHandler());
}
});
// 当 JVM 关闭时关闭 BossGroup 和 WorkerGroup
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}));
bootstrap.bind(8888);
}
}
3.2.2 服务器的处理器 ServerHandler
/**
* 服务器的处理器
* 每个 Channel (客户端) 对应一个处理器,无法共享
*/
public class ServerHandler extends SimpleChannelInboundHandler<Msg> {
/**
* 时间的格式化器
*/
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 用于固定时间间隔发送消息的单线程池
*/
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
@Override
protected void channelRead0(ChannelHandlerContext ctx, Msg msg) throws Exception {
System.out.println("客户端[" + ctx.channel().remoteAddress() + "]说:" + new String(msg.getData()));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端[" + ctx.channel().remoteAddress() + "]已上线");
// 建立连接 1s 后,每隔 3s 给客户端发送一次消息
executor.scheduleAtFixedRate(() -> {
String content = "当前时间是 " + FORMATTER.format(LocalDateTime.now());
byte[] data = content.getBytes();
int len = data.length;
ctx.writeAndFlush(new Msg(len, data));
}, 1, 3, TimeUnit.SECONDS);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端[" + ctx.channel().remoteAddress() + "]已离线");
}
}
3.3 客户端
3.3.1 客户端类 Client
/**
* 支持收发 Msg 类型的消息的客户端
*/
public class Client {
public static void main(String[] args) {
final EventLoopGroup group = new NioEventLoopGroup();
final MsgEncoder encoder = new MsgEncoder();
final ClientHandler clientHandler = new ClientHandler();
final Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(4096, 0, 4))
.addLast(new MsgDecoder())
.addLast(encoder)
.addLast(clientHandler);
}
});
// 当 JVM 关闭时关闭 Group
Runtime.getRuntime().addShutdownHook(new Thread(group::shutdownGracefully));
bootstrap.connect("127.0.0.1", 8888);
}
}
3.3.2 客户端的处理器 ClientHandler
/**
* 客户端的处理器
* 消息处理是无状态的,可共享
*/
@ChannelHandler.Sharable
public class ClientHandler extends SimpleChannelInboundHandler<Msg> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Msg msg) throws Exception {
System.out.println("服务器说:" + new String(msg.getData()));
String content = "接收成功";
byte[] data = content.getBytes();
int len = data.length;
ctx.writeAndFlush(new Msg(len, data));
}
}
3.4 测试
3.4.1 服务器
客户端[/127.0.0.1:49514]已上线
客户端[/127.0.0.1:49518]已上线
客户端[/127.0.0.1:49514]说:接收成功
客户端[/127.0.0.1:49518]说:接收成功
客户端[/127.0.0.1:49514]说:接收成功
客户端[/127.0.0.1:49518]说:接收成功
客户端[/127.0.0.1:49514]说:接收成功
客户端[/127.0.0.1:49518]说:接收成功
客户端[/127.0.0.1:49514]已离线
客户端[/127.0.0.1:49518]已离线
3.4.2 客户端 [/127.0.0.1:49514]
服务器说:当前时间是 11:00:49
服务器说:当前时间是 11:00:52
服务器说:当前时间是 11:00:55
3.4.3 客户端 [/127.0.0.1:49518]
服务器说:当前时间是 11:00:49
服务器说:当前时间是 11:00:52
服务器说:当前时间是 11:00:55
4. 总结
使用 Netty 框架,可以简单地开发出一个高性能的网络服务器。本次实现主要有三个重点:
- 使用
LengthFieldBasedFrameDecoder
处理 粘包/拆包 问题。 - 自定义消息协议。
- 使用
@Sharable
注解标注可共享的 Handler。