相关文章:
tcp/ip详解卷一(笔记1:概述与IP层协议)
tcp/ip详解卷一(笔记2:UDP及相关的协议)
tcp/ip详解卷一(笔记3:tcp与相关协议)
tcp 协议中包括如下内容:
本章主要对tcp协议做一个简要的描述,包括协议特点,协议报文字段描述等
tcp协议的可靠性通过以下方式进行保证:
要点:
本章主要描述tcp连接过程的各种状态与注意问题
特殊情况处理如下描述
连接超时分成两种情况:
tcp提供了连接的一端在结束后还能接收来自另外一端数据的能力。这就是所谓的半关闭。通过FIN报文进入半关闭状态(设置tcp中的标志位)。
利用tcp半关闭状态的例子
使用unix 的rsh命令,远程登录到另外一个系统执行一个命令,例如:
rsh nodeX sort < datafile
该命令就在nodeX上执行sort排序,排序的数据来源于本机文件内容。rsh通过在目标节点上建立tcp连接,然后将要排序的数据发送给目标机。那目标机什么时候执行排序操作呢?他如何直到源节点已经发送完了数据,可以排序了呢(如果不等对端发送完全部的数据,就执行排序是没有意义的)。这里就通过fin状态。源节点发送完所有待排序数据后,就发送Fin。等待目标节点执行完后,发送结果给自己。
如果没有这个特性的化,tcp还需要发送一个特殊包,表示自己传输完毕。
time_wait状态也称为2MSL等待时间。每个具体tcp实现都必须选择一个报文段最大生存时间MSL(maximum segment lifetime)。它是任何报文段被丢弃前在网络内的最大时间,该值是有限的(tcp数据封装在IP中,而IP数据报存在TTL)。
当主动执行关闭连接的一方(连接双方中第一个发送FIN报文的一方)在收到fin ack,并收到对方fin,并发送fin ack后,进入time_wait状态,且必须在该状态保持2被MSL,防止对方没有收到该fin ack而重发fin后,自己重发fin ack。
另外,在该2MSL等待时间内,这个连接相关的本地socket 端口不能再被使用。在该等待时间内,任何迟到的报文就被丢弃。
PS:如果停止服务器,服务端口进入time_wait状态,则该端口需要1到4分钟才能被重新使用。因此,立马重启服务会抛出端口占用的错误。
无论何时,任何一个发往某一个连接的报文出现错误,TCP都会回复一个复位报文。这里出现错误的情况包括:
(1)发到对端,但是目的端口没有进程监听(对比udp,出现该种情况下,则会产生一个ICMP 端口不可达错误);
(2)连接异常终止。当建立连接后,通信的一方(A)突然挂了,通信的另外一方(B)是无法察觉的(如果不发送数据)。然后A又重新启动了,这是B还是在以前的连接上发送数据给A,则会收到A的RST数据段;
(3)异常终止一个连接:正常终止一个连接通过发送一个Fin消息段。也可以主动发送一个RST消息段来异常终止一个连接(优点:会导致TCP丢弃任何待发数据,并立即发送RST)
大多数TCP服务进程是并发的。当一个新的连接请求达到服务器时,服务器接收这个请求,并调用一个新的进程来处理这个新的客户请求。那当一个服务器进程接收一个来自客户进程的服务请求时是如何处理端口的?
当启动服务器时,会启动一个进程,监听在服务端口上(通过netstat查看,可看到state为LISTEN状态的进程。)。当有client连接过来时,会重新建立一个进程,该进程用户用户处理该client的请求,该新的server进程依然在服务端口上和client通信。
那在server端,处理不同client请求的进程(或线程)共用同一端口,那如何区分不同的Socket,收到数据后,怎么分发呢?
由于TCP使用四元组(server ip,server port, client ip, client port)唯一标识一个连接,而client的远端端口号不同,这不会造成冲突。
服务程序在listen某个端口并accept某个连接请求后,会生成一个新的socket来对请求进行处理,就可以区分客户。
呼入连接请求队列
一个并发服务器调用一个新的进程来处理每个客户请求,因此处理被动连接的服务器应该始终准备处理下一个呼入的连接请求。但是当服务器正常创建新的进程处理连接请求时,达到多个连接请求,TCP应该如何处理这些呼入的连接请求?
在伯克利的TCP实现中采用如下规则:
本章主要描述传输交互数据时可能会发生的经受时延的确认(数据捎带ACK)、发送数据时使用的Nagle算法。
在接收方收到数据包后,接收方可能不会里面发送ACK,而是会根据一些策略进行一定时间的推迟。数据捎带ACK就是一种推延策略,在接收方发送数据时,顺便确认,减小网络中的数据包。
交互数据特点:数据量较小,但是数据段数量较多。以Rlogin 为例,和远程系统通信,需要回显我们键入的字符。当发生一次按键后,发生的tcp通信流如下所示(在建立好连接后)。
在实际中,一般会将报文2,3进行合并,即数据捎带ACK。这种合并技术称为经受时延的确认。
当TCP接收到一个数据段DS后,会启动一个定时器,如果在定时器超时前,有数据要发送,则将该数据段DS的ACK和要发送的数据一起发送出去。如果一直没有数据要发送,则定时器超时时,单独发送DS的ACK。
Nagle算法要求:在一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认达到之前不能发送其他的小分组(即发送一个数据段,然后等待ACK。在等待期间,缓存将要发送的数据,当收到ACK后,一次性的将数据发送出去)。 TCP收集这些小量的数据,并在上一个分组的确认到达的时候,将收集的数据在一个分组发送出去。 该算法的优越之处在于 该算法是自适应的:确认达到越快,数据也发送的越快(ACK达到的越快,则网络情况越好,即不太可能会拥塞,则不必为了网络的整体质量,增加时延,将数据段尽快发送出去)。
算法使用场景:在一个rlogin连接上,客户一般每次发送一个字节到服务器,这就产生了一些41字节的分组:20字节的IP首部,20字节的TCP首部和一字节的数据。在局域网上,这些小分组通常不会引起麻烦,应为局域网一般不会拥塞。但是在广域网中,这些小分组则会增加拥塞(广域网数据量大)。一个简单的就是使用RFC 896中建议的Nagle算法。
注意:该算法可以关闭,即不使用该策略。
另外,在某些交互应用中,如X窗口应用中,不能忍受长时延,即使只有一个字符也必须要立马发送,则需要关闭该算法。在Socket API中,通过TCP_NODELAY来关闭Nagle算法。
本章主要描述TCP中的滑动窗口协议、慢启动、紧急数据;
在传输成块数据时,更加在乎的是网络的吞吐量,即尽快在一段时间内多发送数据,而不是每发送一段数据,尽快给我反馈。那就需要发送方尽量快的发送数据,那是不是越快越好、仅本地网络最大带宽,发送呢?当然也不是,还需要考虑TCP接收方的处理速度。如果TCP都收到了数据,但是上层应用处理数据慢,数据一直放在缓冲区中,发送方不停的发,最终肯定会耗尽接收方的缓冲区(快的发送方与慢的接收方问题)
那接收方如何告诉发送方自己的处理能力呢?这就是滑动窗口协议要做的事。通过TCP首部中的窗口字段,告知发送方自己的当前窗口情况。
那在建立TCP连接后(主动建立连接的client会收到Server的syn+ack包,该包会告知client的当前窗口),那client直到当前窗口后,是不是就可以里面一直使劲的发送,直接撑死Server的TCP接收缓存窗口呢?当然也不是,发送方不知道当前自己所处的通信网络的拥塞情况。举个例子,通过A和D通信,中间经过BC路由器(A–>B–>C–>D),而BC路由器所处的网络情况不好。A在这使劲的发,把自己的电脑卡的要死,但是BC网络差,丢包严重,数据无法传给D,D压根就收不到,那么A会在一定时间还没有收到ACK后,又要重传。最后效果是A不停的发,D收不到几个,且还大大的加重了BC网络的拥塞情况。费力不讨好。
那A怎么发送才是最优的呢?如果感知网络通信质量呢?这就是慢启动要做的事
TCP支持一种称为“慢启动”的算法。该算法基本思想:新分组进入网络的速率应该与另一端返回确认的速率相同。
慢启动为发送方的TCP增加了另一个窗口:拥塞窗口,记为cwnd。当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为1个报文段(即在收到确认前,只能有一个未被确认的数据段)。每收到一个ACK,拥塞窗口就增加一个报文段。发送方取拥塞窗口与通告窗口中的最小值为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口(rwnd)是接收方使用的流量控制。
cwnd在达到阈值前呈指数级增长:假定当前cwnd=1,收到对端的ACK后变为2,若接下来发送的两个数据包又收到ack,则cwnd=4;
慢启动阈值ssthresh:随着cwnd的增加,可能会导致网络过载即出现丢包,此时cwnd的大小会迅速衰减至当前的一半;一旦触发了慢启动阈值,cwnd趋于线性增长,以避免再次迅速引发网络阻塞,直至下次丢包(如此反复)。
网络中实际传输的未经确认的数据大小 = min(rwnd, cwnd);
那拥塞窗口和通告窗口的最佳大小是多少呢?
网络中的传输通道容量为:
Capicity(bit)= bandwidth(b/s) * rount-trip time(s)
该值称为宽度时延乘积。在网络中,最佳稳定状态是每发送一个数据包,就接收到一个ack。这样,不管有多少个报文填充了网络传输通道,返回路径上总有相同的ACK。发送方接收方都不会有积压的未处理的数据。一个报文的返回时间时为rount-trip time(s) 。这段时间要发送一个包,能发送的数据量就是发送速度* 发送时间。
rwnd的合理值取决 宽度时延乘积
cwnd 的合理值 计算公式:cwnd = min(4 * MSS, max(2 * MSS, 4380))
以太网标准的MSS(maximum segment size)大小通常是1460,初始值为4380即3MSS。
https://blog.csdn.net/dhaiuda/article/details/79128584
当TCP中在首部设置了URG标志后,在 16 位紧急指针的特定的位置(指向紧急数据的最后一位,也可以是紧急数据的下一位,两者都是实现标准)的数据就是紧急数据。因为只有一个紧急指针,这也意味着它只能标识一个字节的数据。这个指针指向了紧急数据最后一个字节的下一个字节。
对于16 位紧急指针配合使用,只有设置了URG标志后,该指针才有效。
在读取到紧急指针所指向的位置之前,TCP的接受进程都处于紧急状态,当读取到紧急数据后一位时,回复到正常状态。
从上面的特性可以看到,TCP无法告诉紧急数据从哪里开始,只能告诉紧急数据从哪里结束,URG位为1的TCP报文并不是带外数据。抄袭一段百度上的解释:
传输层协议使用带外数据(out-of-band,OOB)来发送一些重要的数据,如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方。为了发送这些数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。但是TCP协议没有真正意义上的带外数据。为了发送重要协议,TCP提供了一种称为紧急模式(urgent mode)的机制。TCP协议在数据段中设置URG位,表示进入紧急模式。接收方可以对紧急模式采取特殊的处理。
TCP协议没有真正意义上的带外数据,以下是我的理解,原因很简单,从发送主机到接收主机方向的通道只有一条,若要发送带外数据,不是还要建立另外一个TCP连接?TCP连接占用的资源比较大,且连接的建立与释放会耗费一定的时间,每次连接不一定都会频繁的发送紧急数据,甚至不会发送,此时占用资源的利用率非常低,所以TCP协议不会有带外数据。
TCP 在传输数据时是有顺序的,它有字节号,URG 配合紧急指针,就可以找到紧急数据的字节号。紧急数据的字节号公式如下:
紧急数据字节号(urgSeq)= TCP报文序号(seq) + 紧急指针(urgpoint)−1
例子,如果 seq = 10, urgpoint = 5, 那么字节序号 urgSeq = 10 + 5 -1 = 14.
知道了字节号后,就可以计算紧急数据字位于所有传输数据中的第几个字节了,如果从第 0 个字节开始算起,那么紧急数据就是第 urgSeq - ISN - 1 个字节(还记得 ISN 吗,它表示初始序列号),减 1 表示不包括第一个 SYN 段,因为一个 SYN 段会消耗一个字节号。
(1)发送端
发送端也可以进入紧急模式,TCP协议栈会为每个套接字维护一个发送端紧急模式标志和一个发送端紧急指针,当发送端TCP协议栈得知有紧急数据要发送时(即某个进程调用了send(MSG_OOB)函数),将发送端紧急模式置为1,同时将紧急指针的值记录在发送端紧急指针处,随后进入紧急状态,(发送缓冲区中)含有未发送字节到紧急字节之间数据的报文都会将URG位置为1,设置紧急指针的值,进入紧急模式后,无论数据字节是否发出,URG紧急通知都会发送( 数据流会因为TCP流量控制而停止,紧急通知总是无障碍的发送到对端TCP),但紧急数据因为滑动窗口满而不随同发送。当含有紧急字节的报文发送并确认接收后,发送端会解除紧急状态。
可以看到,第十一个字段返回时指出了窗口大小为0,但sun还是发送了含紧急通知的报文(第十二个字段)。接着第十四个字段中真正包含紧急数据。
(2)接收端
接收端TCP协议栈也会给每个套接字配上紧急模式标志和紧急指针接收端在接收到TCP报文后,若发现TCP头部的URG位为1,则将紧急模式标志置为1,保存紧急指针的值,随后进入接收端紧急模式,通知接收进程。此后,TCP监听每一个收到的数据字段,若其中含有紧急字节,则将该字节放在单独的带外缓冲区中(独立于接收缓冲区),如果接收端对套接字调用setsockopt开启了SO_OOBINLINE,此字节将混在普通数据中,称为在线接收。在接收进程读取数据时,只有在下一个待读字节越过紧急字节之后,接收端紧急模式才被解除。
TCP 提供可靠的运输层。它使用的方式之一就是确认从一另一端收到的数据。但数据和确认可能会丢失。TCP通过在发送时设置一个定时器来解决这种问题。如果当定时器溢出时还没有收到确认,它就重发该数据。那定时器时间应该设为多久呢?重复的频率应该是什么呢?本章主要讲述RTT(Round Trip Time,表示从发送端到接收端的一去一回需要的时间)的测量方式与RTO(超时重传时间)如何确定,TCP如何发现包超时、如何决定重发,如何主动避免拥塞
由于路由器和网络流量均会变化,因此RTT会也会进程变化,TCP应该跟踪这些变化并相应的改变其超时时间。
TCP通过记录发送的TCP数据段的时间和接收到该字节的确认的时间来衡量RTT。如下图,可通过报文4,7测量到一个RTT。
用M表示所测量到的RTT。最开始的TCP规范使用低通过滤器来更新一个被平滑的RTT估计器,即为R(即会对每次测量到的RTT进行平滑过滤处理)。
R=a*R+(1-a)M (式1)
这里a是一个推荐值为0.9的平滑因子。每次进行新策略时,这个被平滑的RTT将会得到更新。
RFC 793推荐的重传超时时间RTO(Retransmission timeout)的值为:
RT0=Rβ (式2)
其中β为推荐值为2的时延离散因子(即随时间变化,每次自增两倍,直到RTO变成64s封顶)
举例:如果数据报D超时还没有收到ACK,则首次在R时间后重传,如果还没有收到确认,则在2R后再次重传,之后是4R…,直到增加到64s秒后,一直按照64s的间隔重传。
** 另外,TCP还需要记录RTT的方差。当RRT变化起伏很大时,使用基于均值和方差来计算RTO **,其计算公式如下:
Err=M-A
A=A+gErr
D=D + h(|Err | - D)
RTO = A + 4D
其中A表示被平滑的RTT。即上公式1计算出的值。D是被平滑的均值偏差。Err是刚得到的测量结果与当前RTT估计值之差。增量g起平均作用,取值为1/8。偏差的增益是h,取值为0.25。当RTT变化时,较大的偏差增益使得RTO快速上升。
当ACK超时,发生重传后,收到一个ACK,则该ACK是对之前的包的确认还是对重传包的确认呢?
当一个超时和重传发生时,在重传数据的确认最后到达时,不能更新RTT的估计值,因为我们不知道该ACK是针对那次数据传输。
另外,还有一种情况不不会进行RTT的更新,即:在发送一个报文段时,如果给定连接的定时器已经被使用,则该报文段不被计时(即不会测量、记录该报文的RTT)
举例:
当发送报文3 后,启动了定时器,此时3包的ack没有收到,则该定时器不会关闭,此时发送了报文 4多个报文,则在收到4的ack也不会更新RTT(原因很简单,发送时,这几个报文的发送不会产生新的定时器,所有也无从计时)。这里会在收到3的ack后,记录到一个新的RTT。
TCP会记录追踪RTT,并根据RTT来确定RTO(什么时候应用重传了)。当RTT比较平稳(方差较小时),使用 被平滑的RTT估计值公式估计RTT,并更新RTO。当RTT方差较大,使用均值偏差公式计算RTT和RTO。
另外,当发生重传后,对重传包的ACK不进行RTT的更新。
本节主要讲述TCP如何控制自己的发送速度,以保证自己的传输速度,另外,在网络拥塞发生时,尽量控制自己的速度,造成网络情况的快速恶化。
TCP在开始建立连接后,会执行慢启动来快速的增加自己的发送速度。当发送速度达到某一个值后,开始执行拥塞避免算法来控制自己的发送速度 。 但是网络还坏情况动态变化的,有时候,会发现一下网络异常情况,如包丢失,这时TCP会进行快速重传,然后TCP发送进入快速恢复阶段。
相关变量:
拥塞的定义:
由于有份受到损坏引起的丢失是非常少的。因此,分组的丢失就意味者在源主机和目的主机之间的某处发生了拥塞。有两种分组丢失的指示:发生超时和收到重复的确认。
采用该算法的条件:当 cwnd <= ssthresh
算法执行过程:
采用该算法的条件:当 cwnd > ssthresh
算法执行过程:
正常情况(没有发生拥塞时),cwnd的增长曲线如下:
TCP在收到一个失序的报文时,TCP立即产生一个ACK(重复的ACK,该重复的ACK的目的在于让对方直到收到了失序的ACK,并告诉对方自己希望收到的序号) 。正常情况下,会是经受时延的确认,即数据捎带ACK或是ACK确认时钟超时后发送(绝大多数实现,采用的时延是200ms,即TCP将以最大200ms的时延等待是否有数据一起发送)。
由于我们不知道一个重复的ACK是由一个丢失的报文启动的,还是由于因为出现了几个报文段的重新排序,因此我们等待少量的ACK到来。如果只是一些重新排序,则在重新排序的被处理时,可能只会产生1~2个重复的ACK。如果收到3个或三个以上的重复ACK,就非常可能是一个报文段丢失了。于是立马重传丢失的报文段,而无需等待超时定时器溢出,这就是快速重传。
快速重传:当确定发生包丢失时,立马重传丢失的报文段,而无需等待超时定时器溢出,这就是快速重传。
采用该算法的条件:当发生快速重传后,会进入快速恢复算法
算法执行过程:
当TCP通过让接收方指定希望从发送方接收的自己树(即窗口大小)来进行流量控制。如果当窗口变成0之后,发送方会停止发送新的数据,直到接收方发送一个窗口更新报文。那么如果该窗口更新报文丢失会怎么样呢?发送方等待窗口更新,而接收方等待发送方发送新的数据。TCP必须能够处理此窗口更新报文丢失的情况。
TCP 的ACK传输并不可靠,TCP不会对ACK报文进行确认,只确认那些含有数据的ACK报文段(不然,会不停的、无休止的双方确认下去)
为了防止窗口更新报文丢失引起了双方相互等待,发送方使用一个检查定时器(persist timer)来周期性的向接收方查询,以便发现窗口是否增加。这些发送方发成的报文段称为窗口探查。
当TCP发送方收到一个通告窗口为0的报文时,发送方设置其坚持定时器。如果该定时器时间到时还没有收到窗口更新报文,则发送方发送窗口探查报文,查询这个窗口更新报文是否丢失。
定时器的时间序列:坚持定时器使用了普通的TCP指数退避,对于一个典型的局域网连接,检测定时器首次超时时间1.5s左右,第二次超时增加一倍,为3s,下次乘以4,再下次乘以8…
==窗口探查报文:==包含一个字节的数据。TCP总是允许发送已关闭的窗口之后的一个字节的数据(如果窗口更新了,则会对该字节进行确认;否则确认是原先的数据,该新数据由于缓冲区慢,而丢弃了,不会进行确认)
基于窗口的流量控制方案,会导致“糊涂窗口综合症”的状态。如果发生这种状况,则连接之间会发送少量的数据,而不是满长度的报文段(即连接之间会存在小报文)
该种情况可发生在连接的任何一方:接收方通告一个小的窗口(而不是一直等到有大的窗口时才通告),而发送方发送少量的数据(而不是等待其他的数据以便发送一个大的报文段),这两种情况就是“糊涂窗口综合症”的现象。为了避免这种情况(糊涂窗口综合症),在窗口更新时,TCP采用如下的方法:
正常情况下,在建立连接后,如果双方都没有发送任何数据,只要两端的主机没有重启,则连接会依然保持建立。
宝货定时器的作用是:试图检测半开发的连接(例如,当client关闭后,服务器还是保持连接,等待来自client的数据)。
注意:保活并不是TCP规范中的一部分。该功能也可以在应用级完成,如在应用层协议或是应用程序中实现。