Netty详解之八:编解码器

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<SocketChannel>() {
            @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();
    }

  • SocketMessageEncoder 是将要实现的编码器;
  • SocketMessageDecoder 是将要实现的解码器;
  • SocketMessageDispatcher 负责将消息从Netty Pipeline转发到业务层;

最后我们要定一下编码方案,由于SocketMessage的content字段是不定长的,编码时有必要加一个长度头部,最终编码方案如下:

[4字节:消息总长度]—-[4字节:userId]—-[(总长度-4)字节:content]

SocketMessageEncoder

先实现编码器:

public SocketMessageEncoder  extends MessageToByteEncoder<SocketMessage>
{
	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是基类为我们准备的数据缓冲区,我们只要把编码数据往里面写就行了,非常方便。

SocketMessageDecoder

再来看解码器,它和SocketMessageEncoder几乎是对称的:

public SocketMessageDecoder extends ByteToMessageDecoder {

	@Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
1        while (in.readableBytes() > 4) {
2            in.markReaderIndex();
3            int length = in.readInt();
4            if (in.readableBytes() < length-4) {
5                in.resetReaderIndex();
6                return;
7            }
8            int userId = in.readInt();
9            byte[] temp = new byte[length-4-4];
10           in.readBytes(temp);
11           String content = new String(temp);
12           out.add(new SocketMessage(userId,content));
        }
	}
}

解码逻辑要稍微复杂一些,因为channel接收数据受网络影响,存在不确定性:

  • 行1:参数in是channel接收的数据,按我们的编码方案,头部有4字节,所以不足4字节跳出;
    • 循环能够处理TCP粘包问题
  • 行2:记住ByteBuf的读位置,需要时恢复;
  • 行3:读取头部长度;
  • 行4:如果剩余数据不足头部指示的长度,说明消息尚未接受完整;
  • 行5:退出之前,恢复ByteBuf读位置,否则下次解码会出错;
  • 行8:按编码方案,读userId字段;
  • 行9~11:按编码方案,读content字段;
  • 行12:将字段组成数据对象放到out数组里面;

参数out是存放解码结果的地方,因为ByteToMessageDecoder并不知道解码的结果数据类型,所以类型是Object;out内的数据会沿着Pipeline继续传递。

SocketMessageDispatcher

SocketMessageDispatcher最终用来接收解码后的消息:

public SocketMessageDispatcher extends SimpleChannelInboundHandler<SocketMessage>
{
     @Override
    protected void channelRead0(ChannelHandlerContext ctx, SocketMessage msg) {
		//一般,将msg通过消息队列转发
    }
}

SocketMessageDispatcher是pipeline内inbound消息的终点,一般的实现是将消息通过消息队列传递给业务层。我们避免在pipeline内处理业务逻辑,否则可能导致Netty的eventLoop阻塞。

如果有必要,SocketMessageDispatcher可以将msg继续通过pipeline传递,当然,如果后面没有channel接受该消息的话,会被丢弃。

Netty对编解码的支持

分析ByteToMessageDecoder

一般来说,解码器的编写要复杂很多,因为读取网络数据有很多的不确定性,而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<Object> out) {
    while (in.isReadable()) {
        int outSize = out.size();
        if (outSize > 0) {
            fireChannelRead(ctx, out, outSize);
            out.clear();
            outSize = 0;
        }
        
        int oldInputLength = in.readableBytes();
        
        //此方法就是调用decode算法,做了一些安全性处理,代码不贴了
        decodeRemovalReentryProtection(ctx, in, out);

		 //如果这次解码没有获得任何消息
        if (outSize == out.size()) {
        	  //如果解码算法没有consume任何字节,结束算法
            if (oldInputLength == in.readableBytes()) {
                break;
            } else {
            //否则,重启循环,再次调用解码算法
                continue;
            }
        }
        
        //到这里,说明解码获得了消息
        //此时,如果没有consume任何字节(消息凭空出现),算法肯定有bug
        if (oldInputLength == in.readableBytes()) {
            throw new DecoderException(
                    StringUtil.simpleClassName(getClass()) +
                            ".decode() did not read anything but decoded a message.");
        }
        //如果设定了每次调用解码算法一次,结束
        if (isSingleDecode()) {
            break;
        }
    }
}

对照一下ByteToMessageDecoder和SocketMessageDecoder的代码,相信大家已经了然于胸。

解决TCP粘包

由于tcp传输的是字节流,所以在接收端,需要确定消息边界,大体有以下几种设计。

  • 固定长度:每个消息的字节长度是固定的,可以使用FixedLengthFrameDecoder,它这一步解码的结果是定长的ByteBuf,后续的解码算法不需要关注边界问题;
  • 分隔符:消息之间有分隔符,可以使用DelimiterBasedFrameDecoder;
  • 特定消息格式:有些消息格式天然就能判定边界,比如json、xml,Netty有对应的JsonObjectDecoder,XmlFrameDecoder;
  • 长度域:在消息数据前面加一个长度域,可以用LengthFieldBasedFrameDecoder来解决。

应用层协议编解码器

Netty对http,websocket,ssl这些协议,都是通过一些编解码器来支持,大家可翻阅官方文档,及Netty自带sample代码。

自定义解码器

本章示例展示的自定义解码器非常简单,但足以说明netty编解码的原理和实现方式。要实现一个复杂、高效的编解码器是有一定难度的,因为处理字节流就不是一件容易的事,需要和ByteBuf进行深度交流,下一章我们就要介绍它了。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值