TCP学习
本文施工状态
未完工,一些具有说明意味的图片未加入,流量控制和拥塞控制没有提及,与UDP的对比也没有。
本文是什么
在之前的合集文章中,我们了解了UDP并简单说明了一下什么是连接。UDP格式简单,发送简便,但是并不是可靠的传输。而在本文中将会对TCP进行介绍。其中包括TCP的报文格式,TCP的连接的建立和关闭,TCP的丢包重新传输,拥塞控制和流量控制等内容,并使用Wireshark进行实际的介绍。
正文
TCP
TCP是传输控制协议(Transmission Control Protocol)的缩写。从RFC中为TCP的定义我们知道,TCP为应用提供可靠,有序,基于字节流的服务。其可靠性在于TCP能够感知到一些数据包在网络中丢失,并安排重新传送,知道这个数据确实传输到对端。其有序性在于TCP提供了数据的序列号,通过序列号就可以将收到的数据包进行排序。其基于字节流也和UDP也有着区别,UDP面向数据报,发出的一个数据就是一段完整的报文;而TCP则可以将一个报文看作字节流,可以分成多段,一段一段地发送过去。
TCP报文格式
在具体说明TCP其性质的实现之前,我们需要提前了解一下TCP报文的格式与内容。
![](https://img2024.cnblogs.com/blog/3272170/202403/3272170-20240301213459530-623440570.png)
- 源端口:16位长度,与UDP一样,为发送方的端口号。
- 目的端口:16位长度,与UDP一样,为接收方的端口号。
- 序列号:32位长度,可以简单理解为本次数据载荷首字节在整体数据的位置,但其实最开始是一个随机值。比如初始随机值为100,本次要发送的数据的首字节在整体数据的位置是1000,则序列号是1100=100+1000。
- 确认号:32位长度,表示希望收到下一次数据的序列号,与对端的序列号有关。比如我们收到了一个数据,序列号为2000,长度为1000。则我们希望下一次收到数据的序列号为3000,同时也表示序列号在这之前的数据已经被我们收到。
- 数据偏移位置:4位长度,单位为4字节。因为TCP的报文长度是一个可变的值,数据偏移位置指定了数据载荷的开始的位置,其实也就是TCP报文头的大小。假设这个值为0x5,也就说明报文头的长度为20=5*4字节。
- 保留位:4位长度。
- 控制位 8位长度,目前共8个控制位,分别是:
- CWR:1位长度,置1表示减少拥塞窗口大小。
- ECE:1位长度,置1表示ECN-Echo。
- URG:1位长度,置1表示有紧急信息且紧急指针是有效的。
- ACK:1位长度,置1表示确认号是有效的。
- PSH:1位长度,置1表示将缓冲区的所有数据都推给应用。
- RST:1位长度,置1表示连接出现了问题,需要进行重置。
- SYN:1位长度,置1表示进行连接。
- FIN:1位长度,置1表示连接结束。
- 窗口大小:16位长度,滑动窗口的大小。
- 校验和:16位长度,与UDP一样,检验数据是否合法。
- 紧急指针:16位长度,如果有紧急消息,指示紧急消息在数据载荷的位置。
- 可选选项:TCP的一些选项,后面使用填充位补齐为4字节的倍数。
TCP连接的建立,三次握手
TCP的三次握手是一个经常被提起的事物,但很多朋友对此的理解可能比较生硬,让我们举一个生活中的例子。在我们打电话的时候,我们第一句会说 “你好,你听得见吗?”。如果对方回答了“你好,我听得见”,这就说明我们的声音能传给对方。否则意味着对方什么都没听到,我们的声音没法传过去。我们可能需要再次询问或者重新拨打对方的电话。在确认对方能听见我们后,我们也会告诉对方”我也听得见你“,对方在听到这句话后也知道他的声音能够传到我们这里。这样,双方就可以正常开始沟通了。
如果你能理解上面的例子,那就让我们开始正式介绍TCP连接的建立过程吧。
![](https://img2024.cnblogs.com/blog/3272170/202403/3272170-20240302153613153-1749876537.gif)
- 一开始A是连接关闭的状态,B是正在监听的状态。
- A发送一个<SEQ=100><CTL=SYN>的报文。A的状态变为SYN-SENT,表示A此时已将连接请求发送。
- B收到了A发来的连接请求。返回<SEQ=300><ACK=101=100+1><CTL=SYN, ACK>的报文。B的状态状态变为SYN-RECEIVED,表示B已经收到了连接请求,并且同样发起连接建立的请求。
- A收到了B发来的报文。返回<SEQ=101><ACK=301=300+1><CTL=ACK>的报文。A的状态变为ESTABLISHED,说明A->B的传输连接已经建立。
- B收到回复。B的状态变为ESTABLISHED,表示B->A的连接已经建立。
- 双端的连接都完成了,现在A开始发送数据,可以看到发送的报文中已经带上了数据载荷。
- 双方连接建立完成,A发送的数据报文带上了真正数据载荷。
下图为通过HTTP(HTTP使用TCP)来展示了真实的TCP的连接建立过程,各位朋友可以根据上面所展示的过程与实际抓包结果进行联系。
同时,我们还注意到了一点,握手中发送的确认报文中ACK都是对方序列号+1。一般来说,收到了普通报文中如果没有数据载荷,则ACK是不会变的。而在收到SYN报文和FIN报文的时候,都会将ACK+1。
TCP连接的结束,四次挥手
再说完TCP连接的建立后,我们就要就要开始谈谈TCP连接的结束了。刚建立就要结束,或许是太快了一点。但这也是因为TCP连接的结束与建立有着一些相同之处和不同之处。我们通过对比的方法能够更好地理解这两个过程。关于连接中的那些事,我们后面再慢慢来说。
- 一开始,双方都是处于连接的状态中。
- 这里假设A已经发完数据了,于是A发出了一个<SEQ=100><ACK=300><CTL=FIN, ACK>的报文。A的状态也变为FIN-WAIT-1,表示自己已经没有多余的数据了,请求关闭连接。
- B收到A的报文,返回<SEQ=300><ACK=101=100+1><CTL=ACK>的报文。B进入CLOSE-WAIT状态,而A在收到确认后状态变为FIN-WAIT。这时候连接是一种半开状态,A无法向B发送数据,因为自己主动结束了发送,而B还能继续向A发送数据。
- 过了一段时间,B的数据也发完了,于是它也发送<SEQ=300><ACK=101><CTL=FIN, ACK>报文。自身进入LAST-ACK状态,这是它最后一次发生ACK了(其实也不一定,如果第一次ACK丢失了,它还得重新发送)。
- A收到报文,返回<SEQ=101><ACK=301><CTL=ACK>报文,表示自己已经收到了关闭请求。B收到这个报文后就会进入CLOSED状态,表示已经关闭连接。A则会先进入TIME-WAIT状态,因为如果A发送的报文丢失了,他收到B的重传报文后还要再发一遍,可不能立即关闭了。在等待两倍最大传输时间之后,就认为对面应该是收到了确认报文了,于是就将自己关闭也进入CLOSED状态。
重传
我们说TCP是一个具有可靠性的传输协议,不是说TCP使用了什么方法将整个网络质量提高了而不会丢包(它也做不到)。而是TCP建立了重传机制,丢包是可能发生的,但是TCP在发现丢包后会将这个包重新发过去,直到对方确实收到(TCP:包可能会迟到,但不会缺席)。
于是就出现了一个问题,TCP是如何知道有丢包的呢?其实方法很简单,我们只要在包发出去的时候记录发出的时间并设置一个计时器,如果计时器超过了一个阈值并且没收到确认报文,就认为这个包丢了,需要进行重传。这个方法又叫做超时重传。
解决了如何发现丢包,现在又有了一个问题。这个超时阈值又该怎么设置呢?如果超时阈值设置太短,那么很容易发生重传,整个链路上都是重复的包,效率实在是太低了!那么我们把时间调长一些呢?如果超时阈值太长,那么确认到丢包要进过很久,这个时候链路上又没有包了,效率实在是太低了!进过考量,我们需要这个超时阈值比实际的报文往返时间稍微大一点。TCP会统计每次包的往返时间,并进行一个取均值的方案(样本估计中的均值估计)。同时为了防止测量的报文往返时间波动较大,采用了滑动平均的方式进行平滑处理。
在超时重传中,当发现重传的包再次丢包后,意味着网络此时比较堵塞,于是会进行一个将超时时间加倍的方案来防止将本就不堪重负的链路再次雪上加霜。在Linux中,最长的超时时间可能会到达2分钟,这或许有点太长了。于是TCP又加入了快速重传机制,当收到多个(实际上3个)相同的确认报文后就进行重传。我们举一个例子。
- A发送了4个包<SEQ=100>、<SEQ=200>、<SEQ=300>、<SEQ=400>。其中<SEQ=100>的包丢失了,但剩下三个包都顺利到达。
- B收到三个包,对于<SEQ=200>,B说我收到了这个数据并进行保存,但是我没收到前面的,下次还是希望你发送<SEQ=100>的包。其他两个包也是同样的道理。于是B发送3个<ACK=100>。
- A收到这3个相同的确认报文,很明显知道<SEQ=100>的包丢失了。这时候就需要开始进行重传。
流量控制
TCP连接的双方都会设置各自的发送缓冲区和接受缓冲区来缓冲收过来的数据和要发出的数据。现在我们进行一个简单的假设,A为发送方,B为接收方,其中A读取一个MP4文件并发给B,B收到发来的文件开始进行解码。在这个场景中,A读取文件的速度相较于B解码视频的速度肯定是要快很多的。如果A不进行流量控制,而是拿到包就发,越快越好。那么会出现什么情况呢?答案是大量的包发到了B那里,B尽最大力解码都比不过A发的速度快。于是B看着缓冲区越来越满,直到数据溢出,大量的包只能无奈被丢弃。由于很多包被丢弃了,那么A只能将被丢弃的包再次进行重传。这样看来,让A无所忌惮地发包只能是害人害己啊。
从生活中的例子来看,我们去取快递。我们一般拿两三个快递就已经差不多了,但是快递师傅想让你一口气拿完他好离开,于是将五六个快递直接送到你的手上。这时你的心里肯定想着这个师傅真的是太急了,一点都不考虑自己拿不动的事情。
所以,TCP为了让发送的速度和接收的速度进行匹配。B在向A发送确认报文的时候都会在报文段中报告目前的接收窗口有多少。A在收到B的确认报文后读取接收窗口大小,让自己发出还未确认的包的大小小于对方的接收能力。在这种动态的流量控制反馈机制中,A和B又能愉快的交流了。
拥塞控制
拥塞控制的目的很简单,防止发送方发送过多的数据造成整个链路的拥堵。前面提到的流量控制限制了发送方发数据的速度,而这里提到的拥塞控制也是要限制发送方发数据的速度。二者又有什么区别呢?很简单,流量控制考虑的对象是对端的接收能力,而拥塞控制考虑的则是传输链路的流畅情况。换而言之,一个考虑的是目的地,一个考虑的是路仅本身。而发送方的发送窗口也是取二者中的最小值。
当发送丢包事件的时候,我们就可以确认是链路发生了拥塞。TCP采用了慢启动,拥塞避免和快速恢复等机制来进行拥塞控制。
- 慢启动:一开始将拥塞窗口设置为1开始发送,如果没有丢包就将拥塞窗口翻倍(即以2的指数进行增长),也就是1,2,4,8,16。这样看来,这个慢启动的名字还真是不贴切,这个增长速度也太快了,只不过起点低,应该叫做低启动才对。当然,这个慢启动可不能一直持续下去,否则拥塞窗口没多少次就会变的很大,这是往这丢包而去啊。
- 拥塞避免:一般来说,TCP会设置一个值ssthresh(一般为16)。当慢启动到达这个阈值之后也不敢太放肆了,开始进行线性增长,每次发送成功就将拥塞窗口加一,在丢包的边缘进行小心试探。
- 出现拥塞:拥塞是不可避免的,因为上面的两种方案都在将拥塞窗口增大。出现超时或连续三个相同的ACK后,TCP将ssthresh设置为当前拥塞窗口的一半,然后重新开始慢启动流程。
- 快速恢复:后续也发现一旦进行重传就重新慢启动有一些不太合理。因为对比超时,连续收到三个相同的ACK说明虽然网络拥塞了,但也不是很严重。于是快速恢复就提出来了。当出现的是连续三个相同ACK时,不再是进行慢启动,而是将拥塞窗口的大小变成新ssthresh+3,然后进行拥塞避免流程,加快窗口大小的恢复。