16 KiB
TCP
参考文献
1 概述
TCP提供的是一种面向连接的、可靠的字节流服务。
- 面向连接:使用TCP的两端在彼此交换数据之前必须先建立一个TCP连接。TCP连接是点对点的,在一个TCP连接中,仅有两方可以彼此通信,TCP不使用广播和多播。TCP的连接和电话网络的连接不同,它对中间的转发设备即路由器、交换机是透明的,连接的信息只存在于连接的两个端系统之上。
- 可靠:TCP保证数据传输的可靠性。
- 字节流:两个应用程序通过TCP连接交换8bit字节构成的字节流。字节流服务中,接收方无法了解发方每次发送了多少字节,可以确保的是一端将字节流放到TCP连接上,同样的字节流将出现在TCP连接的另一端。另外,TCP对字节流的内容不作任何解释。TCP不知道传输的数据字节流是二进制数据,还是ASCII字符、EBCDIC字符或者其他类型数据。对字节流的解释由TCP连接双方的应用层解释。
- TCP提供的是全双工的服务。
2 TCP报文
- 源端和目的端的端口号:用于TCP的多路复用和多路分解,即标识发端和收端应用进程。
- 序号:标识字节流。TCP用序号对字节流中的每个字节进行计数,一个报文段的序号被设置为该报文段中第一个数据字节的计数值。序号是32bit的无符号数,序号到达232-1后又从0开始。当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(InitialSequenceNumber)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号。由于TCP提供的是全双工的服务即连接双方可以同时独立地发送数据,因此连接的每一端必须保持每个方向上的传输数据序号。
- 确认序号:确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加1。只有ACK标志1时确认序号字段才有效。发送ACK无需任何代价,因为32bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。
- 首部长度:首部中32bit字的数目。这个字段占4bit,因此TCP最多有60字节的首部。正常的长度是20字节。
- 6个标志比特:
- URG紧急指针有效。
- ACK确认序号有效。
- PSH接收方应该尽快将这个报文段交给应用层。
- RST重建连接。
- SYN同步序号用来发起一个连接。
- FIN发端完成发送任务。
- 窗口大小:通告给对端的本段窗口大小。用于流量控制。
- 检验和:整个TCP报文段的校验和,覆盖了TCP首部和TCP数据。必须给出,由发送端设置,接收端验证。类似于UDP的校验和,校验和计算是用了和UDP相同的一个伪首部。
- 紧急指针:只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。
- 选项:包括TCP所支持的一些选项。最常见的可选字段是最长报文大小,又称为MSS(MaximumSegmentSize)。每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。
3 TCP的多路复用和解复用
- 由于TCP是面向连接的,需要目地端口、目地IP、源IP、源端口提供连接两端的足够的信息。TCP使用源IP+源端口+目地IP+目地端口的四元组来标志一个socket。
- 在TCP工作时,无论发送端还是接收端,都必须首先建立连接,在连接建立完成后,socket即拥有了它所需的四元组的信息,之后的收发都经过socket进行。
4 TCP的状态迁移
状态迁移图
图中粗的实线箭头表示正常的客户端状态变迁,粗的虚线箭头表示正常的服务器状态变迁。
- 两个导致进入ESTABLISHED状态的变迁对应打开一个连接。
- 两个导致从ESTABLISHED状态离开的变迁对应关闭一个连接。ESTABLISHED状态是连接双方能够进行双向数据传递的状态。
- 图中左下角放在一个虚线框内的4个状态标为“主动关闭”。
- 状态CLOSE_WAIT和LAST_ACK也用虚线框住,并标为“被动关闭”。
- CLOSED状态不是一个真正的状态,而是这个状态图的假想起点和终点。
- 只有当SYN_RCVD状态是从LISTEN状态(正常情况)进入,而不是从SYN_SENT状态(同时打开)进入时,从SYN_RCVD回到LISTEN的状态变迁才是有效的。这意味着如果我们执行被动关闭(进入LISTEN),收到一个SYN,发送一个带ACK的SYN(进入SYN_RCVD),然后收到一个RST,而不是一个ACK,便又回到LISTEN状态并等待另一个连接请求的到来。
状态迁移图说明
-
2MSL等待状态
TCP中有一个报文段最大生存(MSL,Maximum Segment Lifetime)时间的概念。
TIME_WAIT状态也称为2MSL等待状态。它是指:当TCP连接的一方执行主动关闭时,在它方完最后一个ACK(即对对端FIN的ACK)后,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样的目的是可以让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
2MSL等待导致当一个TCP连接处于该状态时,该连接的socket使用的四元组不能被重新使用,即不可能在这个时间段内再重建具有相同四元组的socket。现实中这种限制甚至更为严重--该socket的所使用的本地端口将不能被重新使用。在连接处于2MSL等待时,任何迟到的报文段将被丢弃。
只有主动关闭的一端会进入该状态,被动关闭的一端不会进入该状态。由于这个特性,所以应该尽量让客户端执行主动关闭,服务器执行被动关闭。因为客户端的端口是可以随意选取的,而服务器必须使用为客户端所知道的端口,因而该状态对客户端影响很小,但是如果是服务器进入了该状态就会影响较大。
-
平静时间
2MSL等待状态可以防止将来自一个连接(即四元组相同的连接)的迟到的报文段解释为新连接的一部分。
但如果进入2MSL等待状态的主机由于故障重启了并且在MSL内完成重启,并使用相同的信息建立了一个应该处于2MLS等待状态的连接。这时在故障前从这个连接发出的迟到的报文段会被错误地当作属于重启后新连接的报文段。无论如何选择重启后新连接的初始序号,都会发生这种情况。
为了防止这种情况,RFC793指出TCP在重启动后的MSL秒内不能建立任何连接。这就称为平静时间(quiettime)。只有极少的实现版遵守这一原则,因为大多数主机重启动的时间都比MSL秒要长。
-
FIN_WAIT_2状态
FINWAIT2状态中本端已经发出了FIN,并且另一端也已对它进行确认。如果是半关闭,则到这一步就可以结束了,但是如果不是半关闭,则会等待对端发送FIN来关闭另一个方向的数据传输,此时对端处于CLOSEWATI状态。但是如果对端不发送FIN,本端就一直处于FINWAIT2状态,而对端也将处于CLOSEWAIT状态,并一直保持直到对端决定进行关闭。
linux实现通过shutdown来支持半关闭,而close则用来执行一个全关闭(即期望对端也关闭其在另一个方向的发送)。而在在linux实现中,如果执行的是全关闭,则会设置一个超时间来防止连接在FIN_WAIT_2状态一直等待。
5 TCP的连接管理
建立连接
TCP通过三次握手完成建立连接的工作:
- 连接发起者发送一个SYN段指明自己期望连接的对端的端口,以及初始序号。这个SYN段为报文段1。
- 对端发回包含对端的初始序号的SYN报文段(报文段2)作为应答。同时,将确认序号设置为连接发起者的ISN加1以对连接发起者的SYN报文段进行确认。一个SYN将占用一个序号。
- 连接发起者必须将确认序号设置为对端的ISN加1以对对端的SYN报文段进行确认(报文段3)。
终止连接
TCP是全双工的,因而每个方向的传输都必须单独进行关闭。TCP允许一个方向的传输被关闭,而另一个方向的传输不关闭,这就是TCP的半关闭。由于TCP支持半关闭,因而关闭一个连接需要4次握手。
当连接的一端完成它的数据发送任务后它就可以发送一个FIN来终止这个方向的连接,这意味着本端将不会再向对端发送数据。当一端收到一个FIN后,它必须通知应用层另一端已经终止了那个方向的数据传送,但是这一端仍能发送数据。程序这样做。正常关闭过程:
-
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
-
Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
-
Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
-
Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
连接建立的超时
由于各种原因,连接可能会无法建立。在连接无法建立时,TCP不是立即返回失败,而是会尝试重新建立连接,因为连接无法建立的原因可能是建立连接的请求被丢弃了,所以重新尝试是很有必要的。
当建立连接失败时,在TCP放弃重试之前它会尝试多次。重试通过定时器实现,并且采用了指数后退的方式来确定重试的时间,但同时也定义了一个最大的时间限制,在这个时间点还没成功的话就会放弃。
同时打开
极少数情况下会出现两个应用程序同时执行主动打开的情况。对于同时打开有一个要求,即双方都需要使用为对方所知的端口作为本地端口,否则就无法实现同时打开。TCP可以支持同时打开,并且对于同时打开其最终建立的是一条连接而不是两条,同时打开需要连接双方交换4个报文段,比正常的三次握手多一个。
- 两端几乎在同时发送SYN,并进入SYNSENT状态
- 当每一端收到SYN时,状态变为SYNRCVD,同时它们都再发SYN并对收到的SYN进行确认
- 当双方都收到SYN及相应的ACK时,状态都变迁为ESTABLISHED
同时关闭
连接的双发也可能在同一时间发送第一个FIN来执行主动关闭。TCP支持同时关闭,同时关闭与正常关闭使用的段交换数目相同。
- 应用层发出关闭命令,连接双发各发送一个FIN,并均从ESTABLISHED变为FIN_WAIT_1。
- 连接双方收到对端的FIN后,状态由FIN_WAIT_1变迁到CLOSING,并发送最后的ACK。
- 当收到最后的ACK时,状态变化为TIMEWAIT
复位连接
RST标志表示复位,用来复位连接。
-
TCP会在自己认为的异常时刻发送RST报文段复位连接。一种常见情况是当连接请求到达时,目的端口没有进程正在听。另一种场景是尝试在半打开的连接上发送数据。
-
终止一个连接。终止一个连接的正常方式是一方发送FIN,这种方式被称为有序释放。在这种方式中,FIN在所有排队数据都发送后才会被发送,一般不会有任何数据丢失。但是连接的一方也可以选择通过RST来释放一个连接,这被称为异常释放。异常终止一个连接对应用程序来说有两个优点:
- 直接丢弃任何待发数据并立即发送复位报文段,无需ACK对RST报文段进行确认。
- RST的接收方会区分另一端执行的是异常关闭还是正常关闭。并可以通知应用程序。
TimeWait状态
https://zhuanlan.zhihu.com/p/523678245
主动关闭放
如果我们来做个类比的话,TIME_WAIT的出现,对应的是你的程序里的异常处理,它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题:
第一,防止前一个连接【五元组,我们继续以 180.172.35.150:45678, tcp, 180.97.33.108:80 为例】上延迟的数据包或者丢失重传的数据包,被后面复用的连接【前一个连接关闭后,此时你再次访问百度,新的连接可能还是由180.172.35.150:45678, tcp, 180.97.33.108:80 这个五元组来表示,也就是源端口凑巧还是45678】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:
SEQ=3的数据包丢失,重传第一次,没有得到ACK确认 如果没有TIME_WAIT,或者TIME_WAIT时间非常端,那么关闭的连接【180.172.35.150:45678, tcp, 180.97.33.108:80 的状态变为了CLOSED,源端口可被再次利用】,马上被重用【对180.97.33.108:80新建的连接,复用了之前的随机端口45678】,并连续发送SEQ=1,2 的数据包 此时,前面的连接上的SEQ=3的数据包再次重传,同时,seq的序号刚好也是3(这个很重要,不然,SEQ的序号对不上,就会RST掉),此时,前面一个连接上的数据被后面的一个连接错误的接收 第二,确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:
主动关闭方关闭了连接,发送了FIN; 被动关闭方回复ACK同时也执行关闭动作,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态; 但是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态 此时,如果没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也即是说,如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST 造成主动创建连接的一方,由于收到了RST,则连接无法成功 所以,你看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据粗乱,或者短暂性的连接失败。
CloseWait
被动关闭方。
CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。
关于主动关闭和被动关闭的讨论
请求的发起方和接收方都可能主动关闭或者被动关闭。与请求发出方和接收方无关。
例如,服务器响应超时,客户端等不及了,会触发超时时间主动关闭,此时虽然服务器收到了fin包,并且协议层可以返回ack进行确认,进入了closewait状态。但是服务器因为还在处理超时的任务,肯定要继续等待任务处理完成才会调用close函数,主动关闭。








