TCP协议简介

特点

  • TCP是面向连接的运输层协议。也就是说,应用程序在使用TCP连接之前,必须建立TCP连接。数据传输完毕之后,必须释放TCP连接。
  • 每一条TCP连接只能有两个端点,每个TCP连接都是点对点的。
  • TCP提供可靠交付服务,也就是说数据是无差错,不丢失,不重复,按照顺序到达。
  • TCP提供全双工通信,允许通信双方在任何时候都能够发送数据,TCP双方都设有发送缓存和接收缓存,用来临时存放双方通信的数据。
  • 面向字节流。TCP中的**“流”指的是流入到进程或从进程流出的字节序列**。面向字节流的含义是:虽然应用程序和TCP交互是一次一个数据块,但是TCP把应用程序交下来的数据仅仅看成是一连串无结构的字节流。TCP并不知道所传送的字节流的含义,也不保证接收方收到的数据块大小与发送方的数据块大小有什么对应关系,有可能发送发10个数据块的数据在接收方被整理成4个数据块交付给上层。

TCP与udp在发送报文的时候采用的方式完全不同,TCP并不关心应用进程一次把多长的报文发送到TCP连接的缓存中,而是根据对方给出的窗口值和当前网络的拥塞状况决定来决定每一个报文应该包含多少字节(UDP的报文长度是应用进程给出的,应用进程给出的报文,UDP只是简单的加上头部就交给下一层)。

目前我的理解是:上层分多个大小不同的数据块将字节流存入缓存中后,TCP会根据窗口值和拥塞状况等重新切割字节流去发送报文,只要保证最终所有字节按顺序到达即可

什么是TCP连接

TCP把连接作为最基本的抽象,TCP的许多特性都是建立在面向连接这个基础之上的,所以我们要搞清楚TCP连接到底是什么。

每个TCP连接都有两个端点,也叫作套接字或者插口,套接字其实就是将端口号拼接到IP地址后面组成的。TCP连接的端点是个很抽象的套接字,同一个IP地址可以有多个不同的TCP连接,而同一个端口号可以出现在多个不同的TCP连接中。TCP连接其实就是有协议软件提供的一种基于套接字的抽象。

置于为什么说TCP是面向连接的,目前我的理解是:TCP连接需要占用连接双方的端口,在占用期间,这个端口传送的数据只属于这条TCP连接,所以对于双方来说像是一条链路,但是并不是说TCP连接之间的所有路由器都要为这条连接做什么特殊的处理。

当你在阅读下面内容的时候如果产生了:凭什么TCP这样设计就可以实现这种效果的时候,想一下它面向连接这个特性

可靠传输的原理

TCP协议下面的网络提供的是不可靠的传输,因此TCP必须采取一些措施去对一些意外情况进行补救,使得运输层之间的传输变得可靠。

停止等待协议

