常规的网络通信程序引用的是TCP/IP协议的传输层接口,对于面向连接的TCP协议,数据的传输形式是流(Stream),其特点是传输的数据无消息边界。套接字就像管道的两端,数据从一端流入,从另一端流出,数据的流动是连续的。
TCP 协议提供了发送和接收数据的接口,发送操作每次发送一串字节,接收操作每次接收一串字节,TCP协议保证发送数据的顺序和接收数据的顺序相同,比如发送数据为0123456789(10个字节),只要套接字不出现错误,另一端接收到的数据一定是0123456789(10个字节)。但接收数据的操作却不一定要和发送数据的操作一一对应。这主要是因为 TCP 协议是字节流形式的、无消息边界的协议,受网络传输中的不确定因素的影响,不能保证每次send和recv操作能够一一对应。上述情况可以形象表示为:
情况1: | 发送: | 012 | 3456789 | 接收: | 012 | 3456789 | |||
情况2: | 发送: | 012 | 3456789 | 接收: | 0123456789 | ||||
情况3: | 发送: | 012 | 3456789 | 接收: | 01 | 234 | 56789 | ||
...... | |||||||||
情况N: | 发送: | 012 | 3456789 | 接收: | ...... |
有些时候传输的数据具有一定的协议语义,为了保证接收方不出现解析错误, 编程时必须要考虑消息边界问题, 否则就可能会出现命令格式解析错误、丢失命令等后果。实际应用中,解决 TCP 协议消息边界问题的方式有三种:
第一种方式是发送固定长度的消息。该方法类似于CPU的RISC架构,每条指令的长度相同,这种处理方式最简单,但需要仔细设计指令体系。
第二种方式是各种指令具有不同的头标识,具体到每种指令的长度则固定,收到指令后根据头部标识判断指令的总长度。该方法类似于CPU的CISC架构,各种指令的长度不同,但每种指令的长度是已知固定的。
第三种方式是将消息长度在头部进行标识,相当于对第二种方式的优化,将整条指令划分为头部和数据部分,头部长度固定,包含指令类型和数据部分长度,解析后一次读取数据部分长度。这种方式降低了指令解析的复杂度。
以第三种方式为例,分别使用java.nio.ByteBuffer和io.netty.buffer.ByteBuf演示指令解析的过程。
使用java.nio.ByteBuffer演示网络流的读取、解析过程:
// 读取数据.
int cnt = channel.read(src);
if(cnt < 0) {
// ...
} elseif (cnt == 0) {
// ...
}
// 解析指令循环,一次读取可能会读到多条指令
ByteBuffer buf = src.duplicate();
buf.flip();
while (buf.remaining() >= PDU_HEAD_LEN) {
int pduLen = buf.getInt(buf.position());
if (buf.remaining() >= pduLen) {
// 循环处理指令的各个字段
// ...
}
// buf中剩下的数据为下一个指令的数据,下一次循环将继续解析,直到数据不足一条指令
}
// buf中剩下的数据不足一条指令,等待下次读取数据后再次处理
// 将数据转移到src的头部,供读取添加,每次都需执行
bufTmp.clear();
bufTmp.put(buf);
bufTmp.flip();
src.clear();
src.put(bufTmp);
使用io.netty.buffer.ByteBuf演示网络流的读取、解析过程:
// 读取数据.
src.clear();
int cnt = channel.read(src);
if(cnt < 0) {
// ...
} elseif (cnt == 0) {
// ...
}
src.flip();
buf.writeBytes(src);
// 解析指令循环,一次读取可能会读到多条指令
while (buf.readableBytes() >= PDU_HEAD_LEN) {
int pduLen = buf.getInt(buf.readerIndex());
if (buf.readableBytes() >= pduLen) {
// 循环处理指令的各个字段
// ...
}
// buf中剩下的数据为下一个指令的数据,下一次循环将继续解析,直到数据不足一条指令
}
// buf中剩下的数据不足一条指令,等待下次读取数据后再次处理
// 根据必要进行buf的压缩,根据系统内存进行权衡,可以每500次执行一次压缩
if (buf.writerIndex() > ALLOW_CAP) {
buf.writeBytes(buf);
}
结论:从上述代码看,使用io.netty.buffer.ByteBuf进行网络流的读取、解析,不仅代码简洁、易懂而且高效。最好的代码不是使用了丰富的注释,而是不需过多注释就通俗易懂。这其中的一个重要原因在于io.netty.buffer.ByteBuf将readerIndex和writerIndex分开标记,并使用了准确的接口命名带来的。而java.nio.ByteBuffer的接口设计中,get和put则使用了同一个位置标识position,用limit标识有效数据的末尾,如果是交替读写,其代码反复调用duplicate和flip将让人感到莫名其妙,或者使用getXXX(index)或者put(index, value)就更让人摸不着头脑。另外put和get的命名也让人奇怪,为什么不使用write和read更直接的单词命名呢?同样是开源工程,显然netty这一小步,给网络编程的进化带来了一大步。Netty带来的优势远不至于此,后续将花时间逐步介绍。在此祝愿会有更多更好的像netty这样的开源工程涌现出来!
转载于:https://blog.51cto.com/abelli/1353584