Apong's Blog

当你快坚持不住的时候,困难也快坚持不住了

0%

TCP详解

OSI七层模型

应用层

表示层

会话层

传输层

网络层

数据链路层

物理层

tcp,udp位于传输层

tcp/ip 标识:源IP、端口号,目的IP、端口号,协议号

端口范围

0-1023 知名端口号

1024-65535 动态分配的端口号

常见知名端口号

ssh: 22

ftp: 21

http: 80

telnet: 23

https: 443

在 linux 中使用 cat /etc/services 查看已记录端口号

UDP协议

结构图

在这里插入图片描述

特点:

  • 无连接
  • 不可靠:没有确认和重传机制
  • 面向数据报:只能一次性发送全部报文

缓冲区:

存在接收缓冲区,没有发送缓存区

因为 UDP 不保证可靠性,即使报文丢失,也不需要重发。

而 TCP 必须要有,当报文丢失,需要重新发送。

虽然具有接收缓冲区,但是无法保证接收的顺序,当缓冲区满了的时候再到达的报文会被丢弃。

UDP 是全双工协议,最多传送 64 k【计算方式??】,如果超过需要手动分包。

基于 UDP 的应用层协议:

NFS:网络文件协议

TFTP:简单文件传输协议

DHCP:动态主机配置协议

BOOTP:无盘设备启动协议

DNS:域名解析协议

TCP协议

在这里插入图片描述

源端口号/目的端口号:表示数据从哪个进程来,要到那个进程去

32位序号:TCP将要传输的每段数据组进行了编号,序号是本报文段发送的数据组的第一个字节的编号,可以保证传输信息的有效性。

比如:一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为401。

32位确认序号:每一个ACK一个确认号:表示下一个期待收到的字节序号,且该序号之前的所有数据已经正确无误的收到。

确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。

4位首部长度(数据偏移): 表示该TCP头部有多少个32位bit(有多少个4字节),可以将TCP报头和有效载荷分离。

TCP报文首部固定为20个字节即偏移量 5,最大能扩充到 15*4 = 60个字节。

6位标志位:

  • URG:标志紧急指针是否有效。
  • ACK:标识确认号是否有效。
  • PSH:提示接收端应用程序立即将接收缓冲区的数据拿走。
  • RST:它是为了处理异常连接的, 告诉连接不一致的一方,我们的连接还没有建立好, 要求对方重新建立连接。我们把携带RST标识的称为复位报文段
  • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
  • FIN:通知对方, 本端要关闭连接了, 我们称携带FIN标识的为结束报文段

16位的紧急指针:指定优先被处理的报文段,这时就可以设置紧急指针,指向该报文段【序号??】即可,然后将紧急标志设置为1。
16位窗口大小:用于控制发送方发送的流量。接收方将自己接收缓冲器剩余空间的大小告诉发送方叫做16位窗口大小。

发送发可以根据窗口大小来适配发送的速度和大小,窗口大小最大是2的16次方,表示最多能表示发送64KB,但也可以根据选项中的某些位置扩展,最大扩展1G。
16位校验和:发送端填充,CRC校验。如果接收端校验不通过, 则认为数据有问题

(此处的检验和不光包含TCP首部也包含TCP数据部分???)。

确认应答机制

发送端向接收端发送消息后,如果接收端成功收到了信息,会返回 ACK 和下一个报文段的确认号。

如,主机A给主机B发送了1-1000的数据,ACK应答,携带了1001序列号。告诉主机A,我已经接受到了1-1000数据,下一次你从1001开始发送数据。

超时重传

当接收端没有接受到消息,或者发出的 ACK 消息丢失了,发送端就会在特定的时间间隔内重发信息。

因此接收端会产生重复信息,需要根据【16位序列号????】去重。

如何确定超时的时间?

太短会导致频繁发送重复的包,太长会影响重传效率,因此 TCP 为了保证高性能,会动态计算这个时间。

如 linux,以 500 ms 为单位进行控制,以整数倍递增,倍数以指数型递增。

当首次重发后,仍然超时就会等待 2*500ms、4*500ms…

连接管理机制

三次握手:确保两端都有发送和接收的能力,两次握手只能确认一端能够发送,另一端能够回应,但无法确认另一端发送后,这一端也能正常回应。

image-20240801223020726

四次挥手

img

收到 FIN 的时候无法立即关闭,需要等待 2 TTL(发收都完成),因此需要先返回确认 ACK,表示收到。

然后处理完成后,再发送 FIN 给客户端(第三次挥手),最后客户端回应,连接正确断开。

TIME_WAIT 状态

当我们实现一个TCP服务器时,我们把这个服务器运行起来。

然后将服务器关闭掉,再次重新启动服务器会发现一个问题:

就是不能马上再次绑定这个端口号和ip,需要等一会才可以重新绑定,其实等的这一会就是TIME_WAIT 状态。

TCP协议规定主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL的时间后才能回到CLOSED状态。
当我们使用Ctrl-C终止了server,server是主动关闭连接的一方在TIME_WAIT期间仍然不能再次监听同样的server端口。

MSL

