目录
一、深刻理解read和write
(1)发送/接受缓冲区的存在
(2)缓冲区是什么样子的
二、UDP协议
(1)UDP协议报头格式:
(2)UDP的缓冲区
三、TCP协议
(1)TCP协议报头格式
(2)报头标志位以及序号
1.序号和确认序号
2.RST标记位
3.URG标记位和紧急指针
4.PSH标记位
(3)各种机制的实现逻辑
1.确认应答机制
2.超时重传机制
3.连接管理机制
4.滑动窗口
5.流量控制
6.拥塞控制
7.延迟应答
8.捎带应答
(4)TCP是面向字节流的协议
传输层位于网络层之上,应用层之下,是唯一一个负责总体的数据传输和数据控制的层次。它提供了端到端的通信服务,确保数据在源端和目的端之间可靠、有效地传输。
即应用层用于处理数据,而传输层则针对于socket套接字,实现将数据从一台主机传送到另一台主机。
一、深刻理解read和write
(1)发送/接受缓冲区的存在
我们之前在进行网络通信的时候,无论是使用UDP协议还是TCP协议,都会用到read和write这两个系统调用,当时我们说这个就和文件类似,但今天我们要说read和write其实并不是真正传输数据进行网络通信的的那个人,而是将应用层的数据拷贝交给系统层面的一个缓冲区,再由系统自动完成由发送缓冲区到接收缓冲区的过程。(如下图)
(2)缓冲区是什么样子的
在传输层的缓冲区其实是一个环形队列(在某些系统中是这样实现的),这个和TCP协议中的滑动窗口有着密切的联系,我们在后面会细说。如果把环形队列看成一个数组,则该缓冲区天然就像一个char[ ]数组,每一个字节都有着自己的下标。TCP报头中是有着序号这一概念的,这里的序号取的就是缓冲区中数据段的首元素地址下标。
当一个程序调用write系统调用的时候,其实是把自己的应用层报头+数据打包到一起,拷贝到发送缓冲区中,而write这个函数考虑的就是把该数据放到环形队列中的哪一块区域(因为环形队列中肯定有其他数据,有已经发送完成需要丢弃的,还有等待发送的,还有正在发送的)。当他拷贝完成后会交付给下一层,即网络层,在交付的时候会添加传输层报头,如TCP/UDP。
而read函数同理,他是从接受缓冲区中把数据拷贝到应用层,交给应用代码来处理。
二、UDP协议
(1)UDP协议报头格式:
UDP协议比较简单,从他的报头信息就能看出。
他的报头中仅仅只有目的端口号+源端口号+UDP长度+UDP校验和。其中这两个端口号表示的是发送端和接收端的应用程序绑定的那个端口号。而UDP长度表示报头+正文数据一共有多长,这样应用层在拿到UDP数据报的时候,就能先看UDP的长度是多少,然后再决定要读多少个字节了。
应用层交给 UDP 多长的报文, UDP 原样发送, 既不会拆分, 也不会合并。
比如:
(2)UDP的缓冲区
1. UDP没有真正意义上的发送缓冲区,但是有接收缓冲区。这是因为UDP协议比较简单,系统并不需要像TCP一样做很多处理,而且UDP是不可靠传输,没有超时重传,丢包重传等机制,即只要应用层有数据交给我传输层,我才不管你发送的情况呢,我直观往后发送就行了。所以在调用sentto系统调用的时候,会直接把数据从应用层交给内核,由内核将数据交给网络协议栈进行后续操作。
2.UDP具有接收缓冲区。虽然UDP是不可靠传送,但是也不能出现明显的纰漏,如果同时有几个人给我发数据,且没有接收缓冲区的存在的话,操作系统由于正在处理上一次的通信,无法继续接收,则会选择丢弃数据,这样出现丢包的情况就太大了。所以UDP虽然不可靠,但还是有一个接收缓冲区的,同一时间来了大量的数据会先放到缓冲区,以后处理一个拿一个。如果缓冲区被写满了,后续到来的数据仍然会被丢弃。
3.UDP接收到的数据是无序的。即可能先发送的数据后面才到,后发送的数据却先到了。这是因为网络传输需要经过多个节点多个路由器等,可能有的数据在某个节点卡了很久才被送到。
注意:我们看到UDP报头中的16位UDP长度,这个限制了UDP数据报的长度,即2^16次方,即64kb,在现代网络通信中64kb是一个很小的大小,如果应用层要传输一个较大的信息,需要手动在应用层进行分包,多次传输。且在接受端手动拼接。
三、TCP协议
(1)TCP协议报头格式
每一次发送信息,即使只有一个ACK应答,发送的也是一个完整的报头。即除了选项和数据其他的都得有。
(2)报头标志位以及序号
1.序号和确认序号
我们先来谈谈序号和确认序号。
序号:
确认序号:
序号的存在,给每一个数据段都标号了。即使两个数据前后乱序到达接收端,也能根据序号先将数据段在缓冲区中重新排队。
而确认序号是上一次数据的最后一个下标+1。他表示的是在这之前的数据我都接受到了。比如发送端发来三个数据段,分别为1000-2000(序号1000),2000-3000(序号2000),3000-4000(序号3000)。但是由于网络问题接收端只收到了2000和4000,那么接收端在返回ACK应答的时候的确认序号就是1001,表示1-2000的数据我已经收到了,这样发送端就能知道自己2000-3000段的数据丢失了,于是触发重传机制。
2.RST标记位
3.URG标记位和紧急指针
URG标记位相当于紧急指针的使能位
4.PSH标记位
(3)各种机制的实现逻辑
1.确认应答机制
确认应答机制让发送端只要发送一个数据,接收端接收到了之后,就要立即返回一个报头,其中的ACK标志位置1,表示我已经收到了。但是接收端在收到ACK应答之后不会再对应答做应答,否则就演变成了鸡生蛋蛋生鸡的问题永无止境。
TCP 将每个字节的数据都进行了编号. 即为序列号.
每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下
一次你从哪里开始发.
2.超时重传机制
但是, 主机 A 未收到 B 发来的确认应答, 也可能是因为 ACK 丢失了。那么主机A仍然会重发给主句B,直到得到主机B的ACK应答。
因此主机 B 会收到很多重复数据。那么 TCP 协议需要能够识别出那些包是重复的包, 并
且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。
那么, 如果超时的时间如何确定?
TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
3.连接管理机制
或许你曾听说过TCP的三次握手,四次挥手。他们具体是什么含义,又该如何理解?请听我慢慢道来。
下面TCP三次握手四次挥手的示意图。(注意这里的客户端和服务器是对等的关系,可以左右倒置)
三次握手:
1.当客户端想访问服务器的时候,首先要向服务器发送SYN报头。
2.服务器会返回一个ACK,再返回一个SYN,但是一般这两步都会融合成一个(捎带应答)。
四次挥手:
1.客户端消息发送完了想和服务器断开连接,首先发送FIN报头。
2.服务器收到FIN之后,自身变为ClOSE_WAIT状态,并返回ACK应答。
3.服务器再发送FIN报头,客户端接受到之后变成TIME_WAIT状态,并返回ACK应答,陷入等待。等待完成后客户端关闭连接。
4.服务器接收到ACK应答后,立马关闭连接。
服务器和客户端的状态变化过程:
如何理解TIME_WAIT状态?
当启动服务器后,用一个客户端连接,马上用ctrl+C关闭服务器。然后立马重启服务器会发现该端口被绑定了,启动失败。
这是因为,虽然 server 的应用程序终止了,但 TCP 协议层的连接并没有完全断开,因此不
能再次监听同样的 server 端口. 我们用 netstat 命令查看一下:会发现该端口陷入了CLOSE_WAIT状态。
为什么需要TIME_WAIT状态?
1.最后一次ACK应答我们说发送端是无法知道对面到底有没有接受到的。如果对方在1MSL没有接收到ACK应答,则会重新发送FIN报头,此时由于还没有完全关闭,处于TIME_WAIT状态,就能重发ACK,一来一回就是2MSL时间。
2.一个端口被使用后,关闭。TIME_WAIT可以短暂时间内禁止其他应用程序绑定该端口,造成上一次由于网络传输较慢到达的数据,被下一个绑定该端口的进程当做自己的数据处理,造成数据混乱。
解决 TIME_WAIT 状态引起的 bind 失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的。如下:
使用 setsockopt()设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符。从而达到快速使用相同端口号的目的。
以下由文心一言生成:
如何理解 CLOSE_WAIT 状态?
CLOSE_WAIT是TIME_WAIT的对应面。出现CLOSE_WAIT是因为没有进入到LAST_ACK状态,即自己没有调用close触发FIN发送。对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.
4.滑动窗口
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个 ACK 确认应答. 收
到 ACK 后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.
而滑动窗口的存在,可以让发送端一次发送多条数据段,暂时不需要应答。这样就使得效率提高了。(其实是将多个段的等待时间重叠在一起了)
快重传能保证传输的效率性,而超时重传就好像个保底的,在网络较差的情况下确认网络状态,降低丢包率。
5.流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这
个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端如何把窗口大小告诉发送端呢? 回忆我们的 TCP 首部中, 有一个 16 位窗口字段,
就是存放了窗口大小信息;
那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么?
实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位(即扩大M倍);
但发送端真正的滑动窗口大小其实不仅仅由发送端来决定,还和我们拥塞控制密切相关,下面讲。
6.拥塞控制
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开
始阶段就发送大量的数据, 仍然可能引发问题.因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
注意:此处的1并非是一个字节,而是一个报文段(MSS)的大小,MSS是TCP报文段的最大长度
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速
度非常快。
拥塞窗口的大小是动态变化的,它根据网络的拥塞状况和TCP连接的状态进行调整。这种调整机制使得TCP连接能够在不引起网络拥塞的情况下高效地传输数据。
7.延迟应答
如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小.
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率,而延迟应答就能一定程度上增大滑动窗口大小,提高网络吞吐量。
那么所有的包都可以延迟应答么? 肯定也不是。具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms
8.捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收"的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine,thank you";那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端。
最典型的一个例子就是三次握手。客户端发送一个SYN请求后,一般情况下应该是服务器先发送ACK应答,再发送SYN请求。但是由于服务器一般需要无条件有向客户端连通的意向,所以服务器是一定要发送SYN请求的,这时候往往就能和ACK应答合并成一个应答。
(4)TCP是面向字节流的协议
即TCP并不会像UDP一样一条消息就是一条消息。而是像水流一般无间断的,这样就需要我们程序员在应用层手动划分各个报文,从而读取到真正的数据而不会产生混乱。即需要粘包处理。
如何处理粘包问题?