我们都知道,tcp通讯属于流传输,对于上面承载的业务协议栈是不做分包处理的,所以大量客户端给服务器发送数据,就会有黏包现象,所以必须分包,反之,服务器给客户端发数据,也会黏包。
netty提供了很多decoder用来分包,目前个人觉得效率最高,最好的方式还是LengthFieldBasedFrameDecoder,没有之一。
很多人刚开始做开发经验不足,按照教科书上的指点,使用了分隔符作为分包机制,其实这种方式效率非常低,比较“愚蠢”,不管是服务器还是客户端,因为你作为接收端,不知道tcp流里面什么时候分隔符到来,所以必须一个字节一个字节去和分隔符做对比,有的人说,那有什么,netty都做好了我们只管用就好了,既然你说这种方式不好,为什么netty还有这种方式呢?这就是杠精附体,根本不懂所以然的那些人。
不信可以参考一下DelimiterBasedFrameDecoder里面的decode实现,看看netty是不是用了for循环,是不是用了indexOf在一个字节一个字节和分隔符做对比,有遍历,有对比,就消耗CPU时间,消耗服务器性能,在今天网络如此发达,不只是一个客户端,可是成千上万的客户端。
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
if (lineBasedDecoder != null) {
return lineBasedDecoder.decode(ctx, buffer);
}
// Try all delimiters and choose the delimiter which yields the shortest frame.
int minFrameLength = Integer.MAX_VALUE;
ByteBuf minDelim = null;
for (ByteBuf delim: delimiters) {
int frameLength = indexOf(buffer, delim);
if (frameLength >= 0 && frameLength < minFrameLength) {
minFrameLength = frameLength;
minDelim = delim;
}
}
if (minDelim != null) {
int minDelimLength = minDelim.capacity();
ByteBuf frame;
if (discardingTooLongFrame) {
// We've just finished discarding a very large frame.
// Go back to the initial state.
discardingTooLongFrame = false;
buffer.skipBytes(minFrameLength + minDelimLength);
int tooLongFrameLength = this.tooLongFrameLength;
this.tooLongFrameLength = 0;
if (!failFast) {
fail(tooLongFrameLength);
}
return null;
}
if (minFrameLength > maxFrameLength) {
// Discard read frame.
buffer.skipBytes(minFrameLength + minDelimLength);
fail(minFrameLength);
return null;
}
if (stripDelimiter) {
frame = buffer.readRetainedSlice(minFrameLength);
buffer.skipBytes(minDelimLength);
} else {
frame = buffer.readRetainedSlice(minFrameLength + minDelimLength);
}
return frame;
} else {
if (!discardingTooLongFrame) {
if (buffer.readableBytes() > maxFrameLength) {
// Discard the content of the buffer until a delimiter is found.
tooLongFrameLength = buffer.readableBytes();
buffer.skipBytes(buffer.readableBytes());
discardingTooLongFrame = true;
if (failFast) {
fail(tooLongFrameLength);
}
}
} else {
// Still discarding the buffer since a delimiter is not found.
tooLongFrameLength += buffer.readableBytes();
buffer.skipBytes(buffer.readableBytes());
}
return null;
}
}
既然分隔符这么多问题,为什么netty还保留有分隔符这种分包机制呢,原因就是这种分包方式非常适合那种每次通讯就十几个字节的这种业务,问题是现在很多tcp业务数据量动辄至少都是几个KB,还有几MB的那种,还用分隔符这种方式效率极低。
再看看LengthFieldBasedFrameDecoder分包是怎么实现的,他是使用了包头+包体这种分包方式,tcp接收端,无论服务端还是客户端,收到固定长度的包头,就从里面拿出了包体的长度,之后直接从netty的ByteBuf里面取出包体的长度内容即可,因为接收端是可以预知到包有多长的,什么时候读取完毕,分隔符分包,你并不知道接下来什么时候分隔符到来,只能一个字节一个字节去对比。
那有人说,就算你用这种包头+包体的方式,那发送端发送的数据包格式错误怎么办,会不会导致接收端出问题,那是肯定的了,计算机是机器,不是人,你要故意给它投毒,那它只能死,所以既然是通讯协议,双方必须按照统一规范来做,不是随意就去发数据。
举例,比如要设计一种客户端和服务端的tcp通讯协议,包头2个字节的业务代码,4个字节的长度,4个字节就有4GB的容量了,足够任何tcp业务使用了吧,应该没有人一次发送4GB的tcp包。无论是客户端还是服务器,每次接收数据,先接收固定的6个字节,解析出包体长度,之后再从tcp流中读取指定的长度即可,有人就会有疑问,那不需要CRC校验么,其实不需要,因为TCP的可靠性,不会像UART那样会受到电磁干扰导致数据出错。