MSL在RFC1122中规定为两分钟(120s),但是各操作系统的实现不同。

在Centos7上默认配置的值是60s可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout查看MSL的值。

为什么TIME_WAIT时间一定是2MSL:

保证两端都发送和接收都已完成。

首先,TIME_WAIT是为了防止最后一个ACK丢失,如果没有TIME_WAIT,那么主动断开连接的一方就已经关闭连接,但是另一方还没有断开连接,它收不到确认ACK会认为自己上次发送的FIN报文丢失会重发该报文,但是另一方已经断开连接了,这就会造成连接不一致的问题,所以TIME_WAIT是必须的。

MSL是TCP报文在发送缓冲区的最大生存时间,如果TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失。

(否则如果服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)。

同时也是在理论上保证最后一个报文可靠到达。

(假设最后一个ACK丢失, 那么服务器会再重发一个FIN,这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK,这就会导致问题) ????

解决TIME_WAIT状态引起的bind失败的方法:

在server的TCP连接没有完全断开之前不允许重新绑定,也就是TIME_WAIT时间没有过,但是这样不允许立即绑定在某些情况下是不合理的:

比如:服务器需要处理大量的短暂的客户端的连接
(每个连接的生存时间可能很短,但是每秒都有很大数量的客户 端来请求)
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),

这样服务器端就会有大量的端口产生大量TIME_WAIT状态,无法立刻重用,需要等待。
每个连接都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议)。其中服务器的ip和端口和协议是固定的,如果新来的客户端连接的ip和端口号和TIME_WAIT占用的连接重复就造成等待。
解决方法:使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。

CLOSE_WAIT 状态

在两次挥手过后,如果服务器还没有第三次挥手,此时就是 CLOSE_WAIT 状态。

滑动窗口

确认应答策略要求每一个发送的数据段都要给一个ACK,收到后再发送下一个数据段。

相当于 2L 的时间发送一段数据,性能较差,尤其是数据往返的时间较长的时候。

滑动窗口可以同时发送多条消息,将多个段的等待时间重叠在一起。

想当于 1L 的时间发送多条数据,大大的提高了吞吐率。

在这里插入图片描述

窗口大小:指的是无需等待确认应答而可以继续发送数据的最大值。

上图的窗口大小就是4000个字节(四个数据段段)。

和 TCP 协议头的窗口大小是一个东西吗??控制发送流量

发送前四个段的时候,不需要等待任何ACK直接发送即可。

当收到第一个ACK后滑动窗口向后移动,继续发送第五个段的数据,然后依次类推。

操作系统内核通过发送缓冲区来记录当前还有哪些数据没有应答。

只有确认应答过的数据,才能从缓冲区删掉,窗口越大,则网络的吞吐率就越高。

滑动窗口左边代表已经发送过并且确认,可以从发送缓冲区中删除了,

滑动窗口里边代表发送出去但是没有确认。

滑动窗口右边代表还没有发送的数据。

在这里插入图片描述

如果在这种情况中出现了丢包现象,应该如何重发呢?

ACK丢失:数据到达接收方,但是 ACK 报文丢失:可以根据后边的ACK确认。

因为接收方也是一段一段同步接收的,所以如果丢失了前面的 ACK,后面的可以帮忙确定。

假设发送方发送1-1000的数据,接收方收到返回确认ACK,但是返回的ACK丢失了,

另一边发送1001-2000收到的确认ACK 2001,就可以认为1-1000数据接收成功 。

数据段丢失: 当某一段报文段丢失之后,发送端会持续收到 1001 这样的ACK,就像是在提醒发送端 “我想要的是 1001” 一样。

如果发送端主机连续三次收到了同样一个”1001” 这样的应答,就会将对应的数据 1001 - 2000 重新发送;

并且这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了。

因为2001 - 7000接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。

这种机制被称为 “高速重发控制”(也叫 “快重传“)。

在这里插入图片描述

快重传要求接收方在收到一个失序的报文段后就立即发出重复确认