停止等待协议非常简单,

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587171346/computer_network/微信图片编辑_20200418085415_bobkzr.jpg

  • 无差错情况。A发送完分组M1之后就暂停发送,等待B的确认。B收到了M1就向A发送确认,A在收到对M1的确认之后再继续发送下一个分组M2。

  • 出现差错。B收到M1时检测出了差错,就直接将M1丢弃,然后什么都不做。A只要一段时间内没有收到确认,就认为刚才发送的分组丢失了,因而重传前面发送的分组。这就叫做超时重传。要实现超时重传,就必须在每发送完成一个分组,就设置一个超时计时器,如果在计时结束之前收到了确认,就撤销计时器。这里要注意三点:

    • A在发送完成一个分组后,必须暂存已发送的分组的副本,直到收到确认才可以将其删除。
    • 分组和确认分组都必须设置编号,这样才能知道是哪一个分组收到了确认,哪一个没有。
    • 超时计时器设置的重传时间,应当比数据在分组传输时的平均往返时间稍微长一点
  • 确认丢失和迟到。如果B发送的对M1的确认丢失了,A在设定的超时时间内没有收到确认,无法知道是自己发送的分组出现错误,丢失,或者是B发送的确认丢失,因此在超时计时器到期后A就要重传M1,假定B又一次收到了M1,则采取两个行动:

    • 丢弃这个重复的分组M1,不向上层交付。
    • 再次向A发送交付确认,不能认为已经发送过的确认就不再发送。
  • 信道利用率。停止等待协议的优点是简单,缺点是信道利用率太低

    https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587195669/computer_network/微信截图_20200418154042_tktoqj.png

    假定A发送分组需要的时间是TD,显然TD等于分组长度除以数据率。再假定分组正确到达B后,处理时间可以忽略不计,同时立即发送确认。假定B发送确认分组需要时间TA。如果A处理确认分组的时间也可以忽略不计,那么A在经过时间(TD + RTT + TA)后才可以发送下一个分组。因为仅在TD内才用来传送有用的数据,因此信道利用率U可以表示为

    U=TDTD+RTT+TAU = \frac{T_D}{T_D + RTT + T_A}

    为了提高传输效率,发送方可以 不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可以连续发送多个分组,不必每发完一个分组就停顿下来等待对方的确认。

    当使用流水线传输时,就要使用连续ARQ协议滑动窗口协议

连续ARQ协议

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587196595/computer_network/微信截图_20200418155614_iod7dv.png

上图表示发送方维持的发送窗口,它的意义是:位于发送窗口的5个分组都可以连续发送出去,而不需要等待对方的确认。

在讨论滑动窗口时,我们应当注意到,图中还有一个时间坐标。按照习惯,向前是指向着时间增大的方向,而向后指的是向着时间减少的方向。分组发送是按照分组序号从小到大发送的。

连续ARQ协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。

接收方一般采取累计确认方法,也就是说接收方不必对收到的分组逐个发送确认,而是在收到几个分组,对按序到达的最后一个分组发送确认

TCP报文段首部

TCP虽然是面向字节流的,但是TCP传输的数据单元却是报文段。一个TCP报文段分为首部和数据两部分,而TCP的全部功能都体现在它首部的各字段的作用。因此只有掌握TCP首部各个字段的作用,才能够掌握TCP的工作原理。

TCP首部的前面20个字节是固定的,后面4n个字节是根据需要增加的选项。

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587197557/computer_network/微信截图_20200418161217_djnuqg.png

源端口和目的端口

各占两个字节,分别写入远端口号和目的端口号。

序号

占四个字节,序号范围是[0, 2^32 - 1],序号增大到最大后,就是用取模运算。

在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段指的是本报文段发送的数据的第一个字节序号。

确认号

占四个字节,是期望收到对方的下一个报文段第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节,这表明B正确收到了A发送的到序号700的为止的数据。因此B期望A下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701 。总之,如果确认号等于N,则表明到序号N-1为止的所有数据都已经正确收到

一般情况下,可保证当序号重复使用时,旧序号的数据早已通过网络到达终点。

数据偏移

占4位,它指出的是TCP报文段首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移是必要的。这个字段单位是32位(4字节)。由于4位二进制数能够表达的最大数为15,因此数据偏移最大值是60字节。

保留

占6位,保留为今后使用,目前置为0

紧急URG(URGent)

当URG为1时,表明紧急指针字段有效,它告诉系统此报文中有紧急数据,应当尽快传送,而不要按照原来的排队书序来传送。例如如果在远程控制某个程序的运行时需要中断,用户从键盘发出Ctrl + C。如果不采用紧急数据,那么这两个字符将存储在接受TCP的缓存末尾。只有在所有的数据都被处理完成之后这两个字符才会被交给接受方的应用程序。

当URG置为1时,发送应用进程就告诉发送方的TCP有紧急数据要发送,于是TCP就把紧急数据插入到本报文段数据的最前方,而后配合另一个首部字段紧急指针字段表明紧急数据的长度,然后剩下的数据仍然是普通的数据

确认ACK

只有在ACK = 1时,确认号字段才有效,当ACK = 0时,确认号无效,TCP规定,在连接建立之后所有传送的报文都必须把ACK置为1

