3.4. 建立一个连接
建立连接应用的是三消息握手。如果双方同时都发送SYN也没有关系,双方会发现这个SYN中没有确认,于是就知道了这种情况,通常来说,应该发送一个"reset"段来解决这种情况。三消息握手减少了连接失败的可能性。下面就是一个例子,在尖括号是的就是数据段中的内容和标记。其它的就不多说了。
在第2行,TCP A发送SYN初始化序列号,表示它要使用序列号100;第3行中,TCP B给出确认,并且期待着A的带有序列号101的数据段;第4行,TCP A给出确认,而在第5行,它也给出确认,并发送了一些数据,注意第4行的序列号与第5号的一样,因为ACK信息不占用序列号空间内的序列号。同时产生请求的情况如下图所示,只复杂一点。
使用三消息握手的主要原因是为了防止使用过期的数据段。为了这个目的,必须引入新的控制消息,RESET。如果接收TCP处理非同步状态,在接收到RESET后返回到LISTEN状态。如果TCP处理下面几种状态ESTABLISHED,FIN-WAIT-1,FIN-WAIT-2,CLOSE-WAIT,CLOSING,LAST-ACK,TIME-WAIT时,放弃连接并通过用户。我们下面就详细说明后一种情况。
通过上面的例子,我们可以看出TCP连接是如何从过期数据段的干扰下恢复的。请注意第4行和第5行中的RST(RESET信号)。
半开连接和其它非正常状态
如果一方在未通过另一方的情况下关闭连接,或双方虽然失败而不同步的情况我们称为半开连接状态。在一方试图发送数据时连接会自动RESET。然而这种情况毕竟属于不正常情况。应该做出相应的处理。如果A处的连接已经关闭,B处并不知道。当B希望发送数据到A时,就会收到RESET信号,表示这个TCP连接有误,要中止当前连接。
假设A和B两个进程相互通信的时候A的TCP发生了失败,A依靠操作系统支持TCP的存在,通常这种情况下会有恢复机制起作用,当TCP重新恢复的时候,A可能希望从恢复点开始工作。这样A可能会试图OPEN连接,然后在这个它认为还是打开的连接上传送数据,这时A会从本地(也就是A的)TCP上获得错误消息“未打开连接”。A的TCP将发送包括SYN的数据段。下面的例子将显示这一过程:
上面这个例子中,A方收到的信息并没有确认任何东西,这时候A发现出了问题,于是发送了RST控制信息。另一种情况是发生在A失败,而B方仍然试图发送数据时,下面的例子可以表示这种情况,请注意第2行中A对B发送来的信息不知所云。
在下面的例子中,A方和B方进行的被动连接,它们都在等待SYN信息。过期的包传送到B方使B回应了,而收到回应的A却发现不对头,传送RST控制信息,B方返回被动LISTEN状态。
现实中的情况太多了,我们列举一些产生RST控制信息的规则如下:通常情况下,RST在收到的信息不是期待的信息时产生。如果在不能确定时不要轻易发送RST控制信息。下面有三类情况:
如果连接已经不存在,而发送来的消息又不是RST,那么要返回RST。如果想拒绝对不存在的连接进行SYN,可以使用这种办法。如果到达的信息有一个ACK域,返回的RST信息可以从ACK域中取得序列号,如果没有这个域,就把RST的序列号设置为0,ACK域被设备为序列号和到达段长度之和。连接仍然处于CLOSE状态。
如果连接处于非同步状态(LISTEN,SYN-SENT,SYN-RECEIVED),而且收到的确认是对未发出包的确认或是接收到数据段的安全级别与不能连接要求的相一一致时,就发送RST。如果SYN未被确认时,而且收到的数据段的优先级比要求的优先级要高,那么要么提高本地优先级(得事先征得用户和系统的许可)要么发送RST;如果接收数据段的优先级比要求的优先级低,就算是匹配了,当然如果对方发现优先级不对提高了优先级,在下一个包中提高了优先级,这就不算是匹配了。如果连接已经进入SYN,那么接收到数据段的优先级必须和本地优先级一样,否则发送RST。如果到达的信息有一个ACK域,返回的RST信息可以从ACK域中取得序列号,如果没有这个域,就把RST的序列号设置为0,ACK域被设备为序列号和到达段长度之和。连接仍然处于与原来相同的状态。
如果连接处于同步状态(ESTABLISHED,FIN-WAIT-1,FIN-WAIT-2,CLOSE-WAIT,CLOSING,LAST-ACK,TIME-WAIT),任何超出接收窗口的序列号的数据段都产生如下结果:发出一个空确认数据段,此段中包括当前发送序列号,另外还包括一个确认指出希望接收的下一个数据段的序列号,连接仍然保存在原来的状态。如果因为安全级,优先级之类的问题,那就发送RST信号然后进入CLOSED状态。
RST过程
除了SYN-SENT状态外的其它状态中,所有的RST数据段可以通过检查SEQ域查明。如果序列号在接收窗口中,RST是有效的。当连接处于SYN-SENT状态时,如果ACK域确认SYN,那么RST也是合法的。
RST的接收方首先确认它的合法性,然后进行状态转换。如果接收方处于LISTEN状态,它就忽略RST包。如果接收方处于SYN-RECEIVED状态,而且以前处于LISTEN状态,接收方返回LISTEN状态,否则接收方关闭连接进入CLOSED状态。当接收方处于其它状态时,直接关闭连接回到CLOSED状态。
3.5. 关闭连接
CLOSE是一个操作,它的意思就是“本方已经有数据发送”。由于是全双工的,所以会造成一些麻烦,因为接收方对于处理接收方的连接有点麻烦。我们以一种简单的方式对待CLOSE,发送CLOSE的一方在接收到对方的CLOSED之前,还要继续接收数据。因此程序可以在一个CLOSE之后初始化几个SEND,然后开始RECEIVE,直到接收到对方的CLOSED而RECEIVE失败为止。我们假设TCP可以通知用户连接关闭,即使仍在RECEIVE也可以,这样用户就可以正常关闭了。这样,TCP可以在连接关闭前可靠地发送数据。下面列举了三种基本情况:
情况1:本地用户关闭
这种情况下,可以建立一个FIN段放入发送队列。TCP不再接收用户的SEND指令,TCP进入FIN-WAIT-1状态。RECEIVE在这种状态下是允许的。所有数据段和FIN在未接收到确认以前会一直发送。当另一个TCP确认FIN,并发送自己的FIN后,本地的TCP可以确认这个FIN了。请注意,TCP也可以在确认FIN时不返回自己的FIN,直到用户关闭连接时再返回自己的FIN。
情况2:TCP从网络上接收到一个FIN
如果在没有请求的情况下收到FIN,TCP可以返回ACK并通知用户连接已经关闭。用户可以以CLOSE响应,TCP在发送完剩下的数据后发送自己的FIN,然后TCP等待对这个FIN的确认,在接收到后,它关闭连接。如果确认超时,可以关闭连接并通知用户完事。
情况3:双方同时关闭
双方同时关闭会导致交换FIN。双方会在收到对自己FIN的确认后关闭连接。
3.6. 优先和安全
TCP的操作必须在两个优先级相同的端口间进行。TCP使用的优先和安全参数在IP协议中定义。我们这里所说的安全/间隔就是指的IP中定义的优先,用户组和处理规定。如果不符合则发送RST。这些内容请大家看上一节中的叙述。TCP在操作过程中也会检查接收数据段的优先级,还可以在操作中提高优先级。虽然运行在无安全环境中,主机也必须能够处理安全参数。
3.7. 数据通信
建立了连接以后就是传送数据了,TCP通过重新传送保证每个数据段到达对方,因为有了重新传送,所以对方可能接收到两个相同的包,那就必须根据内部的序列号判断哪个数据段是可以接收的。发送方通过使用SND.NXT跟踪下一个要发送的数据段,而接收方则跟踪RCV.NXT来知道下一个要接收的数据段。发送方要还未确认的最老的序列号保存于SND.UNA。
当发送方形成数据段并发送它后SND.NXT增大;当接收方接收到数据段后RCV.NXT增大并发送确认;当发送方接收到确认后SND.UNA增大。它们三者在不同的时间增大,这是因为传送时延造成的。而增大多少则由数据段中数据的大小决定。注意:连接进入ESTABLISHED状态后,所有的段必须包括当前的确认信息。而CLOSE用户操作的性质类型于推操作,这和在接收到的数据段中的FIN标记一样。
重传超时
因为网络中有不类型的网络,而使用TCP的范围又很广,因此重传超时必须动态决定。下面给出一个例子,通过例子可以看出确定重传超时的过程。下面有两个变量说明时延的问题,一个是环路时间(RTT),它是由一个序列码得到的,这个序列码在发送时给出,在接收到确认时被覆盖;另一个平滑环路时间(SRTT):
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)
通过上面的式子,可以得到重传超时(RTO):
RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)}
其中UBOUND是超时的上界(如1分钟),LBOUND是超时的下界(如1秒钟),ALPHA是平滑因子(如0.8到0.9),BETA是延时变量(如1.3到2.0)。
传送紧急消息
TCP的紧急机制是允许发送者使接收者接收一些紧急消息,并让接收方在接收到这一消息后立刻通知用户。这种机制是在数据流是加入一个点,指出这是紧急数据的结束点,当接收方要接收到这个点之前,它会通知用户进入紧急状态,在接收到这个点的数据后,它会通知用户进入通常状态。如果这个紧急点在用户进入紧急状态时更新,这个更新必须对用户透明。
应用一个紧急域的方法可以达到上述目的,而URG控制标记则指明紧急域是否被使用,而且在数据段中必须加入指示紧急点的序列号,如果没有这个标记则说明没有紧急点。如果需要发送紧急数据,发送方必须起码发送一个字节。
管理窗口
如果我们学习过网络基础,我们一定知道有一个窗口协议,TCP中每个数据段都包括下一个希望接收到的序列号。窗口比较大会提高传送速度,如果传送过来的数据超过的窗口大小,数据会被抛弃。这样会加重网络负担。如果TCP开始时的接收窗口比较大,而到最后这个窗口缩小了,这种作法可不是好办法。对于健壮的TCP来说,最好不要自己缩小窗口,但要做要准备对方的TCP缩小窗口。
即使发送窗口大小为零,发送TCP也必须做好准备接收数据并且可以发送至少一位新数据。即使接收窗口为零,发送方也会重发,重发间的间隔时间一般为两分钟。当接收TCP的接收窗口为零时,在它接收到数据时,它依然会返回确认,其中包括希望收到的序列码和当前窗口大小(为零)。
发送TCP将数据打成符合当前大小的包,但在重新传送队列中可能重新打包。这种重新打包并不是必须的,但是会很有好处。如果一个连接只有单向数据流,窗口信息可以在确认信息中得到,这些的序列号都是一个,因此没法区别出谁先谁后。但这并不是一个严重的问题。改进的方法是在数据段中加入最新的期待序列号,这样就可以区别出谁先谁后了。窗口管理对传送效率有很大影响,下面是一些建议:
当使用更少的大数据而要获得高效率时分配非常小的窗口使数据以许多小段传送。对于发送者来说要避免在发送小数据段时必须等到窗口大到一定程度再进行。确认不要被延时。接收窗口如果大小为零时,要传送的数据段可能被分成越来越小的段。如果发送TCP仅仅发送窗口允许大小的数据段,可能这些数据段会被换成比这个段大(或者小)的数据段发送。接收方时不时地进行窗口大小调整会使大数据段分为小段,而不成对。尽量把小的窗口合成大的窗口。
3.8. 接口
网络协议的分层结构决定了TCP有两个接口,向上的用户接口和下面的接口。对下层接收口的描述不是很清楚,这个工作由下层协议来描述,但是这里我们也描述一些TCP要使用要的参数。
用户/TCP接口
下面描述一些TCP命令,这些命令是功能性的,因各系统不同具体实现可能不同。最后,我们必须清楚,不同的TCP实现可能有不同的用户接口,但是有一些功能是最基本的,本节描述的功能便是这个基本功能集。
TCP用户命令
下面定义的命令与其它高级语言中定义的方法差不多,但有些陷井操作需要的,如SVC,UUO,EMT。下面只是一个功能介绍,具体实现可能大为不同,如有些系统会把这里的多条命令结合成一条命令使用。为了实现通信功能,TCP不能只会接收命令,而且能够返回消息给它服务的进程,这些消息包括:
(a) 关于连接的通常消息,如中断,远程关闭等等;
(b) 对用户命令做出应答,是成功还是失败。
Open
格式:OPEN (local port, foreign socket, active/passive [, timeout] [, precedence] [, security/compartment] [, options]) -> local connection name
我们假定本地TCP注意到它所服务的进程标记,而且将检查进程的认证。因实现不同,本地网络和源地址的TCP认证可能由TCP进行也可以由下层协议进行(如IP协议)。这些考虑主要是基于安全性的考虑。
如果active/passive标记设置为passive,TCP会检测到达的连接请求,它是被动的,被动的连接可以有一个完全指定的外套接字用于等待特定的连接或未指定的外套接字来等待任何呼叫。一个完全定义的被动呼叫可以通过执行一系列的SEND命令而变为主动的。创建一个传输控制块(TCB),其中一部分参数是由OPEN命令参数而来。在主动OPEN时,TCP会立刻开始同步连接。如果给出参数timeout,会允许呼叫者应用于所有TCP数据。如果在timeout规定的时间内还未把数据送到目的地,就关闭连接,一般的默认值是5分钟。
参数precedence或security/compartment是用于规范用户对某一连接的安全性的,如果没有指定则使用默认值。TCP会匹配这两个参数,只有在security/compartment一致而且接收到的precedence小于等于要求的precedence时才会打开连接。当连接的precedence大于要求的值时,从接收到的数据段中取得这一值,并在连接中一直使用这个比较大的值。具体实现时可以给用户权力控制precedence的决定过程。例如,用户可以要求precedence必须安全一致,或在提升precedence时要通知用户。
本地连接名(local connection name)由TCP返回用户,它可以代替<本地套接字,外套接字>标记一个连接。
Send
格式:SEND (local connection name, buffer address, byte count, PUSH flag, URGENT flag [,timeout])
这个命令使指定缓冲区内的数据发向指定连接,如果连接未打开则返回一个错误,一些实现中可能在打开连接以前调用SEND,由SEND自动打开连接。如果设置了PUSH标记,数据必须立刻传送给接收者,而且PUSH位在缓冲区中的最后一个段是必须设置。如果没有设置,此数据段就会因为效率的考虑而和下一个SEND命令传送的数据一起传送。如果设置了URGENT标记,那就必须象上面所述的一样紧急处理。传送方设置的URGENT数目不一定非要等于通知接收方用户的次数。
如果在OPEN中没有指定外套接字,那缓冲区数据会被发送到隐式外套接字。未使用外套接字打开连接的用户仍然可以SEND,而不用知道外套接字地址。但是,如果在指定外套接字前进行SEND,则会出错。用户可以使用STATUES确定连接状态。如果指定了timeout,对于当前连接的当前用户timeout会更改为新的连接。
最简单的实现方法就是在没有把数据发送完毕以前,SEND不把控制权交给发送进程,但是这样可能会造成死锁(例如,双方都试图SEND,而不准备RECEIVE),效率也不好。好的实现方法是在发送一段数据后返回控制权给发送进程,当然如果能够多个SEND同时发送,那更好不过了。有多个SEND需要服务时一般采取先来先服务。
对于本地来说,我们现在假定的是SEND在发送数据后会产生类似中断的东西告诉发送进程数据发送的情况;当然SEND也可以在发送完后立刻告知进行数据发送的情况。我们可以乐观地认为发送是成功的,如果发送失败,连接会因为超时而关闭。在实现中,即使是同步返回情况,也需要一些异步信号,但这些异步信号是用于处理连接的,而不是用于处理数据发送的。既然允许多个SEND同时工作,因此有必要区别返回的信息是哪一个SEND的。具体的情况下面会讨论到。
Receive
格式: RECEIVE (local connection name, buffer address, byte count) -> byte count, urgent flag, push flag
此命令分配一个接收缓冲区给指定的连接。如果下面不是一个OPEN命令或者此调用进行未被授权使用此连接返回错误。最简单的实现方法是在缓冲区没有填充完以前不返回控制权,但这样可能会造成严重的死锁。