最后终于来到了大块头TCP协议,为了给应用层提供可靠的传输服务,tcp协议设计了各种机制以实现丢包、重发、乱序、链路传输错误等传输过程中可能出现的错误。
1. TCP协议概述
我们首先来看一下TCP协议的首部,它将给收发两端提供怎样的信息:
与UDP一样,TCP报头的前8个字节也是源和目的端的端口号。<源ip地址,源端口号,目的ip地址,目的端口号>(即一个socket pair)确定一条tcp连接。
序列号用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的第一个数据字节。反过来,确认序列号是表示TCP发端期望从TCP收端收到的下一个字节(好像说得不是很清楚,后面再说)。
首部长度给出首部中32bit字的数目,跟IP首部一样,TCP最多有60字节的首部。
接下来是6个标志比特,它们中的多个可以被同时设置为1:
URG:紧急指针有效,与后面的紧急指针结合起来
ACK:确认序号有效
PSH:接收方尽快将这个报文段交给应用层
RST:重建连接
SYN:同步序号用来发起一个连接
FIN:发端完成发送任务,将要关闭连接
检验和的计算方法和UDP中的检验和一样,也要加上伪首部,也要填充奇数字节,与UDP不同的是,TCP强制要求计算检验和,而UDP的检验和是可选的。
窗口大小表明接收端当前的接收能力,以字节为单位,16位窗口限制了最大值为65535字节,在选项字段中,有一个窗口刻度选项,允许这个值按比例放大。
紧急指针是一个正的偏移量,和序号中的值相加表示紧急指针最后一个字节的序号。
选项字段可以包括最长报文大小(MSS),这是最常见的可选字段。每个连接方通常都在通信的第一个报文段中指明这个选项,表明本端所能接收的最大长度的报文段;还有上面我们提到的窗口扩大选项以及时间戳选项,我们将在后面看到时间戳选项的作用。
这里摘录一段话来描述TCP协议:“TCP可以表述为一个没有选择确认或否认的滑动窗口协议。我们说TCP缺少选择确认是因为TCP首部中的确认序列号表示发方已经成功收到字节,但还不包含确认序号所指的字节。当前还无法对数据流中选定的部分进行确认。例如,如果1~1024字节已经成功收到,下一个报文段中包含序号从2049~3072的字节,收端并不能确认这个新的报文段。它所能做的就是发回一个确认序号为1025的ACK。它也无法对一个报文进行否认。例如,如果收到包含1025~2048字节的报文段,但它的检验和错,TCP收端所能做的就是发回一个确认序号为1025的ACK。”这段话也好很地解释了前面提到的确认序列号的问题。
2. 连接的建立与终止
接下来就是著名的tcp建立连接的三次握手了。用时间序列图来表示最清楚不过了:
tcp连接的其中一方发起主动连接,它填写目的端口和源端口号,初始化序列号,设置SYN位,并设置了mss选项,将该TCP段发给连接的另一方。另一方收到tcp段后,与主动连接方做了同样的事情,同时携带ACK,把对主动连接方的初始序号加1填入确认序列号字段,发送给主动连接方。主动连接方向被动连接方发去一个ack,连接由此建立。
图中还演示了连接关闭的过程,终止一个连接需要四次握手。任何一方在最后的发送数据段中设置FIN位来终止这个方向的连接。当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传输,也就是说,不再会有数据从那个方向传来,但它仍然能够发送数据,收到FIN方回复一个ack。
由图我们还可以看到,SYN和FIN各占用了一个序号。
图中的端口A、B还让我们想起一个问题,如果不存在用户进程在监听端口B(即端口B没有打开)时,主机A将会收到什么呢?在UDP中,发送端将收到一份ICMP端口不可达报文,那么在TCP连接中呢?TCP使用复位,即在回应发送端的TCP段中设置了RST位,携带ack主动发送端的确认序列号,自己的序列号为0.发送端收到这样的tcp段后,即知道连接被拒绝了。
那如果主机B根本就不存在呢?这时主机A将过一段时间再发送一个SYN到主机B请求连接,一般建立一个连接的最长时间限制为75秒。
如果一方已经关闭或导演终止而另一方却不知道,我们将这样的TCP连接称为半打开的。比方说在主机A(客户端)上运行telnet程序,通过它和主机B(服务器)连接,由于突然停电,主机A没有向主机B的telnet端口发送FIN消息,结果主机B就以为与主机A的连接还在。主机A重新启动后再次与主机B连接将会启动新的服务器程序,这样将会导致主机B上产生很多半打开的TCP连接。如果是服务器主机B突然当掉了,而客户端A并不知道,它继续向主机B发送数据,假如主机B很快恢复了,然而先前的所有连接信息都丢失了,收到来自主机A的消息时,它回复以RST消息(相当于没有端口在监听)。
TCP支持同时打开或同时关系,不过同时打开将经历4次握手。
下面是TCP的状态变迁图(很难画,于是决定截图):
状态图中比较重要的一点就是,主动关闭方在收到对方的对自己FIN的ACK以及对方的FIN后,进入一个状态叫TIME_WAIT,这种状态也称为2MSL等待状态。每个TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。对于一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL,以防这个ACK丢失的时候,可以重发一个ACK(对应另一端收不到ACK重发最后的FIN消息)。这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务的IP地址和端口号)不能再被使用,这个连接只能在2MSL结束后才能被使用。(书中说事实上很多TCP实现比这个要求还要更严格,在2MSL等待期间,插口中使用的本地端口在默认情况下不能再被使用。)
前面提到TCP的选项,下图是这些选项的格式:
nop选项用于填充其他选项使其以4个字节对齐,如下面这个选项:
<mss 512, nop, wscale 0, nop, nop, timestamp 146647 0>
第一个nop填充窗口扩大选项,第二三个nop填充时间戳选项。
3. TCP的交互数据流(经受延时的确认、nagle算法)、成块数据流(慢启动)
建立完连接后,两台主机开始进行数据的传输。传输的数据可以分成两种,一种是交互式数据的传输,如通过telnet发送指令;一种是大量数据的传输,如通过ftp传输文件。TCP显然需要同时能够处理这两种类型的数据,但使用的算法有所不同。
我们在telnet客户端键入一个字符时,字符被传输到服务器,服务器发来对该字节的确认;接着服务器发回这个字节,以回显到客户端的屏幕上,客户端又要回复一个确认。也就是说,我们在一个远程终端上发送一个字符,竟然产生了4个报文段的交互。
经受时延的确认是这样一种机制:接收到数据的一端接收完数据后并不立刻回复一个确认,而是设定一个定时器,以便在定时器复位之前将要传输的数据携带这个确认一起发送过去;当然如果定时器复位时还没有数据要传输,就会单独发送一个ack。这就是所谓的“经受时延的确认”。这种方法对上面提到的过程产生影响是,服务器把回显的数据跟确认一起发送回客户端,4个报文段变成了3个报文段。
为了发送一个字节,我们产生了一个41字节长的分组,这在广域网上,可能会增加拥塞出现的可能。Nagle算法要求一个TCP连接上最多只有一个未被确认的未完成的分组,在该分组的确认到达之前不能发送其他的小分组,相反,TCP收集这些少量的分组,并在确认到来时以一个分组的方式发出去。但是这种算法对交互性很强的应用来说不是很合适,会有明显的时延。
对于成块的数据流,TCP更应该关注的是流量的控制。发送端有发送缓冲区(即从应用程序到tcp),接收端有接收缓冲区,并不是接收到的数据马上就能被应用程序处理,如果发送端不断地发送数据,而接收端的缓冲区已经被占满,它必须通知发送端在缓冲区有空隙前,请不要再发送数据了。在TCP中,缓冲区被形象地比喻成一个可以滑动的窗口,TCP通过一些算法来根据窗口的大小发送数据。这是端到端的。还有另外一种情况,就是,当发送方和接收方之间存在多个路由器和速率较慢的链路时,就有可能出现一些问题,一些中间路由器必须缓冲分区,并有可能耗存储器的空间。因此,连接建立时,双方应该慢慢了解去往对方的路况,然后以一个比较合适的速率大小发送块数据。TCP支持一种被称为“慢启动”的算法,该算法通过观察到新分组进入网速的速率应该与另一端返回确认的速率相同而进行工作。慢启动为发送方的TCP增加了另一个窗口:拥塞窗口,当与另一个网络建立TCP连接时,拥塞窗口被初始化为1个报文段(即另一端通告的报文段大小)。每收到一个ack,拥塞窗口就增加一个报文段,发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口是接收方使用的流量控制。
4. TCP的四个定时器
对每个连接,TCP管理4个不同的定时器:
(1)重传定时器用于等待另一端的确认。即当发送端发送出数据后,经过一段时间后假如仍然没有收到接收端的确认,那么就重传该数据块。定时器设计的时间是动态的,并且随着重传的次数的增加而成指数增长。“动态的”的意思是,重传定时器的值随着网络的状态(用RTT来数学化)而发现改变。中间有一条计算式子我都没明白是怎么算出来的,这让人很泄气,看了几遍也没看明白,最终就只是知道了有这么一回事。
(2)坚持定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。我们知道当接收方的窗口大小为0时,发送方将不能再向它发送数据,直到接收方用一个窗口大小为非0的消息来通告发送端。可是,万一这个消息丢失了呢?接收方就一直这样等着发送方发来数据,而发送端就一直等着接收方发来窗口大于0的消息,两方就都僵在那里了。为了避免这种情况的出现,便有了坚持定时器,发送方使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否已增大。坚持定时器的定时时间也是指数退避的。
糊涂窗口综合症是指接收方一旦有非0的窗口大小就向发送方通告,从而引起发送端发送少量的数据这样的情况。可以在任何一方采取措施避免出现这种状况:
A. 在接收方,接收方不通告小窗口,一般是除非窗口可以增加一个报文段大小或可以增加接收方缓冲区空间的一半,不然通告窗口大小为0.
B. 在发送方,发送方除非收到一个比较大的窗口(如一个报文段小大、是接收方通告窗口大小一半的报文段)或者是还没有未被确认的数据的情况下,才会发送数据。
接收方和发送方两方同时进行决策,因为接收方不能通告一个不合理的窗口大小(比方说,原先的窗口大小是1500,报文段长度为1024,发送方发送了1024字节的数据后,这时候接收方的窗口大小是476,小于一个报文大小,但是如果通告窗口大小为0,岂不是很不合理?),因此在收到这个的窗口通告消息后,就轮到发送方使用它的策略了,发送方设定一个坚持定时器,在这个定时器的时间内,除非收到足够大的通告窗口,否则不发送数据。当然,如果定时器超时了,发送方还是要发送小数据量的报文的。
(3)保活定时器可以检测到一个空闲连接 的另一端何时崩溃或重启。前面我们提到“半打开”的连接,这种情况很可能占用服务器很多端口,因此一般由服务器使用保活选项。如果一个给定的连接在两个小时之内没有任何动作,则服务器就向客户发送一个探查报文段,客户主机将必须以下四种状态之一:
A. 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常工作的。服务器在两个小时后将保活定时器复位。如果在两个小时定时器到时间之前有应用程序的通信量通过此连接,则定时器在交换数据后的未来2个小时再复位。
B. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应,服务器将不能收到对探查的响应,并在75秒后超时。服务器总共发送10个探查,每个间隔75称。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
C. 客户主机崩溃并且已经重新启动。这时服务器将收到一个对其保活探查的响应,但是这个响应是一个复位,使得服务器终止这个连接。
D. 客户主机正常运行,但是从服务器不可达。这跟情况B是一样的。
(4)最后一种定时器就是那个2MSL的时间测量器。
嗯,基本上TCP如何建立连接,传输数据,如何确定重传,控制流量,大致都总结了一下。我又要开始忙实验室的活了,嘿嘿,好久没有看论文 ~. ~