目录
1. 流量控制
2. 滑动窗口
2.1 滑动窗口概念
2.2 滑动窗口模型详解
高速重发控制(快重传)
3. 拥塞控制和拥塞窗口
4. 延迟应答
5. 捎带应答
6. 面向字节流
7. 粘包问题
8. 16位紧急指针
9. listen的第二个参数
10. TCP总结+异常情况+与UDP比较
10.1 TCP机制总结
10.2 TCP异常情况
10.3 TCP与DUP比较
本篇完。
上一篇讲了挺多TCP的机制的,重点是三次握手和四次挥手,这一篇续上TCP的各种机制。
1. 流量控制
上一篇也提到过关于流量控制方面的知识,现在正式的看看TCP的流量控制。
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起超时重传等等一系列连锁反应。
TCP支持根据接收端的接收能力来决定发送端的发送速度。这个机制就叫做流量控制(Flow Control)。
和滑动窗口中发送端知道接收端接收能力一样,接收端将自己可以接收的缓冲区大小放入TCP首部中的16位窗口大小中,再通过ACK端告诉发送端自己的接收能力。
窗口大小字段越大,说明网络的吞吐量越高。
如上图所示,主机A先发送了一个数据段,得到的ACK中,窗口大小是3000,表示主机B有三个数据段的接收能力,主机A下次发送数据时,调整为一次发送三个数据段。
由于应用层读取数据缓慢等原因,接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口之后,就会减慢自己的发送速度。
如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据,接收方也不再有ACK确认应答了。
发送端定期发送一个窗口探测数据段,让接收端把窗口大小告诉发送端,一但窗口值不再是0了,发送端就可以额继续发送数据。一旦窗口值是0通信就会暂停在这里了。
16位变量的最大值65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位。有兴趣的小伙伴自行研究首部中的选项。
在TCP首部的6个标志位中有一个PSH标志位(push的意思),该标志位的作用是催促接收端应用层尽快从接收缓冲区中将数据读走。
当接收端的接收能力快为0的时候,在发送数据的时候可以将PSH置1,接收方收到后发现PSH为1,就会尽快将接收缓冲区中的数据拿走,好尽快提高接收能力。
2. 滑动窗口
上篇博客讲解了TCP的确认应答机制:
如上图所示,主机A每发送一个数据段,主机B都要给一个ACK确认应答, 主机A收到ACK后再发送下一个数据段。
这样做有一个比较大的缺点, 就是性能较差,数据段和数据段之间的发送就变成了串行的了,尤其是数据往返的时间较长的时候,效率更低。
为了提高效率,采用一次发送多条数据的方式:
如上图所示,假设一个数据段的大小是1000字节,主机A一次性发送四个数据段,主机B一次给主机A四个ACK确认应答。
2.1 滑动窗口概念
经过前一篇博客学习知道,TCP协议中有超时重传机制,如果主机A在一定的时间内没有收到主机B的确认应答,那么就会触发超时重传,再次将刚刚的数据段发送一遍。
所以,数据段被发送出去以后,不能立马清除,需要再保存一段时间,直到收到对端的确认应答,这样是为了以防超时重传时再次发送。
那么在收到确认应答之前这些暂存的数据段是存放在哪里的呢?答案是存放在发送缓冲区中。
如上图所示,用户层将数据send到TCP的发送缓冲区,发送缓冲区会存在大量的数据,需要操作系统在合适的时候发出去,由于TCP是面向字节流的,所以势必不会一次性将发送缓冲区中的数据都发出去。
此时就会导致发送缓冲区中的数据有三种不同的状态,同时也将发送缓冲区分成了三部分:
① 已经发送并且收到ACK的数据。如上图序号①所示,这个区域的数据是已经发送了,并且收到了ACK确认应答的数据,说明这些数据对方完全收到了,就没有存在的必要了,所以新数据来了以后会将其覆盖。
② 已经发送但是没有收到ACK的数据。如上图序号②所示,这个区域的数据是已经发送了,但是还没有收到ACK确认应答,说明这部分数据对方可能没有收到,也可能对方的ACK确认应答信号自己没有收到。当触发超时重传后,这部数据会被再次发送,所以这部分数据不能被覆盖,也不能被清除。发送缓冲区中,这个存放已经发送但是没有收到ACK数据的区域就是滑动窗口。
③ 没有发送的数据。如上如序号③所示,这个区域的数据还没有发送,更谈不上有没有ACK确认应答。
2.2 滑动窗口模型详解
下面通过一个模型来完善对滑动窗口的理解:
如上图所示,内核中的发送缓冲区可以看成是一个char outbuffer[N]数组,存在win_start和win_end两个指针来标识滑动窗口的范围,窗口滑动的本质就是数组下标的更新。
窗口最开始的大小是如何设定的?在TCP协议的报头中有一个16位的窗口大小,该值就是滑动窗口的大小。在通信双方进行三次握手建立连接的过程中,接收方将自己的接收能力告诉了发送方(起初是整个接收缓冲区的大小),也就是发送方在发送数据之前就确定了滑动窗口tcp_win(接收能力)的大小。
伪代码:
win_start = 0;
win_end = win_start + tcp_win(接收能力);
更新后:
win start = 收到的应答报文中的确认序号;
win end = win start + 收到的应答报文中的窗口大小
所以最开始,滑动窗口的大小是从发送缓冲区起始位置开始的tcp_win个字节,也就是对方通告我的接收能力大小。
窗口一定是向右滑动吗?会不会向左滑动?
如上图所示,假设现在滑动窗口的大小是4个数据段,也就是4000个字节,主机A先发送一个数据段(1001~2000),当主机B收到并且返回ACK时,其中确认序号是2001。
确认序号表示发送端下次发送从这个序号的位置开始发送即可。
此时原本滑动窗口中的第一个数据段就变成了已经发送并且收到ACK的数据,可以被覆盖了,所以滑动窗口继续向右滑动,第一个数据变成了原本的第二个数据段(2001~3000)。
由于数据的发送是从左到右的,数据滑动窗口的滑动方向也是从左到右的,不会向左滑动。
所以滑动窗口一定不会向左滑动,
窗口不一定是向右滑动(左侧边界可以右移,右侧边界没人拿数据就不右移)(下图能理解)。
窗口大小会一直不变吗?变的依据是什么?
如上图所示,假设发送缓冲区中一个黑色小框是一个数据段(大小是1000字节),在没有收到ACK前,滑动窗口的大小是5个数据段。
确认序号是1001,说明0~1000的数据段对方收到了,下次从1001处开始发送,所以滑动窗口的win_start向右滑动一个数据段,指向1001处。
除此之外,由于对方的接收缓冲区应用层没有读数据,再加上又有新数据到来,所以接收能力下降了,应答信号中的16位窗口大小表示对方此时的接收能力是3000,所以此时滑动窗口的大小就要发生变化。
win_end = win_start + tcp_win(3000)得到的就是新滑动窗口的结束位置,此时滑动窗口相比原来变小了。这也是窗口不一定是向右滑动(左侧边界可以右移,右侧边界没人拿数据就不右移)。
同样的,也有可能对方在发送这次ACK确认应答的时候,应用层恰好把整个接收缓冲区的数据都读走了,此时接收能力就变大了,确认应答信号中表示接收能力的16位窗口大小的值也比之前要大。
虽然win_start在向右移动,但是win_end = win_start + tcp_win向右移动的更多,所以此时滑动窗口和原来相比变的更大了。
滑动窗口变化大小的依据是对方接收能力的大小。
收到的确认应答不是滑动窗口最左边数据的确认应答,而是中间的,或者结尾的,要滑动吗?
发送数据的顺序是从左到右的,理论上收到确认应答的顺序也是从左到右的,收到不是最左边数据的确认应答的情况,一定是丢包了。
丢包又有两种情况,一种情况是发送的数据没有丢,对方也收到了,只是对方的确认应答信号丢了,如下图:
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。
ACK确认序号:该序号前的所有数据全部都收到了,下次从该序号处开始发送。
如上图所示的情况中,即使确认序号为1001的ACK丢了,但是确认序号为2001的ACK没有丢。收到2001的ACK后就知道1~1000的数据也收到了,滑动窗口可以直接向右移动两个数据段。
高速重发控制(快重传)
第二种情况就是发送的数据丢了,对方没有收到,如下图:
如上图所示的情况中,1~1000的数据段丢失之后,发送端会一直收到确认序号为1001 的ACK,就像是在提醒发送端 “我想要的是1001” 一样。如果主机A连续三次收到了同样一个确认序号是1001的应答,就会将包含1001的数据段(1001~2000)重新发送。这个时候收到了1001~2000的数据段之后,返回ACK的确认序号就是7001了。因为2001 - 7000其实之前就已经收到了,并且被放到了接收端操作系统的内核接收缓冲区中。
这种重传机制被称为高速重发控制。也被叫做快重传。
滑动窗口必须要滑动吗?会不会不动?大小会不会变为0?
滑动窗口是否滑动的依据是ACK中的确认序号。
当发生丢包等情况时,滑动窗口是不会发生滑动的,因为无法确定对方是否收到了发生的数据,此时就会保持不动,等待下一步策略执行。
当对方的接收缓冲区满了,并且应用层没有读走数据时,此时接收能力就是0,所以ACK确认应答中的16为窗口大小也是0,此时滑动窗口大小就会变成0。
滑动窗口向右滑动,如果空间不够了会越界吗?
滑动窗口如果一直向右滑动,当发生缓冲区的空间不够时,滑动窗口会不会越界呢?答案是不会的。因为实际上发送缓冲区被操作系统组织成了环状结构,所以滑动窗口无论怎么滑动都不会越界。(下标就像环形队列一样进行模运算)
理解最后一个问题当成环装,之前的问题把滑动窗口当成线性的就可以了。
流量控制和滑动窗口分别主要解决效率问题还是可靠性问题?
流量控制主要解决可靠性问题(更多的是防止放送过多的数据)。
滑动窗口主要解决效率问题(一次传达大量数据)。
(都有解决,只是主和辅的关系)
3. 拥塞控制和拥塞窗口
前面学的TCP的机制都只考虑了两个主机端的问题,没有考虑网络,下面带上网络看看。
如上图所示,此时客户端要发送1000个数据段,服务端接收到以后返回了ACK确认应答,客户端根据确认序号发现丢了两个数据段,直接使用快重传或者超时重传机制重新发送这两个数据段即可。
但是如果有999个数据段丢了,服务端只收到一个数据段,那么此时就是网络出问题了,所以导致大量数据没有发出去。
少量的丢包,仅仅是触发超时重传,大量的丢包,就会认为是网络拥塞。
客户端对于这999个发送失败的数据该如何处理呢?
如果也进行重传,那么就会让已经出现问题的网络雪上加霜。并且一个局域网中不止你一个客户端在发数据,如果都采用这种重传方式,那么整个网络中就会存在大量拥塞的数据,使网络问题更加严重。
遇到网络拥塞时,不能使用超时重传机制,而应该使用拥塞控制的策略。
先不管拥塞控制是什么,从TCP有这一机制就可以看出,TCP的可靠性不仅仅考虑了双方主机的问题,还考虑了网络的问题。
TCP引入慢启动机制来实现拥塞控制:当发送网络拥塞后,先发少量的数据,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
如上图所示,当网络发生拥塞时,主机A先发送一个数据段,摸清当前的网络拥堵状态,如果收到这个数据段的ACK,再将发送数据段个数增加。
此处引入一个拥塞窗口的概念。表示会产生网络拥塞的数据量。
在网络拥塞后,第一次发送数据时,将拥塞窗口的大小设置为1,即一个数据段(1000)的大小,每次收到一个ACK应答,拥塞窗口加1。
再次发送时按照拥塞窗口的大小来发,这一就会导致发送数据段的个数按照指数级增长,每次都是前一次的二倍。
之所以称这种方式是慢启动,是因为最开始只发送一个数据段,开始数据量的增长确实慢,但是这是指数级增长,后面数据量的增长就会越来越快。
慢启动的方式,可以让发送方发送的数据量快速恢复到正常水平。这种快速恢复提高了网络通信的效率。
但是不能让这种增长速度不断增加下去,否则就导致非常恐怖的数据量,所以不能使拥塞窗口单纯的加倍。
再引入一个概念:慢启动的阈值。
当以指数增长的拥塞窗口大小大于这个阈值的时候,拥塞窗口不再按照指数级增长,而是按照线性方式增长。
拥塞窗口随传输轮次变化的示意图:
- 网络拥塞控制机制触发,势必是因为已经发生了网络拥塞,当上一次网络拥塞发生时,阈值大小为16,当发送方以慢启动方式开始发送数据后,拥塞窗口按照指数级增长到16后变成了线性增长。
- 拥塞窗口线性增长到24以后,再次发生了网络拥塞,因为在这个过程中,发送的数据量也在不断增加。此时将阈值更新为24的一半12。
- 然后发送方再以慢启动的方式发送数据,拥塞窗口变成12以后再线性增长,直到发送网络拥塞,再次更新阈值。
- 如此反复,不断更新阈值和拥塞窗口的最大值,以便试探出当前网络状况下效率最高的数据传输量(阈值和拥塞窗口最大值不再变化)。
如果说在拥塞控制的过程中,网络状况恢复了,那么拥塞窗口就会一直增长下去,发送数据量也在增长,直到当前良好网络状况极限吗?
接收方也是有接收能力限制的,就算网络情况再好,发送方也不以超出接收方接收能力数据量来通信。
发送方在每次发送数据的时候,会将拥塞窗口的大小和接收端反馈接受能力大小作比较,取较小值作为实际发送的数据量。滑动窗口大小 = min(拥塞窗口大小,对方接收能力大小)(较小值)。
所以在网络状况良好的情况下,发送方的滑动窗口大小取决于接收方的接收能力,在网络拥塞的情况下,发送方的滑动窗口大小取决于拥塞窗口的大小。
当TCP开始启动的时候,慢启动的阈值等于滑动窗口的最大值,如果网络状况良好,那么拥塞窗口在增加到大于接收方的接收能力后变不再增加。
如果发送了网络拥塞后,慢启动阈值就会变成原来的一半,同时拥塞窗口置回1(最小值),逐渐找到最合适的拥塞窗口值和阈值。
当TCP通信开始后,网络吞吐量会逐渐上升。随着网络发生拥堵,吞吐量会立刻下降。
4. 延迟应答
假设接收端缓冲区大小为1MB,一次收到了500KB的数据,如果立刻应答,返回的窗口大小就是500KB。
但实际上可能接收端处理接收缓冲区中数据的速度很快, 10ms之内就把500KB数据从缓冲区消费掉了,并且接收端处理速度还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1MB了。
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
所有的数据报都采用延迟应答的方案吗?肯定不是。常用的方案有两种:
- 数量限制:每隔N个包就应答一次。
- 时间限制:超过最大延迟时间就应答一次。
具体的数量和超时时间,依操作系统不同也有差异。一般N取2,超时时间取200ms。
如上图所示,延迟应答的最终表现,就是隔几个数据段确认应答一次。
5. 捎带应答
前面讲解协议格式的时候,说协议首部中有两个序号是为了实现全双工,也就是让应答和数据在一个数据段内。
即使有延迟应答,但是很多情况下,通信双方 “一发一收” 的,虽然是收到多个数据段应答一次,但是应答终究还是只有应答,也就是只有确认序号和ACK标志位,没有数据。
为了通信效率更高,完全可以将确认应答和接收端要发送的数据放在一个数据段中发送出去,发送端收到后既可以知道接收端收到了自己数据,又收到了接收端发送的数据。
如上图所示,确认应答信号中的确认序号和ACK标志位,坐着数据的顺风车就发送出去了,也就是在通信的过程中,确认应答就被捎带给对方了。
早看看四次挥手:
在中间的两次挥手,服务端就可能把ACK和FIN放在一个数据段中发送出去所,所以有时候也称四次挥手为“三次挥手”。
6. 面向字节流
在学C语言文件操作的时候,就听到过面向字节流,在学C++的时候同样也听到过面向字节流,在UDP和TCP的学习中,更是多次见到面向字节流,那么面向字节流到底是什么?
创建一个TCP的socket时,操作系统会同时在内核中创建一个发送缓冲区 和一个接收缓冲区。
应用层在调用write或者send时,数据会先写入发送缓冲区中,如果发送的字节数太长,会被拆分成多个TCP的数据包发出。
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用层可以调用read或者recv从接收缓冲区拿数据。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,如:写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。
简而言之就是,应用层和TCP层是完全独立的,应用层写的时候不用考虑TCP层的缓冲区是否满,应用层读的时候,也不需要考虑缓冲区中的数据是怎么样的,直接读就可以。
至于应用层写入或者读取的是否是一个完整的报文,TCP层的缓冲区无法保证,需要由用户层自己处理。
与面向字节流相对的就是面向用户数据报的UDP,UDP协议用户层写入就是一个完整的报文然后给到UDP层,UDP层并不会缓存,而且增加相应的首部后直接发送出去,发送的上一个完整的数据段。
接收方应用层读取的时候,从接收缓存区中读取到的内容也是一个完整的数据段,不能分多次读。
7. 粘包问题
粘包问题中的 “包”,是指的应用层的数据包。
在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个32位序号的字段。站在传输层的角度,TCP是一个一个数据段过来的,按照序号排好序放在缓冲区中。
站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包,此时应用层在读取数据的时候就会产生粘包问题,可能读取的不是一个完整报文,也可能是一个半报文等等情况。
粘包的问题是指不同报文一起发送到网络上,则当写的缓冲区当中存在不同的报文的时候,就会发生粘包问题。
可能会触发TCP粘包的情况:
- 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包
- 接收方不及时读取套接字缓冲区数据,这将发生粘包
解决这个问题,归根到底就是要明确两个包之间的边界。
解决粘包问题:
使用短连接来解决TCP粘包的问题
缓解粘包问题:
- 关闭nagle算法能够缓解TCP粘包问题(nagle算法可以很大程度的利用网络带宽,nagle算法其中之一的规则为:如果该包含有FIN,则允许发送)
- 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容
- 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符
在之前博客的协议定制中,采用的是TCP协议,在应用层读取接收缓冲区数据的时候,通过用户层代码来保证每次读取到的是一个完整报文。
每个报文都有一个报头,报头中的内容就是有效载荷的长度,这是由程序员定义的用户层协议。
对于UDP协议来说,就不存在粘包问题:
如果还没有上层交付数据,UDP的报文长度仍然在在首部中。同时,UDP是一个一个把数据报交付给应用层就有很明确的数据边界。 站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个"的情
8. 16位紧急指针
TCP协议中缓冲区的数据是按照顺序发送的,接收缓冲区也是按照顺序来接收的,但是如果我们想让某个数据插队呢?让这个数据提前被接收端处理,而不是按照顺序来,此时就用到了紧急指针。
再看看TCP的协议格式:
六个标志位中的URG(urgent紧急的)表示紧急指针是否有效,16位紧急指针指向数据段中具体的某个数。
我们知道缓冲区本质上就是一个char类型的数组,如上图所示,而紧急指针是一个16位的变量,所以它也是一个值,范围是0~65535,这个值其实就是缓冲区中有效载荷的偏移量。
一个紧急指针只能指向缓存区中一个数据。
当发送端想让某个数据插队时,就将URG标志位置1,然后让16位紧急指针指向这个紧急数据,再将数据段发送出去。
当接收端收到数据段后,发现URG置1了,说明有紧急指针,有数据需要优先处理,然后再去16位紧急指针字段中拿到紧急数据的位置,然后先读取这个紧急数据。
man send
在之前我们使用send以及recv的时候,最后一个参数是flags,之前我们都是设为0的,如果将其设置为MSG_OOB就表示发送或者接收的数据中存在紧急指针,也就是将URG标志位置1了。
看到最后一行的sendmsg,第二个参数是msg结构体指针的系统调用,这个结构体中包含紧急指针的位置,也就是16为紧急指针位段。
紧急指针的应用场景非常少,一般应用在紧急获取对端状态的场景,比如说客户端给服务端发送了很多条TCP请求,服务端都没有给回应答,客户端就可以发一个紧急数据确认服务端的状态。
9. listen的第二个参数
man listen
之前创建TCP套接字的时候,没有讲解listen系统调用的第二个参数backlog,只是说随便设置一个数,不要太大(当时定义了一个全局的20)。
int backlog表示全连接长度。
如上图所示,服务器进程中有一个listen状态的套接字用来监听,还有多个recv后的套接字来进行真正的通信。
系统资源是有限的,当用于通信的套接字数量达到限制以后,系统就无法再维护更多套接字了,新来的已经建立连接的套接字就会由于资源不足而被关闭。
当服务中某个或者几个用于通信的套接字使用完毕后,就会释放出一部分系统资源,此时也没有新的连接到来,那么这部分系统资源就会空闲着。
所以TCP协议维护了一个全连接队列,如上图蓝色部分所示,这个队列中放的是处于等待状态的并且已经完成三次握手建立连接的套接字。(就像在一些餐厅外面排队一样,餐厅里已经满人了)
当系统资源不足时,就在全连接队列中等待,当系统资源有空余时,全连接队列中的一个套接字就会被系统维护,进行网络通信。
此时系统资源一空闲出来就会立刻被全连接队列中的套接字使用,不会再出现空闲状态,充分利用了系统资源,提高了效率。
而listen的第二个参数backlog就是用来指定全连接队列的长度,具体长度等于backlog +1 。为什么说backlog值不能太大又不能没有呢?
不能没有的原因就是就像面所说的,要让系统资源一有空余就有新的套接字被系统维护,提高系统资源利用率。
不能太大是因为,维护全连接队列也要消耗系统资源,如果全连接队列太长,所耗费的资源完全够系统再维护一个套接字用来通信了,属于是捡了芝麻丢了西瓜的做法。除此之外,全连接队列太长,里面的套接字等待的时间也会很长,此时又会触发超时重传机制等,进而导致其他问题。
将之前写的TCP网络通信代码的backlog值设置为1,也就是将listen的第二个参数设置为1,此时全连接的长度为2:
服务器不accept(循环里只有睡眠一秒):
将服务器运行起来,此时服务器不会accept新的连接,所以当新的连接到来时,被会放入全连接队列中等待,而设置的全连接队列长度是2,所以最多放两个建立连接的套接字。
创建两个Xshell窗口,充当两个服务端,使用telnet工具充当两个客户端与服务器进行连接:
此时全连接队列中放的就是端口号为53922和53918的两个客户端telnet进程。
全连接队列中的套接字状态是ESTABLISHED,说明是完成了三次握手的,已经和服务器建立了连接,只是服务器没有进行维护进行通信。
此时再增加一个客户端,此时一共有3个telnet和服务器建立连接:
再次查看网络状态,发现服务器端套接字的状态是SYN_RECV。
如上图所示,在三次握手的过程中,服务端第一次收到客户端的SYN请求以后,服务端套接字的状态就变成了SYN_RECV,只有服务端也发送SYN+ACK以后,再收到客户端的ACK以后,服务端才会变成ESTABLISHED,表示连接建立。
而上面过程中,第三个客户端发起连接请求后,服务端套接字停在了SYN_RECV状态,说明服务端已经收到了客户端的三次握手请求,但是此时全连接队列已经满了,服务端没有更多资源来维护这个套接字了,所以不向客户端发起SYN连接请求。
处于SYN_RECV状态的套接字叫做半连接状态。
对于半连接状态的套接字,操作系统同样维护着一个半连接队列,里面放着的是处于SYN_SNET和SYN_RECV等半连接状态的套接字。
此时客户端telnet的状态是ESTABLISHED,也就是说客户端是认为建立了连接的,但是服务端没有建立,所以这次通信的建立是失败的。 当客户端发送数据的时候,发现服务端没有对应的套接字,就会发起RST,请求重新建立连接。
等待一段时间后再次查看网络状态,发现服务器端处于SYN_RECV状态的半连接套接字没有了,而客户端的ESTABLISHED状态的套接字仍然存在。
处于半连接状态的套接字,在一定时间内没有建立连接变成ESTABLISHED状态,操作系统就会将这个套接字释放掉。
半链接队列:用来保存处于SYN_SENT和SYN_RECV状态的套接字。
全连接队列:用来保存处于ESTABLISHED状态的套接字,但是应用层没有调用accept获取。
全连接队列满了的时候,就无法继续让当前连接的状态进入ESTABLISH状态了。
10. TCP总结+异常情况+与UDP比较
10.1 TCP机制总结
TCP非常的复杂,有众多的机制来保证它的可靠性和提高性能。
保证可靠性机制:校验和,序列号,确认应答,超时重传,连接管理,流量控制,拥塞控制。
提高性能的机制:滑动窗口,快速重传,延迟应答,捎带应答。
其他机制:超时重传定时器,保活定时器,TIME_WAIT定时器。
常见的基于TCP的应用协议:HTTP,HTTPS,SSH,Telient,FTP,SMTP,以及前面自己定制的应用层协议。
10.2 TCP异常情况
①进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别。
一个进程终止后,操作系统会释放这个进程的所有资源,包括文件描述符表,会自动调用close关闭对应的文件,当TCP套接字被关闭时,同样会发起四次挥手请求,和正常关闭没有区别。
②机器重启:和进程终止的情况相同。
服务器主机主动关机,会导致连接发生异常。我们平时在关机的时候,会提示有什么什么进程没有结束,要我们强制结束,这个时候也是在终止进程,也会发起四次挥手请求。
③机器断电/网线断开:此时就是一种真正异常情况。
发送端来不及发起四次挥手请求,接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了, 就会进行RST(reset复位,重置)。
首部的六个标志位中有一个RST标志位,该标志位是用来请求重连的。当发送端在触发超时重发机制后,仍然无法将数据发送到对方,就会发送一个带有RST的数据段请求,请求和对方重新发起三次握手建立连接。
若向已经接收到RST的sock继续写入数据,则内核会向该进程发送一个SIGPIPE信号,该信号默认为中止进程。且写操作返回错误EPIPE
服务器主机崩溃后重启后,客户端发送送数据时,会收到一个RST,则read返回ECONNREST
以下四种情况会发送RST包:
1、端口未打开
2、请求超时
3、提前关闭
4、在一个已关闭的socket上收到数据
即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。
另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态,例如QQ,在QQ断线之后,也会定期尝试重新连接。
10.3 TCP与DUP比较
TCP是可靠连接,那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点,不能简单,绝对地进行比较。
TCP用于可靠传输的情况,应用于文件传输, ,重要状态更新等场景。
UDP用于对高速传输和实时性要求较高的通信领域,例如,早期的QQ,视频传输等。另外UDP可以用于广播。
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。
面试时经常会被问怎么用UDP实现可靠传输
参考TCP的可靠性机制回答就行了,在应用层实现类似的逻辑,
例如:
引入序列号,保证数据顺序、
引入确认应答,确保对端收到了数据、
引入超时重传,如果隔一段时间没有应答,就重发数据、
引入滑动窗口,发送方只有在接收到确认消息后才能发送下一个数据包,接收方只有在接收到正确的数据包后才能向上层应用交付数据、
......
应用层协议:在应用层实现可靠传输,例如FTP、TFTP等协议都是基于UDP实现的,但是在应用层加入了可靠传输的机制。
本篇完。
TCP协议到此就结束了,下一篇:9(应用层和传输层_笔试选择题)。
下下篇:网络层_IP协议等。