推送PSH(PuSH)

当两个应用进程进行交互式的通信时,有时一端的应用进程希望在加入一个命令后立即能够收到对方的响应,这个时候就可以使用推送操作。这时发送方将PSH置为1,并立即创建一个报文段发送出去,接收方收到PSH为1的报文,就尽快交给接受程序,而不用等缓存填满之后再一起交付。

复位RST

当出现重大差错时,必须释放连接然后建立新的连接。或者用于拒绝一个非法的报文或拒绝打开一个连接。

同步SYN

在建立连接时用于同步序号,当SYN = 1而ACK = 0时,表明这是一个连接请求报文,对方如果同意建立连接,则在响应报文中使用SYN = 1,ACK = 1 。因此,SYN置为1就表明这是一个连接请求或者连接建立报文

终止FIN

用于释放一个连接,当FIN = 1时,表明此报文段的发送方数据应经发送完毕,并要求释放运输连接。

也就是说理论上不发送这个字段为1的报文,TCP连接就不会中断。

窗口

占2字节,窗口值是[0, 2^16 - 1]之间的整数。窗口值得是发送本报文段的一方的接收窗口。窗口值告诉对方的是:从本报文段首部确认号算起,接收方目前允许对方发送的数据量(以字节为单位),窗口值是作为接收方让发送方设置其发送窗口的依据。

窗口字段之处了现在允许对方发送的数据量,窗口值经常在变动。

检验和

占2字节,检验和字段检验范围包括首部和数据两部分。具体如何计算比较复杂,有兴趣可以自己搜索一下

紧急指针

占2字节,仅在URG = 1时才有意义,指出了本报文段中紧急数据的长度,当所有紧急数据都处理完成之后,对本报文段中接下来的正常数据就会恢复到正常操作

选项

长度可变,最长可达40字节,当没有使用选项时,首部长度就是20字节。

最初的时候只规定了MSS(最大报文段长度,Maximum Segment Size) 。MSS应该尽量大一些,只要在IP层传输的时候不要再分片就好,当然由于IP层经历的路径是动态变化的,这个MSS也很难确定。

在连接建立过程中,双方可以把自己支持的MSS写入这一字段,以后就按照这个字段进行传送,两个传送方向可以有不同大小的MSS。如果主机没有填写这一项,默认就是536,所有互联网主机都应该能够接受的MSS是536 + 20(固定首部长度) = 556字节。

TCP可靠传输的原理

以字节为单位的滑动窗口

TCP的滑动窗口都是以字节为单位的,假定A收到了B发来的确认报文段,其中窗口值是20,而确认号是31(这表明B期望收到的下一个序号是31,而序号30为止的数据已经收到了),根据这两个数据,A就构造出自己的发送窗口。

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587202028/computer_network/微信截图_20200418172540_by140j.png

这里可能会有人和我同样突然有点疑惑,如果每次都要等B返回确认A才能改变窗口位置和大小,然后把窗口数据发送出去,这和停止等待协议有什么区别?目前我能看出的区别就在于窗口大小可能远超MSS,需要多个分组才能发送完成,而每次的确认也不是说整个窗口都被确认了。

对于A的发送窗口,在没有收到B的确认之前,A可以连续把窗口内的数据都发送出去。凡是已经发送的数据,在未收到确认之前都必须暂存,以便超时重传时使用。

发送窗口里面的序号表示允许发送的序号,显然,窗口越大,发送方就可以在收到对方确认之前连续发送更多的数据,因而可以获取更高的数据传输效率。

发送窗口后沿的后面部分表示已经发送并且收到了确认,这些数据显然不需要再保留了。而前沿的前面部分表示不允许发送,因为接收方没有为这部分数据保留临时存放的缓存空间。

