文章目录
- TCP报文结构
- 确认应答
- 超时重传
- 三次握手与四次挥手
- 滑动窗口
- 流量控制
- 拥塞控制
- 延时应答
- 捎带应答
- 面向字节流 - 粘包问题
- 异常处理 - 心跳包
TCP报文结构
16位源端口号:表示数据从哪来的。
16位目的端口号:表示数据要到哪里去。
32位序号:由发送方填充,为发送的数据进行一个编号,后面会详细讲解。
32位确认号:由接受方填充,告诉发送方,确认号之前的数据我已经收到,我希望你从确认号开始发送数据,后面也会再次详细讲解。
4位TCP报头长度:表示该TCP头部有多少个32位bit;所以TCP头部最大长度是15*4 = 60。
6个标志位:
URG:紧急指针是否有效。
ACK:确认号是否有效,我们称携带ACK标识的为应答报文。
PSH:提示接受端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接,我们称携带RST标识的为复位报文段。
SYN:请求建立连接,我们称携带SYN标识的为同步报文段。
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段。
16位窗口大小:由接收端填充,用于控制发送方的发送请求的速度,在之后的TCP特性中会详细讲到。
16位校验和:由发送端填充,使用CRC校验,用来检验数据传输是否出错,接受端校验不通过,则数据传输有问题。
16位紧急指针:用于标识那部分数据是紧急数据。
简单对比一下UDP和TCP:
UDP:
无连接:使用UDP通信的双方可以不用刻意保存对端的相关信息。
不可靠传输:消息发送出去就不管了。
面向数据报:以一个UDP数据报为基本单位
全双工:一条路径,双向通信。
TCP:
有连接:使用TCP通信的双方,需要刻意保存对方的相关信息。
可靠传输:并不是说百分之一百发送成功,而是会尽力发送,在发送失败后也会有一个回应,告诉发送方是否发送成功。
面向字节流:以字节为基本单位,读写方式非常灵活。
全双工:一条路径,双向通信。
所以UDP适用于不要求安全性,追求效率的场景,而TCP适用于需要可靠传输,而又不太追求效率的场景。TCP具体是如何做到可靠传输的,下面我们来进行详细讲解。
确认应答
确认应答机制是TCP实现可靠传输的最核心机制,就是发送方发送一个请求,接收方会返回给接收方一个应答报文ACK。
这样一个场景,周末我想约舍友一起打球,我就给他发消息:
舍友回复我的这条消息,就被我们称为应答报文,此时ACK标志位就为1。而有时候我们一次会发送很多条消息,由于网络等一些原因可能会造成后发先至的状况,此时TCP就帮助我们解决了这个问题,发送方将发送数据进行编号,而每一个ACK都包含对应的确认序号。
例如上述情景,如果我们不进行标号,我们就无法区分舍友答应了我的哪一个请求。
TCP是针对每一个字节进行编号,从前往后把每个字节分别分配一个编号:
确认序号的规则:不是说与发送方的序号一一对应,而是取发送方发送过来的所有数据的最后一个字节的下一个字节的序号。如上图,确认序号1001的意义是:1001前的数据我都接收到了,我希望接下来发送方从1001开始发送数据。
同时序号的另一个用途是帮助TCP完成整队任务,因为不能保证数据到达的顺序与发送顺序一样,有可能会有后发先至的情况,所以TCP会在缓冲区中根据序号,针对收到的消息队列进行整队,保证应用程序读到的数据一定是和发送顺序一样有序的。
超时重传
如果在传送数据的过程中,一切顺利,就可以直接进行确认应答了,但是在数据传输过程中,需要经过很多设备中转,任何一个设备出现问题都会导致数据传输的出现问题,并且每个设备都有自己的转发承受能力,如果超过了这个峰值,也可能造成传输数据失败,经常打游戏的兄弟应该知道一个名词叫做丢包。
如果发生了丢包,那么接收方就收不到了,自然不会返回ACK了,此时等待一段时间后发送方没有收到ACK,就视为刚才的数据包丢了,此时会重新发送丢失的这一段数据,这就是超时重传。
如果重传后发送方还是没有收到ACK,此时超时等待的时间会变长,连续多次重传后还是没有ACK,TCP就会尝试重置连接,如果重置连接也失败,TCP就会关闭连接,放弃网络通信了。
如上图,此时我们发现,发送方收不到ACK的情况有两种,一种是数据直接丢了,接收方没收到,所以没有ACK,还有一种是接受方收到数据了,但是返回的ACK丢了。如果是第二种情况接收方就会收到重复的数据,此时TCP已经为我们解决这个问题了,会在接收缓冲区中根据收到的数据的序号,自动去重,保证了应用程序在读到的数据仍然只有一份。
三次握手与四次挥手
TCP三次握手与四次挥手是高频面试考点,首先纠正一下,很多人认为TCP的可靠性是靠TCP三次握手四次挥手实现的,其实不是TCP的可靠性是靠确认应答和超时重传实现的。
TCP建立连接:三次握手
TCP断开连接:四次挥手
三次握手就是在通信双方进行正式通信之前,进行一次网络交互,相当于客户端和服务器之间通过三次交互建立了连接(双方各自记录对方的信息)。如上图SYN就是同步报文段,意思是一方向另一方申请连接,此时就是客户端向服务器发送了一个申请连接的请求,服务器返回的ACK是一个应答报文,对刚才的请求进行一个回应,同时服务器发送一个自己的请求,客户端对服务器刚才的请求做一个应答。
举一个简单的例子,这就跟我们打电话是一样的:
我:你听的到嘛?
对方:我听的到!!!
对方:你能听的到嘛?
我:我也听的到!!!
是不是生动又形象。
这不是交互了四次嘛?为什么是三次握手呢?原因是ACK和SYN都是又内核进行操作的,所以ACK和SYN是在同一时间发送的,所以可以将这俩操作放在一起发送,提高效率,就有了我们的TCP三次握手。
TCP三次握手的主要功能就是投石问路,验证了客户端和服务器,各自的发送能力和接受能力是否正常,保证建立连接后数据传输的可靠性。
四次挥手跟三次握手非常相似,通信双方给自给对方发送一个结束报文FIN,再各自给对方返回一个ACK。
注意:建立连接的请求只能由客户端发起,而断开连接的请求,客户端和服务器都可以发起。
为什么此时不可以是三次挥手而是四次挥手呢
因为ACK和FIN发送时机是不一样的,ACK由内核完成发送,会在收到FIN的第一时间返回,而FIN是由程序代码控制的,在调用到socket的close方法时才会触发FIN。所以是四次挥手。当然也有可能变成三次挥手,需要看代码逻辑,还有延时应答机制也有可能造成FIN与ACK合并。
滑动窗口
前几个特性都是保证连接保证数据传输的可靠性的,滑动窗口这个特性是提高数据的传输效率的。
TCP的确认应答特性,导致了我们每发送一条数据就要等待ACK,要想提高传输效率,就要减少等待ACK的时间,我们就可以批量发送数据。假设批量发送4条数据,发完之后统一等待ACK,每收到一个ACK就立即发送下一条数据,使用一份时间等待多个ACK,总的等待时间就缩短了,整体的效率就提升了。
而为什么叫做滑动窗口呢?
原因是批量传输不是无限发送,发送到一定的程度,就等待ACK,返回一个ACK就发送下一条数据,说明总的可以批量发送的数据是一致的,我们把这一堆数据的数量称为窗口大小。假设我们的窗口大小为4:
当我们传输了4个数据之后,窗口已经满了,此时等待接收端返回ACK,接受到ACK后再次发送下一个数据,此时如果传输速度很快,窗口就会向后移动,看起来就像在滑动一样,所以我们称他为滑动窗口。
一些特殊情况说明,丢包了怎么办:
这样看起来接收方返回了4个ACK,只到达了一个,丢包率非常高,但是这种情况并没有造成影响,因为ACK的意义是告诉我们,4001前的数据都已经接受到了,下一个数据要从4001开始,所以前面的ACK丢了也就丢了。
如果在传输的过程中某一个数据丢了,接下来接收方就会反复索要丢失的数据段,发送方接收到多次相同ACK的时候就会发现事情不对,会把丢失的数据进行重传,重传后接受方返回的ACK的确认序号是7001,因为2001 - 7000这些数据接受方已经接收到了。
流量控制
在滑动窗口的特性中提到过一个概念叫做窗口大小,窗口大小是可以批量传输的数据的数量,窗口越大批量传输数据越多,速度越快,但是如果不进行控制,传输速度过快瞬间可以将接受缓冲区填满,之后发送的数据就会丢包,得不偿失,所以我们需要对窗口大小进行控制,这就引入了我们的流量控制。
在ACK报文中有一个窗口大小的字段,当ACK为1时,窗口大小字段就会生效,窗口大小的值就是建议发送方发送的窗口大小,当窗口大小为0时发送方就暂停发送了,每过一段时间发送方会发送一个窗口探测的包,如果发现窗口大小的字段不为0了,就会继续发送数据。
ACK返回的窗口大小如何计算呢?
简单粗暴拿接收缓冲区剩余大小作为窗口大小,此时我们将返回的窗口大小当作实际的窗口了,实际中会有出入,因为还有一个拥塞控制的机制,所以发送方窗口大小 = 流量控制 + 拥塞控制。
拥塞控制
上面我们说窗口大小 = 流量控制 + 拥塞控制
因为数据传输不只是从主机A到主机B,中间还有很多节点,任何一个节点上的设备出现瓶颈,都会对整体传输的速率产生明显的影响。
拥塞控制就是衡量中间节点的传输能力,通过实验的方式,找到一个合适的发送速率,开始的时候,按照一个小的速率发送,如果不丢包,就可以提高一下速率,扩大窗口大小,如果出现丢包,则立即把速率调小,重复上述过程。
具体的过程就是这样的。
延时应答
ACK为1时,窗口大小会生效,如果收到数据立即返回ACK,此时窗口大小为n,如果等一会在返回ACK那么窗口大小大概率比n要大,因为应用程序会从接受缓冲区中拿取数据。
延时应答的效果就是:通过这个延时返回ACK,让接受方应用程序多消耗一点数据,此时反馈的窗口大小就会更大一点,发送方发送效率也就高一点。
注意:并不是每一个包都延迟,需要根据情况判断是否延迟发送。
捎带应答
基于延迟应答,我们又有了一个特性就是捎带应答。
明明时四次挥手但是三次就可以挥完,就是捎带应答的作用。
面向字节流 - 粘包问题
因为TCP是面向字节流的,并且给每一个字节都编了号,所以在接受缓冲区中的数据都是紧紧挨在一起的,如果此时我们发送很多数据,我们就难以区分从哪到哪是一个完整的应用层数据报。
例如我们在使用微信发消息的时候,如果把所有话都放在一行上进行发送,接收消息的兄弟就很难分辨出从哪到哪是一句完整的话。
针对这样的问题,TCP也给出了解决的方案,可以定义分隔符,例如微信发送的每一条消息都以;结尾,这样即使消息都在一行上,接收方也可以清楚的知道从哪到哪是一句话。还可以约定长度,约定从哪到哪表示是数据报的长度。
异常处理 - 心跳包
在我们进行网络通信的时候,会出现很多异常情况:
1、进程关闭、崩溃
此时只是进程没了,但连接还在,仍然可以四次挥手。
2、主机正常关机
所有进程都关闭,此时会触发四次挥手,如果没挥完,例如对方发的fin我们不能ack了就关机了,此时对端会超时重传,如果多次重传还没有ack就直接释放连接了。
3、主机断电
瞬间所有机器关机,来不及进行任何挥手操作。
如果对端是发送方,对端就收不到ack,对端超时重传,对端重置连接,对端释放连接。
如果对端是接收方,此时不能判断我们是没有发送数据还是已经断开连接了,此时TCP为我们提供了一个心跳包的机制,对端虽然是接收方但是会周期性的向发送方发送一个心跳包,发送方接收到这个心跳包要给出回应,如果没有给出回应那么说明心跳没了,发送方可能挂了。
4、主机断网了
这种情况跟断电一样。