TCP是以流的方式进行字节传输的,所以无法知道数据的边界,也就意味着无法区分出一串数据,比如一段二进制由于没有边界,客户端收到以后随意截取就会导致数据错乱,不是期望收到的数据。
下面一段代码是一个基本的解码器的案例:
public class CustomDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 4) return; // 检查是否有足够的字节读取长度字段 【1】
int length = in.readInt(); // 读取4字节的长度字段 【2】
if (in.readableBytes() < length) { //【3】
in.resetReaderIndex(); // 数据不足,重置读取位置 【4】
return;
}
ByteBuf frame = in.readBytes(length); // 读取消息体【5】
out.add(frame); // 输出到下游 【6】
}
}
代码详解:
【1】in.readableBytes() < 4
这段代码的效果是从TCP流中读取4个字节(想象从TCP流的最开始位置),这4个字节是用来记录后面Body数据的长度的,这样就可以确定后面数据的边界,确定后面的数据边界以后,通过偏移量就可以得到下一个Head,通过Head又可以得到后面Body的长度,
以此类推,这样一来字节流就形成了边界,就可以组装成正确的数据了;那为什么是4而不是其他的呢?其实也可以是其他的(比如2、8字节等),因为4字节可以记录的长度在java中是32位,已经可以得到足够大的整数了,几乎可以满足所有的场景,这也是一个折中的选择
如果用2字节表示的长度又太小,后面的Body内容实际可能很长,如果用8字节,那可能没必要,因为后面的Body大多数场景实际的数据没有那么长,如果用8的话就会造成浪费,完全没必要,所以这就是为什么要用4的原因。
【2】in.readInt()
这段代码的效果是读取4字节(我们规定的是4字节表示头),可以读取到一个整数表示后面Body的长度,连续读取该长度就可以得到一个正常的数据。
【3】in.readableBytes() < length
通过Head头我们知道后面Body应该读取多少长度的数据,但是由于TCP基于流的,可能会将一个很长的消息进行拆分传输,导致没有收到完整的的数据,也就是常常说的半包问题,
因此需要做一个判断,等下次数据满足Head要读取的长度后再读取。
【4】in.resetReaderIndex()
将索引恢复到上次的位置,不然下次读取就会错乱,原因在于调用in.readableBytes()
会导致索引先后移动,竟然这次都没有读取成功,肯定要恢复。
【5】in.readBytes(length)
终于可以读取指定长度(来自Head)的字节数量了,得到的就是实际的Body内容,也就是发送者单次实际发送的数据。
If you've published on Dev.to, read this: a limited-time token giveaway now live for Dev.to contributors to celebrate our authors' impact in Web3! Don’t miss this opportunity here (no gas fees). – Dev.to Team