发送窗口的位置由前沿和后沿共同确定。后沿的变化有两种情况,即不动(没有收到新的确认),和前移(收到了新的确认),后沿不可能向后移动,因为不能撤销已经收到的确认。前沿可以不断向前移动,也可以不动,不动的情况分为两种,一种是没有收到新的确认,多让通知的窗口大小也不变,二是收到了新的确认,但是对方通知的窗口缩小了,正好使得前沿不动。当然前沿也可以向后收缩,但是TCP标准强烈不建议这么做

现在假定A发送了序号31-41的数据,这个时候发送窗口位置并没有改变,窗口内靠后的11个字节表示已发送但是未收到确认,前面的9个字节表示允许发送但是尚未发送。

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587203074/computer_network/微信截图_20200418174406_itnljo.png

从上图可以看出,要描述一个发送窗口的状态需要三个指针,P1,P2,P3,小于P1的是已发送并且已经接收的部分,大于P3的是不允许发送的部分。

再看B的接受窗口。B的接受窗口大小是20,在接收窗口之外,到30号为止的所有数据都已经确认发送过了,并且已经交付给主机了,因此B可以不再保留这些数据。31-50之间的是允许接收的。

在上图中32,33已经被B接收,但是没有按序到达,因为31没有收到,这个时候B只能对按序收到的最高序号给出确认,也就是说现在B返回的报文中确认号仍然是31.

如果B收到了31号,并把31-33交付给主机后,B删除这些数据后,把接收窗口向前移动三个序号,同时给A发送确认,窗口值为20,确认号是34 。A收到后,P1,P3向前移动三个序号,P2不变。

接下来如果A把42-53发送,指针P2和P3重合,窗口内数据发送完成,还没有再收到确认,就需要停止发送

缓存

发送方的应用进程把字节流写入TCP的发送缓存中,接收方的应用程序从TCP的接收缓存中读取字节流

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587209077/computer_network/微信截图_20200418192408_lz3rfp.png

需要明确的是:缓存空间和序号都是有限的,循环使用的。

发送缓存用于暂时存放:

  • 发送应用程序传送给发送方TCP准备发送的数据。
  • TCP已发送但尚未确认的部分。

发送窗口通常只是发送缓存的一部分,已被确认的数据应当从发送缓存中删除,因此发送缓存和发送窗口的后沿是重合的。发送应用程序最后写入发送缓存的字节减去最后被确认的字节,就是还保留在发送缓存中被写入的字节数。

接收缓存用来暂时存放:

  • 按序到达的、尚未被接收应用程序读取的数据
  • 未按序到达的数据。

如果应用程序来不及读取收到的数据,接收缓存最终就会被填满,接收窗口就会减小到0.

在这里还要强调三点

  • 虽然A的发送窗口是根据B的接受窗口设置的,但是同一时刻,A的发送窗口大小并不是和B的接收窗口一样大。因为网络的传输时有延迟的,而且A的发送窗口还要受到网络拥塞状况的限制,当发生拥塞时,应适当减小自己发送窗口的值。
  • 对于不按序到达的数据,TCP通常是先将不按序到达的数据存放在缓存中,等字节流中所缺少的字节收到后,再按序交付到上层的应用进程
  • TCP要求接收方必须有累计确认功能。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时将确认信息顺带捎上,但是确认的推迟时间不应该超过0.5秒。

超时重传时间的选择

TCP的超时重传时间采用了一种自适应算法。它记录一个报文段发出的时间,以及收到相应确认的时间,这两个时间差就是报文段往返时间RTT,TCP保存了RTT的一个加权平均往返时间RTTs ,又称为平滑往返时间

当第一次测量到RTT样本时,RTTs就取值为RTT样本值,接下来每一次受到RTT就重新计算一次:

新的RTTs = (1 - α)* (旧的RTTs) + α * 新的RTT样本

其中的α建议为0.125

显然超时重传时间RTO(RetransmissionTime-Out)应该略大于RTTs

RTO = RTTs + 4 * RTTD

RTTD是RTTs的偏差的加权平均值,第一次测量时,RTTD取值为RTT的一半,接下来采用如下算法:

新的RTTD = (1 - β)*(旧的RTTD)+ β * |RTTs - 新的RTT样本|

