TCP
TCP详解【转载】
关于TCP协议
tcp协议全称:传输控制协议,顾名思义,就是要对数据的传输进行一定的控制
image
下面来分析每部分的含义和作用
- 源端口号/目的端口号: 表示数据从哪个进程来,到哪个进程去
- 32位序号: tcp协议在发送数据的时会根据实际传输能力把数据分割成若干数据段,然后这32位序号就是为这个数据段打上标记,以便数据接收到之后方便重组和检查是否缺失
- 32位确认序号: tcp是全双工的,意思就是A向B传数据的同时B也能向A传输数据。而这个序号就是用来给A填充进报文确认的。
- 4位首部长度: 表示TCP报文有多少个4字节
- 6位保留: 顾名思义,先保留着,以防会用到
- 6位标志位:
URG: 标记紧急指针是否有效
ACK: 标记确认序号是否有效
PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区中读走
RST: 要求重新建立连接,我们把含有RST的报文称为复位报文
SYN: 请求建立连接,我们把含有SYN表示的报文称为同步报文
FIN: 通知对端,本端即将关闭。我们把含有FIN标识的报文称为结束报文
- 16位窗口大小:
- 16位校验和 : 由发送端填充,有CRC校验等. 如果接收端校验不通过, 则认为数据有问题.
- 16位紧急指针: 用来标识哪部分数据是紧急数据.
TCP的三次握手
image刚开始,客户端和服务端处于CLOSE状态。然后客户端向服务端主动发起请求,服务端被动接收链接请求。
-
TCP服务器进程先创建传输控制块TCB,时刻准备接受客户端进程的连接要求,此时的服务端就进入了LISTEN状态
-
TCP客户端进程也是先创建控制块TCB,然后向服务器发出请求报文,此时报文首部中同步标志SYN=1,同时选择一个初始序号seq=x,此时,TCP客户端进入了SYN-SENT(同步已发送)状态。TCP规定,SYN报文(SYN=1的报文不能携带数据),但需要消耗一个序列号
-
TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中的ACK=1,SYN=1,确认序号(ack)是x +1,同时也为自己初始化一个序列号seq=y,
此时TCP服务器进程进入SYN-RCVD(同步收到)状态。该状态下也是不能携带数据的,也要消耗一个序列号 -
TCP客户端进程收到确认后,还要向服务器发出确认。确认报文ACK=1,确认序号ack=y+1,自己的序列号是x+1
此时,tcp连接建立,客户端进入ESTABLISHED(已建立连接)状态,当服务器收到客户端的确认后也进入ESTABLISHED状态,之后双方就可以开始通信了
小答疑
- 关于握手为什么不能两次
为了防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。我们可以设想一下这个场景:
假设,客户端在最开始的时候发送了一段请求连接的报文,但是因为网络不好,在网络中滞留太久。然后TCP客户端因为太久没有收到确认报文,以为服务器没有收到,然后又重新发了一份报文,结果这份成功了,然后建立了连接。可是在连接成功后,那一份在网络滞留的报文突然又好了,到达了服务器,导致服务器再次建立了连接。
这样就导致了服务器的不必要错误和浪费资源
所以如果采用的是三次握手。就算那一份失效的报文重新又发送了,但是因为客户端不会再发送确认报文了,而服务端接收不到确认报文,就会认为客户端并没有请求连接
TCP的四次挥手
数据传输完毕之后,双方都可以释放连接了,此时客户端和服务端都处于ESTABLISHED(已建立状态),然后客户端主动断开连接,服务端被动断开连接
- 客户端进程发出释放报文,并且停止发送数据。
释放数据报文首部,FIN=1,序列号seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时客户端进入FIN-WAIT-1(终止等待1)状态。此时需要消耗一个序号 - 服务器收到连接释放报文,通知高层应用程序,客户端向服务端的方向就释放了,发出确认报文,ACK=1,确认序号为u+1,并且带上自己打的序号seq=v,此时服务端进入close-wait(关闭状态)状态
此时的服务端处于半关闭状态。意思就是此时客户端已经不发送数据了,但是如果客户端发送,服务端也能接受。这个状态会持续一段时间(整个CLOSE-WAIT的时间) - 客户端收到服务器的确认请求后,此时客户端进入FIN-WAIT-2(终止等待2),等待服务器发送
连接释放报文
(在此之前需要接受服务器发送的最终数据) - 服务器将最后的数据发送完毕后,就像客户端发送
连接释放报文
,FIN=1,确认序列号ack=v+1,由于处于半关闭状态服务器很可能又发送一些数据,假定此时的序列号为seq=w。服务器进入了LASK-ACK(最后确认)状态,等待客户端的确认 - 客户端收到服务器的
连接释放报文
后,必须发出确认ACK=1,确认序列号seq=w+1,而自己的系列号u+1。
此时的客户端就进入了TIME-WAIT(时间等待)状态。
不过此时的TCP连接还没有释放,必须经过2*MLS(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。 - 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样撤销TCB后,就结束了TCP连接。
小答疑
- 为什么最后客户端还要等待2*MSL的时间呢?
(1) 保证客户端发送的最后一个ACK报文能够到达服务器。因为这个ACK报文可能丢失,站在服务器的角度看来。我服务器已经发送了FIN+ACK报文请求断开了,客户端还没给我回应,我猜测我自己的报文它没有收到,于是我又重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应,并且会重启*2MSL计时器。
(2) 防止类似三次握手中的“网络滞留导致误以为报文失效”的情况。客户端在发出最后一个确认报文之后,在2MSL时间中,就可以使本连接持续的时间内所有的报文段都从网络中小时。这样新的连接中,就不会出现旧的请求报文了。
- 为什么建立连接时三次握手,关闭连接缺失四次挥手呢
建立连接的时候,服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端
而关闭连接时,服务器收到对方的FIN报文时,仅仅标识对方不在发送数据了,但是还能接受数据
而自己也未必全部数据都发给对方了,所以我可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方表示同意现在关闭连接因此,己方ACK和FIN报文一般都是分开发送的,从而导致多了一次
- 如果已经建立了连接,但是客户端突发故障怎么办?
TCP设有一个保活计时器,显然客户端如果出现故障,服务器不可能一直等下去,白白浪费资源。服务器每收到一次客户端请求后都会重新复位这个计时器,时间通常设置再2小时。如果两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文,以后每隔75分钟就会发送一次。若一连发送10个探测报文还没反应,服务器就会认为客户端出现了故障,接着就会关闭连接。
确认应答机制(ACK机制)
TCP将每个字节的数据都进行了编号,即为序列号。
每一次ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到哪些数据了,下一次你要从哪里开始发
比如:客户端向服务器发送了1005字节的数据,服务器返回给客户端的确认序列号是1003,那么说明服务器只收到了1-1002的数据
1003,1004,1005 都没收到
此时客户端就会从1003开始重新发送
超时重传机制
image主机A向主机B发送数据之后,可能因为网络原因,数据B无法到达主机B。如果主机A在一个特定的时间内没有收到确认应答,就会进行重新发送。
但是也有可能是因为B的确认应答丢失了
image
这种情况下,B主机可能会有很多重复的数据,但是利用前面提到的序列号,就可以轻松完成去重的工作。
- 关于超时的时间是如何规定的
TCP为了保证任何环境下都能保持较高的性能通信,因此会采用动态计算这个最大的时间
滑动窗口
如上文说描述,如果每次发送一条数据,都需要等待一个ACK应答,这样的通信效率会比较低。
故而能不能一次发出多条数据,再统一应答呢,答案是有的。
于是就有了一个新得概念窗口
窗口大小指的是无需等待确认应答就可以继续发送数据的最大值。
image窗口在发送前四个段的时候不需要等待ACK直接发送
等收到第一个ACK确认应答后,窗口会向后移动。这个窗口不断向后移动的行为叫做滑动窗口
操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答
只有ACK确认应答过的数据, 才能从缓冲区删掉。
这里存在一个问题,万一丢包了咋办?
-
第一种情况:数据包已经收到了但是确认应答ACK丢失了
image
这种情况下, 部分ACK丢失并无大碍, 因为还可以通过后续的ACK来确认对方已经收到了哪些数据包.
- 数据包丢失
image
当某一段报文丢失后,发送端会一直收到1001这样的ACK,意思就是提醒发送端缺失了1001
如果发送端连续收到3次同样的1001,发送端就会将对应的1001-2000部分重新发送
这个时候服务端接收到了1001之后,再次返回的ACK就是7001了,因为,2001-7000接收端已经是接收成功了的,被放到了接收端操作系统内核的缓冲区中。
这种机制被称为 "高速重发控制" 也叫(重传)
流量控制
因为接收端处理数据的能力是有限的,如果发送端发送太快,导致接收端的缓冲区域被填满,而此时发送端继续发送就会造成丢包,从而导致重传等现象。
因此TCP支持根据接收端的处理能力,决定发送端的发送速度
这个机制就是流量控制(Flow Control)
接收端会把自己可以接收的缓冲区大小放入TCP首部中的 "窗口大小" 字段中。通过ACK通知发送端。
窗口大小越大,说明网络的吞吐量越高
接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知发送端
发送端接收到这个窗口大小通知之后,就会减慢自己的发送速度
如果缓冲区满了,就会将窗口大小设置为0
这是发送发将不再发送数据,但是需要定期发送一个窗口探测数据段,让接收端把窗口大小再告诉发送端
- 那么接收端是如何把窗口大小告诉发送端的呢?
其实在TCP首部中,有一个16位窗口大小的字段,就存放了窗口大小的信息
拥塞控制
虽然TCP有了滑动窗口,能够高效的发送大量数据,但是如果在刚开始就发送大量数据,很容易引发一些问题。
比如说,有些计算机当前网络本身已经处于拥堵状态,而如果此时再向他发送大量数据很可能会更加堵。
(我个人理解这个行为其实就是,比如你去游泳,你肯定是要先用脚去试试水的温度,而不能直接就跳到水里面。)
因此TCP引入了慢启动机制,先发少量的数据,探测一下,摸清当前网络环境之后再决定按照多大的数据传输数据
在此再引入一个概念:拥塞窗口s
- 发送开始的时候,定义拥塞窗口大小为1
- 每次收到一个ACK应答,拥塞窗口加1
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取小的值作为实际发送的窗口
像上面这样的拥塞窗口增长速度, 是指数级别的.
慢启动,只是一开始慢,但是增长速度是极快的。
但是为了不让它增长太快,又有一个叫做慢启动阈值的东西,当拥塞窗口大小超过这个阈值的时候,不再按指数方式增长了,而是按照线性方式增长
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小
假设,接收端的缓冲区大小为1M,一次收到500k的数据
如果立即应答,那么窗口大小就是500k
但实际上,接收端处理数据能力很快,10ms内就能把这500k的数据处理掉。从而返回更大的窗口值。
所以延迟应答就是为了让返回的窗口值大一些。
因为我们直到,窗口值越大,网络的吞吐量越大,传输的效率就高
TCP的目标是再保证网络不拥堵的情况下,尽可能提高传输效率
但是也并不是所有的数据包都可以延迟应答的。有两个限制
- 数量限制:每隔N个包就应答一次
- 时间限制: 超过最大延迟时间就应答一次
具体的数量和时间,依据操作系统的而异。
一般数量是2, 最大延迟时间是200ms
捎带应答
在延迟应答的基础上, 我们发现, 很多情况下
客户端和服务器在应用层也是 “一发一收” 的
意味着客户端给服务器说了 “How are you”
服务器也会给客户端回一个 “Fine, thank you”
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端
面向字节流
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区
调用write时,数据会先写入发送缓冲区
如果发送的字节数太大,会呗拆成多个TCP的数据包发出
如果太小,就会先放在缓冲区中,等到缓冲区大小差不多了,或者其他适合的实际再一起发送
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区
然后应用程序可以调用read从接收缓冲区拿数据
另一方面,TCP的一个连接既有发送缓冲区也有接收缓冲区
对于这么一个连接,它既可以读数据,又能写数据,就叫做全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配了。
TCP异常情况
- 进程终止: 进程终止会释放文件描述符,仍然可以发送FIN和正常关闭没什么区别
- 机器重启: 和进程终止的情况相同
- 机器掉线/网络断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放
- 另外,应用层的某些协议,也有一些这样的检测机制
例如HTTP长连接中, 也会定期检测对方的状态.
例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
总结
为啥TCP这么复杂?
因为既要保证可靠性,同时又要尽可能提高性能
保证可靠性的机制
校验号
序列号(按序到达)
确认应答
超时重传
连接管理
流量控制
拥塞控制
提高性能的机制
滑动窗口
快速重传
延迟应答
捎带应答
定时器
超时重传定时器
保活定时器
TIME_WAIT定时器
以上,还有2个概念一个是粘包问题。我暂时应该也用不到,就没看,如果哪天想看了就点击下方的原文链接吧~
原文:https://blog.csdn.net/sinat_36629696/article/details/80740678