目录
TCP协议特点
TCP协议是传输层协议。特点:有连接,可靠传输,面向字节流,全双工。
有连接:通信双方保存对方的信息,删除信息则断开连接。
可靠传输:要传输的数尽可能的全部传输给对方。
面向字节流:每次传输的数据是以字节为单位的。
全双工:一条链路能够进行双向通信。
TCP报文格式
源/目的端口号:表示数据从那个进程来,到哪个进程去。
32位序号/32位确认号:
32位序号:按照字节对数据进行编号,连续递增。
32位确认序号:最后一个字节的序号+1,接下来发送从确认序号开始的数据。
4位TCP首部长度:表示TCP头部有多少个32位bit(有多少个4字节),首部长度为4字节。
保留(6位):tcp需要拓展新功能的时候就可以使用这个保留位进行表示。
6位标志位:
URG:紧急指针
ACK:确认号
PSH:提示接收端应用程序立刻读取TCP缓冲区。
RST:对方要求重新建立连接;携带RST标识的称为复位报文段。
SYN:请求建立连接;携带SYN标识的称为同步报文段。
FIN:通知关闭连接;携带FIN标识的为结束报文。
16位窗口大小:只会在ACK应答报文中生效,用来表示接收方接收缓冲区未使用区域(空闲区域)的的大小。
16位校验和:和UDP相同(上一篇文章)。
16位紧急指针:TCP带外数据传输,用来控制TCP自身工作机制特殊的数据包。
选项:可选的,意思是可以选择加还是不加,比如买电脑,配置就是可选择的,可以选择是否装独立显卡,可以选择加不加风扇。选项里包含了特殊属性,窗口拓展因子,可以拓展窗口大小。
TCP的10个比较核心的机制
1.确认应答
TCP协议的特点之一是可靠传输。所谓可靠传输,是确保尽可能的把数据传送到对方,让发送方知道接收方是否收到。
主机A给主机B发送数据,主机B返回应答数据,用于应答的数据,称为应答报文,此时ACK标志位就会置为1。如果仅仅使用单纯的应答机制,主机A在批量发送数据时就会出现问题。
在网络传输的过程中,经常会出现一个情况”后发先置“,后发的数据先到达。这种情况是无法避免的,在网络通信中是无法避免的,此时引入了32位标志位,对传输的数据(按照字节)进行编号,使用32位确认序号让应答报文的编号和发送数据的编号对应起来,即使出现后发先至,也不 影响传输的最终效果。
在TCP中,按照字节对数据进行编号,每个字节都会分配一个序号。
TCP报头中的序号数值就是载荷部分第一个字节的序号,由于序号连续递增的特点,只要知道第一个字节的序号,后续每个字节的序号就都知道了。确认序号就是要应答 数据的最后一个字节的序号再+1。这种序号的机制可以理解成TCP自定义的规则。
2.超时重传
超时重传用来应对网络出现丢包情况的策略。正常情况下,TCP是通过确认应答来知道数据是否被对端收到,假设通信双方(A,B),A给B发送了数据,B没有收到,B就不会对A给出任何应答,A就可以根据”是否收到ACK“来区分是否出现丢包。
A发送数据之后,到B返回ACK这之间会有一段时间,A就会进行一定时间的等待,等待的时间超过了某个阀值,还没有收到ACK,此时就可以认为出现了丢包。发现丢包,就要进行重传,把刚才的数据重新发一遍。
当然,返回来的应答报文也可能丢失。站在A的角度不知道是数据丢失还是应答报文丢失,如果是应答报文丢了,A没有收到就还会进行重发,B这边就会收到两份数据,这种情况是很不科学的,如果对于转帐这样的操作触发上述重传就会造成重复转帐。
站在TCP接收方这边,会针对收到的数据进行去重操作。TCP层次上,重复接收/重复传输,都是没有影响的,会保证应用层(应用程序,读取数据)不能读到重复的数据。无论TCP底层重传多少次,内核中始终会确保应用层读取到的数据就只有一份。所以,这其中的核心机制就是去重。
接收方再操作系统内核中存在一个数据结构,“接收缓冲区”类似于阻塞队列:
A发送数据到B,B收到数据之后,层层分用到TCP这一层之后,就会有类似阻塞队列的数据结构,把收到的数据放到类似阻塞队列的数据结构中,在放入的过程中,就会根据当前数据的序号,在队列中判定这个数据是否在队列中存在(或者曾经存在过),只要存在过,这个新的数据就不会入队,而是直接丢弃,只有在队列里的数据才会被读取走。B收到数据后判断重复,判定刚才应答报文丢包了,就会继续返回ACK。
超时重传的时间是动态变化的,时间会随着重传轮次的增加,会变的越来越长,重传若干次还是不成功,达到一定次数阈值,就会尝试“重置连接”,触发”复位报文“尝试重置连接(RST标志位),重置就是清空之前TCP传输过程中的中间状态(例如清空缓冲区),重新开始传输。当网络出现严重故障的时候,RST报文也无法顺利进行时,就只能断开连接了,即释放掉通信双方互相保存的信息。
TCP的可靠连接主要依靠确认应答和超时重传机制。
3.连接管理
建立连接的流程:3次握手
握手:发送一个不携带业务数据的数据报,不起任何业务作用,只是用来打个招呼。
建立连接,就是通信双方各自保存对端的信息,完成上述过程需要经过三次网络交互。
第一次握手:由客户端发起,谁先发起谁就是客户端,发送的SYN同步报文,这个数据报不携带任何业务数据,载荷部分为空,只有TCP报头,这个TCP报头中,6个标志位中的SYN为1。
第二次握手:服务器返回ACK+SYN报文,其实这是两次交互,只不过为了效率更高,把一个报文中的ACK和SYN同时设为1,也就是合并了两次交互。
第三次握手:客户端发送ACK应答报文给服务器,正式建立连接。
发送的数据报不带有任何载荷数据,只有TCP报头数据。三次握手,就相当于让对方保存自己的信息。双方 都得把对方信息保存好,连接才算建立完成。
3次握手的意义
1.投石问路
正式传输业务之前,确认一下通信链路是否通畅,相当与TCP可靠传输的一种保证方式,当然TCP可靠传输的核心还是确认应答和超时重传。三次握手是正式建立连接,然后才能进行数据传输。
2.确认双方发送能力和接收能力正常
三次握手本质就是确认双方发送和接收能力正常。如果变成两次握手,此时服务器这边对于客户端的“发送能力”和“接收能力”的认知时不完整的,需要第三次交互,,把客户端掌握的完整情报告知给服务器。
3.三次握手协商必要参数
比如TCP通信时使用的序号就是协商的结果,序号往往不是从1/0开始的,而是三次握手时,通信双方协商一个数字。第一次连接和第二次连接协商出来的起始序号是不同的。
在网络中,常常会出现通信双方建立连接进行若干通信之后,在断开连接,然后过一会又重新建立连接进行通信。第一次连接过程中某个数据报在传输过程中走了很远的一段路,此时到达新的连接中,就会被丢弃,此时就是通过序号来进行判断的,每次建立的连接都是从新的数字开始,作为起始的序号。如果突然收到一个数据报,起始序号和当前序号差别非常大,就可以被认定为之前连接的数据报了。
三次握手重要状态
LISTEN:服务器进入的状态,端口绑定成功,服务器准备就绪,随时可以有客户端连上来。
ESTABLISHED:连接建立完成,随时进行数据通信了。
断开连接的流程:4次挥手
四次挥手,可以由客户端或者服务器端发起,客户端代码中调用socket.close()方法,或者进程结束。
四次挥手过程中,中间的两次不能合并,而三次挥手可以。三次挥手过程中,服务器收到SYN会立即返回ACK和SYN报文,同时进行可以合并。而挥手过程中,服务器收到客户端发来的FIN会立即返回ACK报文(内核控制),然后再应用程序调用close时才能触发返回FIN报文(应用程序控制),这两个操作不是同一时机,难以合并。
四次挥手重要状态
CLOSE_WAIT:等待应用程序调用close方法,如果没有及时调用或者忘记调用,此时就可能使机器上出现大量的CLOSE_WAIT。
TIME_WAIT:应对最后一个ACK丢包这样的场景。客户端在收到服务器返回的FIN之后不能立即释放TCP连接,如果立即释放,后续服务端一旦重传了FIN客户端就不能做出回应,因此需要这个状态等待可能到达的FIN重传数据。TIME_WAIT状态不是持续的,而是有一定时间的,在一定时间之内。如果没有收到重传的FIN,也就是最后一个ACK报文被收到了,就不会重传FIN,此时对应的TIME_WAIT就可以释放了。
CLOSE_TIME不一定是服务器器处于的状态(被动接受的一方),TIME_WAIT也不一定是客户端处于的状态(主动发起的一方)。
4.滑动窗口
滑动窗口是一种提高效率的机制。未引入滑动窗口时,数据是一条一条的发送,引入滑动窗口后,把一条一条的发送变为了批量发送。