β推荐值为0.25

现在有存在一个问题,当设置的重传时间到了,还是没有收到确认,于是重传报文,经过一段时间之后收到了确认,如何确认这个确认是对先前发送的报文的确认还是对重传的报文的确认?,这个对于RTTs计算的影响很大。

对于这种情况,目前的方法是:报文每重传一次,就把RTO增加为两倍

选择确认SACK

如果收到的报文段没有差错,只是没有按序到达,中间还缺少一些序号的数据,能否想个办法只传送缺少的数据而不传送已经正确收到的数据呢?

答案是可以的,就是SACK,这里只是简单讲一下原理。

假设收到的报文序号是1 - 1000,1501 - 3000,3501 - 4500,中间缺少了两部分,如果这些字节的序号都在接收窗口之内,那就先把这些数据收下,但是要想个办法通知发送发我已经收到了。

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587211176/computer_network/微信截图_20200418195848_xt17r1.png

从上图我们可以看出,每一个和前后字节不连续的字节块都有两个边界,图中用L1,R1,L2,R2来表示这四个边界

我们知道,TCP的首部没有那个字段能够填进去这种边界数值,

如果想要使用SACK就要在TCP建立阶段在首部中添加允许SACK选项,如果确认使用,原有的确认号字段用法不变,只是在选项中添加了SACK选项。但是在选项中最多指明4个字节块的信息,因为序号有32位,需要四个字节,一个字节块需要两个序号,8个字节,而同时需要两个字节来指明哪些是SACK选项,4个字节块就会用掉34个字节,而选项最长为40字节,再加一个字节块就会超过。

TCP流量控制

所谓的流量控制就是让发送方不要发送太快,而让接收方来得及接收。

利用滑动窗口可以很方便地实现对发送方的流量控制。

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587211807/computer_network/微信截图_20200418200900_kruofd.png

上图的流程可以总结为:

  • 在连接建立时,B告诉A:我的接收窗口是rwnd = 400,因此发送方的发送窗口不能超过接收方给出的接收窗口个,注意,TCP窗口单位是字节而不是报文段。
  • 设每个报文段的长度为100字节,初始序号为1,ACK是确认位,ack是确认号。
  • B进行了三次流量控制,第一次把rwnd减到300,第二次减到100,最后减到0.

思考一个问题,如果A在发送了零窗口的报文段后不久,B的接受缓存又有了空间,于是向A发送rwnd = 400,但是这个报文丢失了,A一直在等待B发送非零窗口的通知,B也一直在等待A的数据,就会产生死锁。

为了解决这个问题,TCP为每一个连接设置持续计时器,只要TCP一方收到了零窗口通知,就开启计时器,到时就发送一个探测报文,如果返回的结果不是0,就可以打破死锁。

TCP报文段的发送时机

应用进程会把数据传送到TCP的发送缓存中,剩下的任务就交给TCP了。可以用不同的机制去控制TCP报文段的发送时机。

  • 第一种是利用MSS,只要缓存中存放的数据达到了MSS就封装成一个报文段发送出去。
  • 第二种是应用程序指明发送,如PSH操作
  • 第三种就是发送方设置一个计时器,到时后就把缓存中的数据打包发送出去。

拥塞控制

在计算机网络中,链路容量,交换节点中的缓存和处理机等都是网络的资源,当对网络中某一资源的需求超过该资源所能提供的可用部分时,网络性能就要变化,这种情况就叫做拥塞。

拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载

拥塞控制是一个全局性的过程,涉及到所有的主机,路由器以及所有与降低网络传输性能有关的因素,而流量控制往往指的是点对点的通信量的控制,是个端到端的问题

TCP的拥塞控制算法有四种,即我们非常熟悉的慢开始,拥塞避免,快重传和快恢复

发送方维护一个叫做拥塞窗口cwnd的值,拥塞窗口的大小事取决于网络的拥塞程度,并且在动态变化。发送方让自己的发送窗口等于拥塞窗口