(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。

快重传算法规定:发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量。

流量控制

接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被装满

这个时候如果发送端继续发送,就会造成丢包,然后引起丢包重传等等一系列连锁反应。

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。-> 窗口字段

接收端将自己可以接收的缓冲区大小放入TCP首部中的”窗口大小”字段,通过ACK确认报文通知发送端
窗口大小字段越大,说明网络的吞吐量越高

接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后,就会减慢自己的发送速度;

如果接收端缓冲区满了, 就会将窗口置为0。

这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
除了窗口大小,TCP首部的 40 字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移M位。

接收端窗口如果更新,会向发送端发送一个更新通知,如果这个更新通知在中途丢失了,会导致无法继续通信,所以发送端要定时发送窗口探测包。

这个窗口探测数据段的数据是无意义的,目的是得到返回的窗口大小。

拥塞控制

虽然 TCP 有了滑动窗口能够同时发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。

因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据是很有可能引起雪上加霜的,造成网络更加堵塞。

TCP引入慢启动机制,先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

在这里插入图片描述

图中的 cwnd 表示拥塞窗口大小,开始时大小为1,根据应答情况进行递增或重置。

每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口。

像上面这样的拥塞窗口增长速度,是指数级别的。

“慢启动”只是指初使时慢,但是增长速度非常快。

为了控制增长的速率,引入一个叫做慢启动的阈值

当拥塞窗口超过这个阈值的时候,不再按照指数方式增长, 而是按照线性方式增长。

在这里插入图片描述

当TCP开始启动的时候,慢启动阈值等于窗口最大值??

哪个阶段的窗口最大值,第二次握手获得的窗口大小??

在每次超时重发后,慢启动阈值会变成原来的一半同时拥塞窗口置回 1
区别于超时重传:是少量的丢包。

大量的丢包,我们就认为网络拥塞。当TCP通信开始后,网络吞吐量会逐渐上升。

随着网络发生拥堵,吞吐量会立刻下降。

拥塞控制归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

在这里插入图片描述

拥塞控制与流量控制的区别:

拥塞控制是防止过多的数据突然注入到网络中,考虑到了网络中的路由器或链路过载,是一个全局性的过程。 流量控制是基于缓冲区的,某个时间点的通信量控制,主要就是控制发送端发送数据的速率,以便接收端来得及接收。

拥塞控制的标志:

  • 重传计时器超时
  • 接收到三个重复确认

拥塞避免并非完全能够避免拥塞,只是尽量使网络不容易出现拥塞。

拥塞避免的思路是让拥塞窗口 cwnd 缓慢地增大。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞

(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理)

这时就把慢开始门限设置为出现拥塞时的门限的一半。然后把拥塞窗口设置为1,执行慢开始算法。

在这里插入图片描述

线性-加法增大:执行拥塞避免算法后,拥塞窗口线性缓慢增大,防止网络过早出现拥塞
超时-乘法减小:无论是慢开始阶段还是拥塞避免,只要出现了网络拥塞(超时),那就把慢开始门限值ssthresh减半

快恢复(与快重传配合使用)

采用快恢复算法时,慢启动只在TCP开始连接建立时和网络出现超时时才使用。

当发送方连续收到三个重复确认时,就执行“乘法减小”算法,但是只把 ssthresh 门限减半,并把 cwnd 设置为 ssthresh 的大小。

因为考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。

延迟应答

如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。

可能会根据本次接收的数据大小来控制窗口。

假设接收端缓冲区为1M 一次收到了 500 K 的数据。如果立刻应答,返回的窗口就是500K。

但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了,在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些也能处理过来。

如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M。

因为窗口越大,网络吞吐量就越大,传输效率就越高。

所以我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。

延迟策略:

  • 数量限制: 每隔N个包就应答一次
  • 时间限制: 超过延迟时间就应答一次

具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms

捎带应答

在延迟应答的基础上,存在很多情况下,客户端服务器在应用层也是”一发一收” 的。

意味着客户端给服务器说了”How are you”, 服务器也会给客户端回一个”Fine, thank you”。那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine, thank you” 一起回给客户端

就是发送 ACK 的同时顺便带上这边要发送的数据(3次握手)

面向字节流

当我们创建一个TCP的socket,会同时在内核中创建一个发送缓冲区和一个接收缓冲区

调用 write 时,内核将数据会先写入发送缓冲区中,

如果发送的字节数太长,会被拆分成多个TCP的数据包发出,

如果发送的字节数太短,就会先在缓冲区里等待, 等到缓冲区长度达到设置长度,然后等到其他合适的时机发送出去。
调用 read 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区。

然后应用程序可以调用read从接收缓冲区拿数据。

TCP的一个连接,既有发送缓冲区, 也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。所以是全双工的。

并且由于缓冲区的存在,TCP程序的读和写不需要一一匹配。

例如:

写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;

读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read一个字节, 重复100次

粘包问题

粘包问题中的 “包”是指的应用层的数据包。

在TCP的协议头中,没有如同UDP一样的 “报文长度“这样的字段,只有一个序号这样的字段。

站在传输层的角度, TCP是一个一个报文过来的,按照序号排好序放在缓冲区中,但是站在应用层的角度,它看到的只是一串连续的字节数据。

但是不知道从哪个部分开始到哪个部分结束是一个完整的应用层数据包,这就是粘包问题。

如何避免粘包问题呢?

明确两个包之间的边界。

  • 对于定长的包,保证每次都按固定大小读取即可。

例如一个Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可

  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。
  • 对于变长的包,还可以在包和包之间使用明确的分隔符

(应用层协议是程序员自己来定义的, 只要保证分隔符不和正文冲突即可)。

对比UDP协议

UDP是一个一个把数据交付给应用层,有明确的数据边界。

站在应用层的角度, 使用UDP的时候要么收到完整的UDP报文要么不收,不会出现”半个”的情况。

TCP连接异常情况

  1. 进程终止:进程终止会释放文件描述符,仍然可以发送 FIN,和正常关闭没有什么区别。机器重启和进程终止一样。

  2. 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行 reset。

即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。
如果对方不在,也会把连接释放。
应用层的某些协议, 也有一些这样的检测机制.例如HTTP长连接中, 也会定期检测对方的状态.

比如 QQ 断线之后, 也会定期尝试重新连接。