将多条数据批量发送:
滑动窗口的大小就是批量发送多少数据,在这写数据发送时不会等待ACK的响应,这个过程就是窗口大小。滑动窗口中,批量发送了4组数据之后并不是等到4个ACK报文全部回来后才能继续发送新的数据,而是收到一个ACK报文后就往后发送新的数据。
在发送过程中,要批量发送4组数据:
第一组如果是1-1000
第二组一定是1001开头(1001-2000)
第三组一定是2001开头(2001-3000)
滑动窗口出现丢包
情况1:数据包到达,ACK应答报文丢了。
此时不需要任何处理,后续收到的ACK报文就涵盖了之前ACK的效果,表示之前的数据都已经收到了,假如收到序号为5000到达报文,那么代表5000之前的报文全部收到。
情况2:数据包丢失
当A发送给B序号为1001-2000的报文,此时B没有收到,虽然主机A任然在向后给B发送数据,但B都是在向A索要1001这个数据包,A这里收到若干个这样的ACK报文,此时重传缺失的数据即可,其他数据不必重传。
1.对ACK不做处理2.仅重传缺失的数据这两种方式对于丢包的处理是很高效的,这种处理又叫做快速重传。
快速重传和超时重传是否冲突?
不冲突,在不同情况下采取的不同重传策略,快速重传相当于超时重传在滑动窗口下的变种。如果TCP传输的数据比较少,不频繁,此时就不会触发滑动 窗口,短时间传输大量数据,此时才会触发滑动窗口,同样在这种情况下任然按照超时重传的方式去解决问题,才会触发快速重传,按照ACK反馈次数来解决问题。
重要结论:
滑动窗口是提升效率的机制,更准确的来说是亡羊补牢的机制,TCP为了保证可靠传输牺牲了很多效率的,引入滑动窗口是让效率上的牺牲变少一些。依靠滑动窗口的机制不会比像UDP这种没有可靠机制的协议更快。
5.流量控制
在滑动窗口中涉及了一个非常关键的概念:窗口大小,实际上是批量发送数据的数据量,就属于不需要等待ACK的数据量。窗口的大小是可变 的,可以通过窗口的大小来控制发送方的发送速度。
窗口越大,单位时间内发的数据就越多,效率越高。
窗口越小,单位时间发的数据就越小,效率越低。
通常情况下,肯定希望尽可能高效的传输,但 高效的前提是保证可靠性。如果发送方发送速度太快,接收方处理不过来,就是个很麻烦的事情。合理的做法是接收方告诉发送方自己接收不过来了,让发送方控制发送速度,这样的机制就叫做流量控制,接收方根据自身能力反向制约发送方,是双方达成一个平衡。
如何进行流量控制?
接收方有一个接收缓冲区(阻塞队列),会以这个缓冲区没有被使用的空间作为发送方发送数据窗口大小:
此时接收方在给发送方返回ACK的报文中,TCP报头中的16位窗口大小里表示这个空间大小。发送方就可以按照上述窗口大小决定下一轮数据 发送的窗口大小了。当接收方的缓冲区满了,就会告知发送方暂停发送。
当发送方暂停发送后,接收方不会返回ACK,接收方缓冲区改变之后如何告知发送方?
此时发送方会周期性发送“窗口探测报文”,也是不携带业务数据的tcp数据包,主要目的是为了触发ACK报文,从而感知接收方的情况。
6.拥塞控制
和流量控制类似,都是和滑动窗口搭配的机制。
流量控制是站在接收方的角度影响发送方的速度,流量控制时很容易定量来衡量,接受区剩余空间大小,用这个作为发送窗口大小,一旦考虑中间节点,中间有多少设备,每次发送路径都可能不一样。无论中间结构有多复杂,TCP都把他们视为一个整体,然后通过实验的方式,找到一个合适的窗口大小(发送速度)。
刚开始,就按照小的速度,小的窗口来发送数据,如果没有出现丢包(说明中间链路非常通畅),就可以增加速度,继续增加窗口大小,增加到一定程度,发送速度非常块了,此时可能中间的某个设备达到瓶颈,出现丢包了,此时发送方立即减小窗口大小,继续发送,看是否还丢包。如果不丢包,尝试继续加,如果丢包就继续尝试减。
动态平衡:这样就能找到一个合适窗口大小的值,就可以不丢包,并且比较快的速度完成传输。由于网络是复杂,也是多变的,按照上述方式,动态调整,随时适应网络中的变化。
上述过程便是拥塞控制,应对网络中间链路节点出现丢包。
拥塞控制和流量控制都会通过控制窗口来影响发送速度,这两个窗口哪个小实际传输过程中就会优先。
拥塞窗口大小变化规律
1.刚开始,使用比较小的窗口来传输数据。
2.按照指数方式扩大窗口。
3.指数增长过程中达到阈值变成线性增长。
4。线性增长,发送速度越来越快,增长到一定程度,就会出现丢包,此时发送方就会立即把窗口变小(减小发送速度)。
5.缩小到出现丢包时候的窗口大小的一半,接下来线性增长。
7.延时应答
ACK报文不会立即返回,而是稍等一会返回。核心目的是为了提升传输速率。
决定传输速率的关键因素是窗口的大小,在能够承受 的前提下尽可能的提高窗口大小。通过延时就可以使窗口大小得到提升,这里的延时,就是给应用程序腾出来更多的消费时间。
假设A给B发送1Kb大小的消息,B接收消息缓存区大小总量使5Kb:
如果此时理解返回ACK报文,那么ACK报文中关于窗口的大小就是4Kb,如果延迟一会再返回ACK,在延时的时间里,应用程序就可能把刚才的数据消耗了一部分(假设读取了512个字节的数据),还剩下512字节了,此时返回的窗口大小是4.5Kb。
此处延时时间有两种方式:1.按照一定时间来指定 2.按照收到的数据量。这两个策略是结合使用,保证窗口大小的合理性。
8.捎带应答
捎带应答是建立在延时应答的基础上,提升效率的机制。
日常开发中,客户端和服务器的通信一般是”一问一答“形式。
正常来说,ACK报文时内核收到请求,自动返回的,响应数据时应用程序执行一系列逻辑之后返回的,由于延时应答,ACK报文不会立即返回,在ACK报文稍等的这一会,正好要返回的响应数据,此时就会在响应数据中TCP中的ACK这一位设置上,把确认序号/窗口大小都设置上。这样的效果,提高了效率,把两次传输合并成一次传输。
9.面向字节流
在字节流读写数据的场景中,会涉及到一个非常关键的问题:粘包问题。
粘包问题粘的是应用层的数据包,主要是需要区分从哪里到哪里是一个完整的应用层数据包,明确包之间的界限。
假设一个翻译服务器:
当服务器调用read方法读取请求时,由于字节流的特点,读的时候咋读都行,一次可以读一个字节,也可以一次读若干个字节,此时服务器无法区分从哪里到哪里是一个完整的单词。
处理方法
在应用层中,各自的协议都有自己的区分方法:
json,xml,yml都是基于分隔符的方式进行区分,protobuffer是按照长度的方式进行区分的。 http即会用到长度,也会用到分隔符。
10.异常情况
1.某个进程崩溃了
进程崩溃,正常结束,操作系统都能够回释放对应的PCB,可以释放里面的文件描述表,相当于调用close,此时会正常和对方进行四次挥手操作。虽然进程不在,但操作系统任然管理着TCP的连接,可以顺利完成挥手操作。
2.某个主机关机(正常流程关机)
正常流程的关键,操作系统会先尝试强制 关闭所有用户进程,然后进入关机流程。这个过程就会和上面进程崩溃一样,结束进程之后进行四次挥手。但是4次挥手可能没有走完主机就关闭了。
A和B建立TCP连接之后,A关机了,A关机之前会告诉B,发送FIN结束报文,B收到了FIN,B返回ACK应答报文,准备发送FIN,此时A已经关机完毕,意味着B接下来的FIN会重传几次,此时B没有收到ACK,就会把A的信息删除。
3.主机断电/网线断开
A和B通信,A突然断电了,A无法做出任何反应就被强制关闭了,B任然会以为A存在。
a.B是数据发送方
B接下来发送的数据没有收到ACK,B就会触发超时重传,重传几次就会发送复位报文(RST),RST也没有响应,B就会单方面删除保存的A的信息。
b.B是接收方
当A不发送数据了,B在一定时间内没有收到A的数据之后,就会触发心跳包,连续发送若干次没有回应,就会单方面释放连接。TCP虽然内置了心跳包,但是这个心跳包周期比较长,需要分钟级别的时间发现对端断开连接。在实际开发过程中,经常会实现应用层的心跳包,用更高频率,更短周期的(秒级/毫秒级)。
心跳包:没有载荷的数据,只是为了触发ACK应答报文。