发送方控制cwnd的原则是:只要没有出现拥塞,就将cwnd扩大一些,只要出现了拥塞或者可能出现拥塞,就减小cwnd。而判断网络出现拥塞的依据就是出现了超时

原理

我们还用这张经典的图来说明:

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587214008/computer_network/微信截图_20200418204614_kprnwt.png

慢开始的思路是这样的:当主机一开始发送数据的时候,并不清楚网络的负载情况,如果立刻将大量数据注入到网络,就可能发生拥塞,于是就从小到大增大cwnd。

  • 在刚开始发送报文时,先把拥塞窗口cwnd设置为2至4个发送方最大报文段SMSS。

  • 没受到一个确认报文,就把cwnd增大最多一个SMSS。

    cwnd每次的增加量 = min(N, SMSS),N是原先未被确认,刚收到的确认报文段中确认的报文字节数。

我们用下面这个例子说明慢开始的原理,为了方便说明 ,我们用报文段的个数作为cwnd的单位,实际上单位应该是字节数。

  • 一开始发送方设置cwnd为 1,发送第一个报文M1,接收方收到M1后返回确认。发送方收到M1的确认后,将cwnd从1增大为2,接着发送M2和M3,收到二者的确认后,cwnd就从2增大到4,也就是慢开始算法,每经过一个传输轮次,cwnd就加倍。一个传输轮次经过的时间其实就是一个RTT

  • 为了防止cwnd增长过快,还需要设置一个慢开始门限ssthresh

    • 当cwnd < ssthresh时,使用慢开始算法。
    • 当cwnd > ssthresh时,使用拥塞避免算法
    • 当cwnd = ssthresh时,二者均可以。

    拥塞避免的思路是让cwnd的增长变慢,每经过一个RTT就让cwnd加1。当cwnd增长到ssthresh时(图中1点)就从慢开始改为拥塞控制算法。

  • 当出现超时(图中2点)后,发送方判断网络出现拥塞,调整ssthresh = cwnd / 2, cwnd = 1,重新进入慢开始。

  • 有时候发送方会连续收到三个ACK(图中4点),出现这种情况是以为有时候,个别报文段会在网络中丢失,实际上网络并未发生拥塞,如果发送方迟迟未收到确认,就会产生超时,认为发生了拥塞,就会导致发送方错误地开启慢开始。采用快重传算法可以让发送方尽早知道个别报文丢失了

    快重传算法要求接收方不要等待自己发送数据时才稍待确认,而是要立即发送确认。即使收到了失序的报文也要立即发出对已收到报文的重复确认。假设接收方收到了M1和M2,并及时确认,现在假定接收方没有收到M3却收到了M4,接收方累计确认只能发送M2的确认,之后又收到了M5和M6,又分别重发了两次对M2的确认,发送方一共收到了4个对M2的确认,其中3个是重复的,就知道接收方确实没收到M2就应当启动立即重传。

  • 在图中4点,发送方知道只是丢失了个别的报文段,就不执行慢开始,而是执行快恢复,ssthresh = cwnd / 2, cwnd = ssthresh。

上述的过程可以不断调整cwnd的大小,因而发送窗口的大小会受到拥塞程度的控制,但是实际上接收方缓存空间有限,接收方根据自己的接受能力设置了rwnd(接收方窗口,又名通知窗口),并把这值写入了TCP头部的窗口字段,从接受方角度考虑,发送方的发送窗口不能超过rwnd,因此,发送方的发送窗口上限应该是rwnd和cwnd中的最小值

TCP的连接管理

我们从开始就说TCP是面向连接的,它的很多设计也是基于这个前提才能够正常运行的,那么TCP又是如何建立和维持这个连接的呢?

在建立TCP连接过程中要解决三个问题:

  • 要让每一方都能够确认对方的存在
  • 要允许双方商定一些数值,如最大窗口值
  • 能够最运输实体资源进行分配。

TCP连接的建立(三次握手)

