Socket只能发送&接收字节数据,我们的业务层肯定期望处理预定义的数据对象,从字节数据到数据对象之间的转换叫做解码,反过来就是解码。Netty对编解码的支持非常优秀,本文以一个案例来介绍“如何编写编解码器”。
假设我们在业务层处理的数据对象定义如下:
//为了缩短代码篇幅,用public字段
public class SocketMessage {
public int userId;
public String content;
}
这个结构的设计有一定的典型性,userId代表用户,content代表消息内容,后者可进一步解释为json或其他格式。
Netty服务端相关启动代码如下:
public static void main(String[] args) throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
bootstrap.group(bossGroup, workerGroup);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new SocketMessageEncoder());
pipeline.addLast(new SocketMessageDecoder());
pipeline.addLast(new SocketMessageDispatcher());
}
});
ChannelFuture sync = bootstrap.bind(6667).sync();
sync.channel().closeFuture().sync();
}
最后我们要定一下编码方案,由于SocketMessage的content字段是不定长的,编码时有必要加一个长度头部,最终编码方案如下:
[4字节:消息总长度]—-[4字节:userId]—-[(总长度-4)字节:content]
先实现编码器:
public SocketMessageEncoder extends MessageToByteEncoder
{
protected void encode(ChannelHandlerContext ctx, SocketMessage msg, ByteBuf out) {
byte[] content = msg.content.getBytes();
int length = 4+ 4 + content.length;
out.writeInt(length);
out.writeInt(msg.userId);
out.writeBytes(content);
}
}
实现一个编解码器一般不直接实现接口ChannelHandler,Netty给我们准备了很多基类,SocketMessageEncoder继承自MessageToByteEncoder,后者从名字就可知它用于实现从数据对象到字节的编码。MessageToByteEncoder又继承自ChannelOutboundHandlerAdapter,说明这是一个outboud事件处理器,通过channel发送的数据会经过它。
上面的代码非常直观,几乎不需要解释;参数out是基类为我们准备的数据缓冲区,我们只要把编码数据往里面写就行了,非常方便。
再来看解码器,它和SocketMessageEncoder几乎是对称的:
public SocketMessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
解码逻辑要稍微复杂一些,因为channel接收数据受网络影响,存在不确定性:
参数out是存放解码结果的地方,因为ByteToMessageDecoder并不知道解码的结果数据类型,所以类型是Object;out内的数据会沿着Pipeline继续传递。
SocketMessageDispatcher最终用来接收解码后的消息:
public SocketMessageDispatcher extends SimpleChannelInboundHandler
{
@Override
protected void channelRead0(ChannelHandlerContext ctx, SocketMessage msg) {
//一般,将msg通过消息队列转发
}
}
SocketMessageDispatcher是pipeline内inbound消息的终点,一般的实现是将消息通过消息队列传递给业务层。我们避免在pipeline内处理业务逻辑,否则可能导致Netty的eventLoop阻塞。
如果有必要,SocketMessageDispatcher可以将msg继续通过pipeline传递,当然,如果后面没有channel接受该消息的话,会被丢弃。
一般来说,解码器的编写要复杂很多,因为读取网络数据有很多的不确定性,而ByteToMessageDecoder是解码器的一个通用的基类,上面的示例代码使用了它,netty很多预置的解码器也继承自它。
ByteToMessageDecoder的核心字段如下:
public ByteToMessageDecoder {
//这是一个聚合ByteBuf,用来组合channel读取的一个或多个ByteBuf(粘包是TCP常见现象)
ByteBuf cumulation;
//ByyteBuf聚合算法,这里不展开,默认实现使用CompositeByteBuf;子类可以定制
private Cumulator cumulator = MERGE_CUMULATOR;
//是否一次只解码一个消息
private boolean singleDecode;
}
ByteToMessageDecoder是典型的策略模式,它实现了消息解码算法的骨架:ByteBuf管理,调用解码算法,传递解码后消息,其中解码算法抽象为decode方法,由子类来实现。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
//创建一个列表,暂存解码后消息
CodecOutputList out = CodecOutputList.newInstance();
try {
//将新读入的ByteBuf合并至cumulation
first = cumulation == null;
cumulation = cumulator.cumulate(ctx.alloc(),
first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
//调用解码算法
callDecode(ctx, cumulation, out);
} finally {
//如果cumulation已经被解码算法读完了,可以完全释放掉
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
//尝试释放cumulation里面的已读数据,防止cumulation无限制增长
numReads = 0;
discardSomeReadBytes();
}
//将解码后的消息传入pipeline
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
//非ByteBuf不处理,沿pipeline继续传递
ctx.fireChannelRead(msg);
}
}
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List
对照一下ByteToMessageDecoder和SocketMessageDecoder的代码,相信大家已经了然于胸。
由于tcp传输的是字节流,所以在接收端,需要确定消息边界,大体有以下几种设计。
Netty对http,websocket,ssl这些协议,都是通过一些编解码器来支持,大家可翻阅官方文档,及Netty自带sample代码。
本章示例展示的自定义解码器非常简单,但足以说明netty编解码的原理和实现方式。要实现一个复杂、高效的编解码器是有一定难度的,因为处理字节流就不是一件容易的事,需要和ByteBuf进行深度交流,下一章我们就要介绍它了。