6.1 缓冲和TCP

作为程序员,在使用TCP套接字时需要记住的最重要一点是:

不能假设在连接的一端将数据写入输出流和在另一端从输入流读出数据之间有任何一致性。

尤其是在发送端由单个输出流的write()方法传输的数据,可能会通过另一端的多个输入流的read()方法来获取;而一个read()方法可能会返回多个write()方法传输的数据。

为了展示这种情况,考虑如下程序:

figure_0165_0233

其中,圆点代表了设置缓冲区数据的代码,但不包含对out.write()方法的调用。在本节的讨论中,“in”代表接收端Socket的InputStream,“out”代表发送端Socket的OutputStream。

这个TCP连接向接收端传输8000字节。在连接的接收端,这8000字节的分组方式取决于连接两端out.write()方法和in.read()方法的调用时间差,以及提供给in.read()方法的缓冲区大小。

我们可以认为TCP连接上发送的所有字节序列在某一瞬间被分成了3个FIFO队列:

1.SendQ:在发送端底层实现中缓存的字节,这些字节已经写入输出流,但还没在接收端主机上成功接收。

2.RecvQ:在接收端底层实现中缓存的字节,等待分配到接收程序—即从输入流中读取。

3.Delivered:接收者从输入流已经读取到的字节。

调用out.write()方法将向SendQ追加字节。TCP协议负责将字节按顺序从SendQ移动到RecvQ。有重要的一点需要明确,这个转移过程无法由用户程序控制或直接观察到,并且在块中(chunk)发生,这些块的大小在一定程度上独立于传递给write()方法的缓冲区大小。

接收程序从Socket的InputStream读取数据时,字节就从RecvQ移动到Delivered中,而转移的块的大小依赖于RecvQ中的数据量和传递给read()方法缓冲区大小。

图6-2展示了上例中3次调用out.write()方法后,另一端调用in.read()方法前,以上3个队列的可能状态。不同的阴影效果分别代表了上文中3次调用write()方法传输的不同数据。

图6-2描述的发送端主机的netstat输出瞬时状态中,会包含类似于以下一行的内容:

figure_0166_0234

在接收端主机,netstat会显示:

figure_0166_0235

figure_0166_0236

图 6-2 3次调用write()方法后3个队列的状态

现在假设接收者调用read()方法时使用的缓冲区数组大小为2000字节,read()调用则将把等待分配队列(RecvQ)中的1500字节全部移动到数组中,返回值为1500。注意,这些数据包括了第一次和第二次调用write()方法时传输的字节。再过一段时间,当TCP连接传完更多数据后,这三部分的状态可能如图6-3所示。

figure_0166_0237

图 6-3 第一次调用read()方法后

如果接收者现在调用read()方法时使用4000字节的缓冲区数组,将有很多字节从等待分配队列(RecvQ)转移到已分配队列(Delivered)中。这包括第二次调用write()方法时剩下的1500字节加上第三次调用write()的前2500字节。此时队列的状态如图6-4所示。

figure_0167_0238

图 6-4 另一次调用read()后

下次调用read()方法返回的字节数,取决于缓冲区数组的大小,以及发送方套接字/TCP实现通过网络向接收方实现传输数据的时机。数据从SendQ到RecvQ缓冲区的移动过程对应用程序协议的设计有重要的指导性。我们已经遇到过需要对使用带内(in-band)分隔符成帧(见第3.3节),并通过Socket来接收的消息进行解析的情况。在下面的章节中,我们将考虑另外两个更加微妙的情况。

[1]Postel,John,“Transmission Control Protocol,”Internet Request for Comments 793,September 1981.

[2]Comer,Douglas E.,and Stevens,David L.,Internetworking with TCP/IP,Volume Ⅱ:Design,Implementation,and Internals(third edition),Prentice-Hall,1999.

[3]Wright,Gary R.,and Stevens,W.Richard,TCP/IP Illustrated,Volume 2:The Implementation,Addison Wesley,1995.