TCP建立连接的过程叫做握手,需要客户端和服务器之间交换三个TCP报文

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587215346/computer_network/微信截图_20200418210855_mpcozg.png

  • 最初两端都处于关闭状态,在上图中是A主动打开连接,B被动打开连接。
  • 一开始B的TCP服务器就创建传输控制块TCB,准备接受客户进程的连接,然后服务器就处于LISTIN状态。
  • A的TCP客户端创建TCB,在建立连接之前,首先向B发送连接请求报文,同步位SYN = 1,同时选择一个初始序号seq = x,TCP规定,SYN报文不能携带数据,但是要消耗掉一个序号,TCP客户端进入SYN-SENT状态
  • B收到请求报文后,如果同意建立连接,向A发送确认,将SYN和ACK都置为1,ack置为 x + 1,并为自己选择一个初始序号seq = y,这个报文段也不能携带数据,但是要消耗掉一个序号。
  • A的TCP客户端收到B的确认之后还要向B给出确认,ACK为1,ack = y +1,seq = x + 1,而这个时候,ACK报文段就可以携带数据了。这个时候A已经进入了ESTABLISHED(已建立连接)状态,B收到确认后也进入ESTABLISHED状态。

上述过程叫做三次握手,但是B发送给A的报文段也可以拆分为两个报文段,先发送一个ACK = 1,ack = x + 1,在发送一个同步报文(SYN = 1, seq = y),这样就变成了四次握手。

那么为什么A最后还要发送一次确认呢?主要是为了防止已经失效的连接请求报文突然又到了B。这种情况一般都产生在A发送的第一个连接请求报文超时后,A再次发送了一次,B收到了第二次的请求,完成建立连接并发送数据,之后释放连接以后第一个请求又到了,这个时候如果B没有收到A的第二个确认,是不会管这第一个请求的。

TCP连接的释放(四次挥手)

释放过程与建立过程类似,只是更加复杂,并且使用的头部字段是FIN

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587216170/computer_network/微信截图_20200418212241_j13fkc.png

  • A的应用进程首先向其TCP发送连接释放报文,并停止发送数据,主动关闭TCP连接

  • A将连接释放报文的首部字段FIN置为1,序号seq = u,它等于前面已传送的所有数据最后一个字节的序号加1,此时A进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文不携带数据,但是消耗一个序号。

  • B收到连接释放报文后发出确认,ack = u + 1,自身的序号seq = v,等于B传送的数据最后一个字节的序号加1,然后B进入CLOSE-WAIT(关闭等待)状态。这时TCP进入半关闭状态,即A已经没有数据要发送了,但是B若要发送数据,A可以接受,也就是说从B到A的连接并未关闭

  • A收到B的确认手进入FIN-WAIT-2状态,等待B发出的释放报文。

  • 若B已经没有数据要发送了,其应用进程通知TCP释放连接,这个时候B发送FIN报文,seq = w(假定B在半关闭状态有发送了一些数据),ack仍然为 u + 1.

  • A收到连接释放报文后,必须对此发出确认,ACK = 1,ack = w + 1,seq = u + 1,然后进入TIME-WAIT状态,经过时间等待计时器(TIME-WAIT timer)设置的时间2MSL之后,A进入CLOSED状态,MSL叫做最长报文段寿命(Maximum Segment Lifetime)。

    之所以要等待这个时间,有两个理由:

    • 保证A发送的最后一个ACK报文到达B,这个报文有可能丢失,所以B可能会重传上个FIN报文,这个时候就需要A重传一次确认然后重启2MSL计时器。
    • 防止上面说的已失效连接,等待2MSL可以保证本连接持续时间内所产生的所有报文从网络中消失,
    • 除了时间等待计时器外,还有个保活计时器(keepalive timer),服务器没收到一次客户数据,就重置它,时间是两个小时,如果两小时没收到数据,就发送探测报文,每隔75秒发一个,如果连续10个都没有回复,那就关闭这个连接。

TCP的有限状态机

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1587217099/computer_network/微信截图_20200418213810_lbd6iv.png