🔥 之前在这篇文章 传输层协议 UDP 中已经说过关于传输层的部分内容,现在我们来了解一下传输层 TCP 的内容吧
Transmission Control Protocol"
). 它对数据传输进行了详细的控制。TCP头部包含多个字段,每个字段都有特定的功能,以确保数据能够可靠地从一个端点传输到另一个端点。理解TCP的报头:
Linux
内核是C语言写的,在 UDP
说过报头是协议的表现,而协议本质就是结构体数据。所有 tcp报头 就是一个结构化或位段。
struct tcphdr
这是一个类型,可以定义出一个对象。把应用层的数据拷贝到缓冲区里,然后把报头拷贝到前面,这不就是添加报头嘛
TCP协议报文也有自己的 报头+有效载荷,这个 有效载荷 是 应用层的报文,当然包含应用层报头和有效载荷
ACK
标志位为 1 时有效;帮助解决丢包问题TCP
头部最大长度是 15 * 4 = 606 位标志位:
Urgent
): 紧急指针是否有效Acknowledgment
): 确认序号是否有效Push
): 提示接收端应用程序立刻从 TCP
缓冲区把数据读走Reset
): 对方要求重新建立连接; 我们把携带 RST
标识的称为 复位 报文段Synchronize
): 请求建立连接; 我们把携带 SYN
标识的称为 同步 报文段Finish
): 通知对方, 本端要关闭了, 我们称携带 FIN
标识的为 结束 报文段TCP协议的特点
报文宽度:0-31 bit 是这个报文的宽度。每行4个字节,总共5行,因此标准 TCP 报文的长度是20字节,选项部分暂不考虑 TCP 报文标准长度:标准 TCP 报文长度是20字节
如何封装解包,如何分用?
在了解如何分用之前,需要先来看看作为接收方,其 如何保证把一个 TCP 报文全部读完呢?其实很简单,具体步骤如下:
① 读取 TCP 标准报头
② 计算 TCP 报头总长度
③ 确定报头长度的计算
④ 计算选项长度
分离有效载荷 🍎 一旦读取完 TCP 报头,剩下的数据即为 有效载荷 ,将其放入 TCP 接收缓冲区供上层继续读取。
隐含问题:TCP 与 UDP 报头的区别
封装和解包的逆向过程 🍎 解包完成后,封装的过程也可以反向推导出来。只要能解包,就可以逆向封装报文
如何分用 TCP 报文 🍎 在 TCP 报头中有 目的端口号,通过该端口号可以定位应用层的进程,将数据交付给相应进程。
如何通过端口号找到绑定的进程? 当接收到一个报文,如何找到绑定了特定端口的进程呢?以下是过程解析:
1、网络协议栈与文件的关系:
2、通过哈希表定位进程:
数据报文经过 OS 各层处理,最终将有效载荷存放到文件的缓冲区中。上层应用可以通过文件的方式统一读取网络数据,实现了网络数据的封装与解包。
谈 TCP 必谈可靠性,但在讨论可靠性之前,先考虑几个问题:❓
为什么网络传输时会存在不可靠的问题? 不可靠问题常见的场景有哪些? TCP 的可靠性如何保证?
以前我们学过冯诺依曼体系结构,里面包括 CPU、内存、外设(如显示器、键盘、鼠标、磁盘等),这些设备都是独立的。但我们可以将键盘的数据放入内存,也可以将内存中的数据传送到 CPU
,这说明各个硬件并非孤立的,它们之间是有联系的。这些设备通过计算机中的“线”连接。
内存和外设之间的通信也有自己的协议。因为有协议,所以可以控制外设。而这类协议的开发者通常属于“嵌入式”领域
虽然内存和外设之间有通信协议,但我们并未讨论它们之间的可靠性问题。原因在于它们之间的距离很近,不存在网络传输中的可靠性问题
网络传输中的不可靠性场景
为什么网络传输时会存在不可靠的问题? 原因:传输距离变长了 常见的不可靠场景有哪些?
如何理解 TCP 的可靠性? 假设两个人 A 和 B 之间相隔 500 米,A 问 B:“你吃饭了吗?” A 不能确定 B 听到了,除非 A 收到 B 的应答。所以,只有在收到应答的情况下,A 才能确认 B 听到了这句话。
但当 B 给 A 回复“我吃了”时,B 也无法确定 A 是否收到了这条信息,如果 B 没有给回应的话, A 也会继续进行询问。同样,只有当 A 回应后,B 才能确定 A 收到了“我吃了”这条信息,如果 A 没有给回应的话, B 也会继续进行询问
这个例子说明了以下三点:
1️⃣ 只有收到了应答,才能100%确认对方收到了之前的信息。 —— 确认应答后,消息才算可靠 2️⃣ 通信中总会存在最新消息没有得到应答的情况。 —— 最新消息一般无法保证可靠性,原因:总有一条最新的消息是没有应答的(这句话也就是表示 相当于老信息是有应答的, 100% 可靠 )
因此,传输距离变长后,不可能存在绝对可靠性,只能保证相对可靠性。只要收到应答,就能保证该报文的可靠性。这就是 TCP 可靠性的基础:确认应答机制
实际通信过程中 TCP 工作模式如下:
因此,通过确认应答机制,双方的数据传输都能保证可靠性。这些请求和应答是通过封装成 TCP 报文进行发送的。在实际通信中,除了正常的数据段,通信时也包含 确认数据段
捎带应答机制
在实际工作模式中,确认应答可以与对请求的响应一起打包发送。以 A 和 B 的例子为例,A 问 B “你吃饭了吗?” B 本来应先确认收到消息,再回复“我吃了”。但 B 可以直接回复“我吃了”,这一条消息既是对 A 的确认应答,也是 B 给 A 的新消息 这就是所谓的 捎带应答
批量确认的工作模式
另一种工作模式是 批量确认。Client 可以一次性给 Server 发出多个请求,Server 则可以批量确认这些请求,而非逐条应答。这种模式下,请求和应答是并发的。
结论:
不管是串行确认还是批量确认,原则上,无论是 C->S 还是 S->C,每个正常的数据段都需要应答来保证可靠性。但最新的一条消息是没有的
🫧 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B; 🫧 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行 重发
🎯 但是, 主机 A 未收到 B 发来的确认应答, 也可能是因为 ACK 丢失了;
🌊 因此主机 B 会收到很多重复数据. 那么 TCP 协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.
那么, 如果超时的时间如何确定?
最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”. 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
TCP
为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
BSD Unix
和 Windows
也是如此), 超时以 500ms
为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.500ms 后再进行重传。
500ms 进行重传. 依次类推,以指数形式递增,累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接。
16位校验和+选项我们不考虑,然后 4 位首部长度下面也不管,接下来学习 tcp
32 位序号 和 32 位确认序号 以及 16 位窗口 还有六个标志位
作用
计算规则
作用
计算规则
今天,客户端 © 可能向服务器 (s) 发送信息,也可能是服务器向客户端发送信息。由于双方都使用 TCP 协议,所以 TCP 的双方地位是对等的。要了解 TCP,只需要搞清楚一个方向的通信过程,反过来,另一个方向的通信也是一样的。
① TCP 真实工作模式
🔥Client
可能一次给 Server
发送多个请求报文,而 Server
也可以一次给 Client
发送多个确认应答。
② 问题分析 1️⃣ 数据的顺序问题:
2️⃣ 确认与请求的对应关系:
③ TCP 报文序号机制 为了保证每个请求和应答可以对应上,TCP 请求报文(数据段)需要有方式标识数据段本身,因此每个数据段都有自己的 32位序号。
④ 确认应答的机制 Server 给 Client 的应答报文中,需要和请求报文一一对应。因此,应答报文的报头中会包含确认序号,这样 Client 可以知道应答是对哪个请求的。 序号与确认序号的对应规则:
确认序号的含义: 确认序号表示接收方已经收到了该序号之前的所有报文(连续且无遗漏),并告知对方下次发送从该确认序号开始。
⑤ 丢包场景下的应答机制 如果某个报文丢失了,比如:
为什么这样设计? 这是为了支持 TCP 的滑动窗口机制,使得确认序号可以线性右移,从而保持可靠的数据传输。
⑥ 两组序号的必要性 ❓为什么需要 32 位序号和 32 位确认序号?缺一不可的主要原因:
序号的作用
确认序号的作用
为什么不能只凭序号?
结论:我们可以把我们上面所说的东西总结为如下流程图:
🌲 TCP 将每个字节的数据都进行了编号. 即为序列号(这里我们可以想像成字节数组
⚡️ 每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 确认序列号之前的报文我已经全部收到了; 下一次你从哪一个序号开始发.
注意:有时候也会出现序号不够用的情况,那么就会进行 回绕,但是在正常情况下,一个通信周期内基本不会,毕竟真到了回绕,历史报文也早消失了, 因为缓冲区大小是有限的
缓冲区的作用
流量控制的必要性
因此此时就需要一个合适的速度:为了确保数据传输既不过快也不过慢,需要一种机制来调节发送速率 — 流量控制
但是这里有个问题:客户端凭什么进行流量控制,它是怎么知道对方来不及进行接收了呢?
流量控制机制
但是现在有个问题:由于双方都要发信息和应答,因此双方都要进行 流量控制, 那么 这个 16 位窗口大小应该填谁的?
因此上面这个字段填入的是接收方的接收缓冲区剩余空间大小,而不是发送方的。
全双工通信
💡 结论: 因此,每个方向上的TCP报文都包含一个16位窗口大小字段,表示自己的接收缓冲区剩余空间大小。 交换接收能力
通过这种方式,TCP协议不仅确保了数据的可靠传输,还有效地管理了网络带宽的使用,提高了整体的通信效率。
虽然有的 TCP 标准包含 8 个标记位,但我们主要学习其中 6 个最常用的。
上面说过数据段在来回通信的时候,有的是正常的数据报文,有的是确认报文。 这里我们就可以 理解 tcp
报文也是有类型的!
6 个标志位来表示不同类型的报文。
站在服务器的角度它一定会收到各种各样的tcp报文!所以接收方要根据不同的tcp报文,要有不同的处理动作!
1、SYN 标记位
2、FIN 标记位 FIN 标记位用于表示断开连接请求报文,默认情况下该标记位为 0,当置为 1 时表示这是一个断开连接的请求。携带 FIN 标识的报文称为结束报文段。
3、ACK 标记位
4、PSH 标记位
5、URG 标记位(紧急插队)
💡 举个例子:
通信时给对方发大量数据,对方的接受缓冲区拉满了,但是突然又终止通信了,然后就会发一个终止的报文,然后对端也需要先把之前的数据接受完了,才能接收到终止报文才可以终止掉请求,但是这种情况终止的时间可能会特别长 因此在这种情况下,我们想立即终止,把 URG=1,此时标识当前报文为紧急报文,该紧急报文一旦被对方接受,即便对方的接受缓冲区还残留量一些数据,那么OS 也会把该报文的有效数据提取出去,方便让上层在 read 的时候优先读到,那么可以提前终止我们的请求了
注意:这里报文中有效载荷并不都是紧急数据!
那么此时有个问题:将URG标志位置为1 ,上层怎么读数据❓
read
优先读取现在这个紧急指针偏移量我知道 ,那这个紧急数据到哪里结束呢? --> 难道到有效载荷的结尾?
并不是,根据16位紧急指针找到偏移量以 字节 为单位,往后读取一个字节就是紧急数据。紧急数据不需要排队直接被上层读取,一般这个URG这个1字节数据也成为 带外数据
带外数据并不是tcp帮我们主动弄这个功能,而是tcp提供这个功能供上层选择,我们自己在写服务器的时候可以自己选择正常读数据之前有没有 带外数据
6、RST 标记位 在看 这个之前,可以先把后面的三次握手、四次挥手先理解了,再看
RST
是 reset
的简写。在写套接字TCP协议的时候我们曾经说过,通信双方在通信之前必须要把三次握手建立好才能进行通信。
应用场景分析:
❓ 三次握手建立连接,三次握手一定能保证握手成功吗?不一定!
这个世界上没有100%一定成功的,并且我们也知道三次握手最后一次ACK是没有应答的,可能会出现握手失败的情况。
同理四次挥手也一样!人家只是在TCP这里设立了建立连接三次握手、断开连接四次挥手,但可没说一定成功。
❓ 但是客户端知不知道服务器重启过呢?并不知道,你服务器又没有给我四次挥手。
所以就可能会存在
client
认为连接还存在,服务器认为连接不存在。
如果 client
认为连接还存在会出现什么问题?是不是就直接发报文了。
SYN
,服务器收到这个报文很奇怪,我和你并没有建立连接,我们协议规定好我们通信之前要先建立连接,客户端你直接把数据发过来了RST
标识的称为 复位报文段 。有了RST标记位,双方连接建立一方认为成功一方认为不成功,那么后序在通信的时候,认为不成功的一方就把 连接重置了。☕️ 在正常情况下,TCP 要经过 三次握手 建立连接, 四次挥手 断开连接
连接的理解
理解:双方通信之前必须先建立连接,经历三次握手。所谓的三次握手就是双方有来有回的吞吐了三个报文
SYN_SENT
SYN_RCVD
,然后发送 SYN + ACKESTABLISHED
,然后发送 ACKESTABLISHED
(关于三次握手关注问题的角度
三次握手是建立连接的机制,可没有说一定能保证握手成功。包括四次挥手也是一样的
① 一次握手:不可行,会导致 SYN
洪水攻击。占用了大量服务器资源
② 二次握手:不可行,客户端未收到 ACK
时,服务器可能认为连接已建立(浪费了服务器的资源
③ 三次握手:最小成本验证全双工通信信道通畅,防止单主机对服务器进行攻击。
1️⃣ 减少服务器受到的 SYN 洪水攻击
❓ 问题:三次握手能否完全防止服务器受到 SYN
洪水攻击
回答:
TCP
握手来解决的。如果攻击者使用多台机器进行攻击,即使有三次握手机制,服务器仍然可能受到 SYN
洪水攻击。SYN
洪水攻击的影响,但其初衷是为了避免连接建立过程中的明显漏洞,而不是专门针对 SYN
洪水攻击设计的。2️⃣ 三次握手才可以阻止重复历史连接的初始化
– 双方验证全双工信道的通畅性
假设有这样一种场景,客户端给服务端发送了一个 SYN
报文(seq=100
),但是这个报文由于网络波动阻塞了,于是客户端又重新发送了一个新的 SYN
报文(seq=200
),注意不是重传,重传的 SYN
的序列号是一样的。
SYN
报文被网络阻塞后,再次发送新的 SYN
报文,由于旧的报文比新的报文先抵达服务端,服务端肯定会回一个 SYN+ACK
报文给客户端,此时客户端就可以根据这个报文来判断这是一个 历史连接,由此客户端就会发送一个 RST
报文,要求断开此次连接(即历史连接)。等新的 SYN
报文抵达服务端后,才会正式的建立连接。如果是两次握手,那就不能阻止历史连接
SYN
报文后,就进入了 ESTABLISHED
状态,这就意味着服务端此时就可以给客户端发送数据了,但是客户端还没有进入ESTABLISHED
状态,必须收到服务端的 SYN + ACK
报文后,才会进入ESTABLISHED
状态。在上面可以看出,当服务端收到第一个
SYN
报文后(旧的)就已经建立了连接(服务端并不知道这是历史连接),并且在回复给客户端的报文中携带了数据,但是客户端通过收到的SYN + ACK
报文,发现这是 历史连接
RST
报文后才会断开连接。因此,要解决这样的问题,客户端就必须在服务端发送数据之前来阻止掉这个历史连接,而要实现这个功能就需要三次握手。
3️⃣ 三次握手才可以同步双方的初始化序列号
⭕TCP协议通信的双方,都必须要维护序列号,序列号是实现可靠传输的一个关键因素,其作用如下:
当客户端给服务端发送SYN(携带着自己的序列号)报文的时候,需要服务端回一个 ACK
应答报文,表明客户端的SYN报文已经成功接收;
ACK
应答号还有服务端自己的序列号(发送的是 SYN + ACK
报文),也需要客户端回一个ACK应答报文,来确保服务端的 SYN
被成功接收;这样一来一回就能保证双方的初始化序列号被可靠同步。
① 三次握手其实是 “四次”
🌋三次握手的另一种解释:
SYN
和 ACK
应该是两个独立的报文,但由于效率考虑,通常将它们合并成一个报文发送。
② 四次、五次或更多次握手是否可行
🌌四次握手:
ACK
是由服务器发送的。🌌五次、六次等更多次握手:
③ 为什么要连接?
④ UDP不需要握手 原因:无需维护状态(UDP不需要维护双方通信状态,因此也不需要握手)
⑤ 三次握手失败发生什么
理解:虽然断开连接是双方的事,但是又由于 tcp
是全双工的,所以断开连接还需要征得双方同意。
状态变化:
FIN_WAIT_1
状态。CLOSE_WAIT
状态。LAST_ACK
状态。TIME_WAIT
状态。CLOSED
状态,至此服务器就已经完成连接的关闭注意:只有主动关闭连接的,才有 TIME_WAIT
状态
我们先来看看一些前置知识 – MSL,来方便对其的理解
MSL(Maximum Segment Lifetime) 定义:MSL 是 TCP 报文段在网络中能够存活的最长时间。超过这个时间后,报文段会被丢弃。
作用:
典型值:
OK,现在可以开始进入正题了,如下:
TIME_WAIT
状态。CLOSE_WAIT
状态。CLOSE_WAIT
状态:如果服务器不调用 close
,则不会发送 FIN
,一直保持 CLOSE_WAIT
状态。因此我们现在也可以知道:服务器端一旦使用完毕
sockfd
, 就需要关闭掉不用的sockfd
, 防止 fd 泄露 和 内存泄露(连接没有释放,而且这个也是占空间的)
现在做一个测试:首先启动 server,然后启动 client,然后用 Ctrl-C 使 server 终止,这时马上再运行 server,结果是如下:
TCP 协议规定:主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个 MSL (maximum segment lifetime) 的时间后才能回到 CLOSED 状态.
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 MSL 的值;
root$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
🤔 想一想, 为什么是 TIME_WAIT 的时间是 2MSL?
TIME_WAIT 状态作用
① 防止旧连接的报文干扰新连接(游离报文):
② 确保服务器收到最后一个 ACK:
🧑💻 在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的:
📚 使用 setsockopt()
设置 socket
描述符的 选项 SO_REUSEADDR
为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符:
// 保证服务器,异常断开之后,可以立即重启,不会有bind问题
int opt = 1;
int n = ::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
(void)n;
以之前写过的 TCP 服务器为例, 我们稍加修改将 套接字的文件描述符关闭的 close(); 这个代码去掉.
tcp 0 0 0.0.0.0:9090 0.0.0.0:* LISTEN 5038/./server
tcp 0 0 127.0.0.1:49958 127.0.0.1:9090 FIN_WAIT2 -
tcp 0 0 127.0.0.1:9090 127.0.0.1:49958 CLOSE_WAIT 5038/./server
对于服务器上出现大量的 CLOSE_WAIT
状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成。这是一个 BUG. 只需要加上对应的 close
即可解决问题.
注意:无论是主动还是被动 和 是否客户端或是服务器没无关,因为TCP是地位对等的协议
虽然此时服务是处于
close_wait
状态,但是如果我们把进程直接关掉,不管是服务器还是客户端都会变成LAST_ACK
, 因为服务器生命周期是随进程的 就直接 close 了,而客户端也早已不在了
❓ 如何让服务器一直处于 CLOSE_WAIT 状态,不继续往下走?
close
!那服务器只是被动触发完成两次挥手,因为不会调用close所以也不会给客户端发送FIN也就不会进入LAST_ACK状态。服务器一直处于CLOSE_WAIT状态。主要原因:服务器端通常需要等待完成数据的发送和处理,所以服务器端的 ACK 和 FIN 一般都会分开进行发送,导致比三次握手多了一次
🦇 四次挥手为啥不能变成三次挥手?
原因 1:TCP 是全双工协议
原因 2:确保数据完整性
原因 3:防止过早关闭连接
🦇 为什么不能将 FIN 变成捎带应答?
① 时序问题 – 确保数据完整性
② 状态不一致
① 第一次挥手失败,发生如下:
如果第一次挥手丢失了,那么客户端会迟迟收不到被动方的 ACK ,这样的话就会触发 超时重传 机制,重传 FIN
报文
② 第二次挥手失败,发生如下:
由于前面已经提到了 ACK 报文是不会进行重传的,因此如果服务器的第二次挥手丢失,那么客户端依然会触发超时重传 机制,重传 FIN
报文,直到收到服务端的第二次挥手 或者 达到最大的重传次数
对于
close
函数的连接,由于无法再发送 和 接收数据,所以FIN_WAIT_2
状态也不可以持续太久,而tcp_fln_timeout
控制了这个状态下连续的持续时长,默认值是 60 s
③ 第三次挥手失败,发生如下:
如果迟迟收不到这个 ACK,服务器端就会重发 FIN
报文,重发次数仍然由 tcp_orphan_retries
参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的
④ 第四次挥手失败,发生如下:
在 Linux 系统中,TIME_WAIT
状态会持续 2MSL 后才会进入关闭状态
💻 服务端状态转化:
CLOSED -> LISTEN
] 服务器端调用 listen
后进入 LISTEN
状态, 等待客户端连接;LISTEN -> SYN_RCVD
] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送 SYN
确认报文.SYN_RCVD -> ESTABLISHED
] 服务端一旦收到客户端的确认报文, 就进入 ESTABLISHED
状态, 可以进行读写数据了.ESTABLISHED -> CLOSE_WAIT
] 当客户端主动关闭连接 (调用 close), 服务器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT;CLOSE_WAIT -> LAST_ACK
] 进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时, 会向客户端发送 FIN, 此时服务器进入 LAST_ACK
状态, 等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN
)LAST_ACK -> CLOSED
] 服务器收到了对 FIN 的 ACK, 彻底关闭连接.💻 客户端状态转化:
CLOSED -> SYN_SENT
] 客户端调用 connect, 发送同步报文段;SYN_SENT -> ESTABLISHED
] connect 调用成功, 则进入 ESTABLISHED 状态, 开始读写数据;ESTABLISHED -> FIN_WAIT_1
] 客户端主动调用 close 时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1;FIN_WAIT_1 -> FIN_WAIT_2
] 客户端收到服务器对结束报文段的确认, 则进入 FIN_WAIT_2, 开始等待服务器的结束报文段;FIN_WAIT_2 -> TIME_WAIT
] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出 LAST_ACK;TIME_WAIT -> CLOSED
] 客户端要等待一个 2MSL (Max Segment Life, 报文最大生存时间) 的时间, 才会进入 CLOSED
状态.下图是 TCP 状态转换的一个汇总:
较粗的虚线表示服务端的状态变化情况,较粗的实线表示客户端的状态变化情况, CLOSED
是一个假想的起始点, 不是真实状态
💻 在上面已经讨论了确认应答策略:对每一个发送的数据段,都要给一个 ACK 确认应答,收到 ACK 后再发送下一个数据段。这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.
‼️ 既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值(由接收方的缓冲区剩余空间决定). 上图的窗口大小就是 4000 个字节(四个段).
滑动窗口的大小实际上就是对方接收缓冲区剩余空间的大小,左侧是已经发送完且 ACK 完毕的
滑动窗口如何滑动? 窗口的边界:
窗口的滑动:
环形缓冲区:
滑动窗口丢包问题 🎯 那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论.
情况一:数据包已经抵达, ACK 被丢了.
这种情况下, 部分 ACK 丢了并不要紧, 因为可以通过后续的 ACK 进行确认;
情况二:数据包就直接丢了
📚 这种机制被称为 “高速重发控制” (也叫 “快重传”).
在这种机制下,如果滑动窗口最左侧的数据丢失,接收方会触发快速重传机制,要求发送方重新发送丢失的数据包。
🧑💻 通过这种设计,滑动窗口机制能够高效处理不同位置的数据丢失问题,确保数据传输的可靠性和连续性
注意:虽然这里的快重传不仅快,还能重传,但是还是需要超时重传,因为快重传是用来提高效率的(其是有条件的),而超时重传是兜底的
条件:收到 3 个同样确认应答时则进行重发
滑动窗口的特点
需要借助滑动窗口实现
定义与目的:
Flow Control
)如何确定对方初始接收能力:
动态调整窗口大小:
注意:接收端如何把窗口大小告诉发送端呢? 回忆我们的 TCP 首部中,有一个 16 位窗口字段,就是存放了窗口大小信息(上面有说的)
关于 TCP 大小:
① 立即应答问题
② 延迟应答 等待一段时间后,再返回窗口大小
举例:
那么所有的包都可以延迟应答么? 肯定也不是;
③ 延迟应答限制
④ 确认序号 TCP不一定对每个报文都应答,确认序号表示之前连续报文已收到。
🧑💻 在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”;
那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端
① 报头应答 主机A给主机B发信息,主机B 返回 ACK+发给 A 的 data
② 捎带应答实现 主机 B 可以携带报头它也有序号也可以携带有效载荷,发送数据 主机B给主机A应答时,将ACK标记位设为1,并携带B给A的信息。
TCP的可靠性机制:
网络问题与重传决策:(与超时重传对比学习)
因此虽然 TCP 有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
拥塞控制的重要性:
🧑💻 因此TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
此处引入一个概念称为拥塞窗口
⭕拥塞窗口与滑动窗口的关系: ① 客户端:发送窗口 ② 网络:拥塞窗口 ③ 服务器:接收窗口(自己的接收能力)
发送窗口的最终上限由谁决定? 🧑💻 发送方的实际发送窗口大小 发送窗口 = min(rwnd, cwnd)。
💻 像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指:初始时慢, 但是增长速度非常快.
慢启动阈值:
💡动态调整与拥塞检测:
拥塞控制:归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又避免给网络造成太大压力的折中方案 ,旨在快速传输数据同时防止网络过载。
慢启动不会无限进行,其增长会在达到一个特定阈值时停止,这个阈值称为慢启动门限(ssthresh)。
慢启动门限的定义:慢启动门限是一个状态变量,用于控制慢启动和拥塞避免算法的切换。
慢启动与拥塞避免的切换条件
拥塞避免的触发条件
ssthresh
时,进入拥塞避免算法。拥塞避免的ssthresh值通常设置为65535字节。
⭕ 拥塞避免的增长规则
拥塞状况的识别 随着cwnd的增长,网络逐渐进入拥塞状态,出现丢包现象。
拥塞发生时的处理:重传机制(包括超时重传 和 快速重传)
超时重传的拥塞发生算法
快速重传的拥塞发生算法
快速恢复的前提
快速恢复的步骤
💦 创建一个 TCP 的 socket, 同时在内核中创建一个 发送缓冲区 和 一个 接收缓冲区
1、调用 write
时, 数据会先写入发送缓冲区中;
2、接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
3、然后应用程序可以调用 read
从接收缓冲区拿数据;
另一方面,TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据。这个概念叫做 全双工
💻 由于缓冲区的存在, TCP 程序的读和写不需要一一匹配,例如:
如何避免粘包问题❓ 本质:明确两个包之间的边界.
💡 思考: 对于 UDP 协议来说, 是否也存在 “粘包问题” 呢
对于 UDP, 如果还没有上层交付数据, UDP 的报文长度仍然在. 同时, UDP 是一个一个把数据交付给应用层. 就有很明确的数据边界。 站在应用层的站在应用层的角度, 使用 UDP 的时候, 要么收到完整的 UDP 报文, 要么不收. 不会出现"半个"的情况。
🎃进程终止:进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别. 🎃机器重启:和进程终止的情况相同. 🎃机器掉电/网线断开:接收端认为连接还在, 一旦接收端有写入操作,接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP 自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接.
为什么 TCP 这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能. 可靠性:
校验和、序列号(按序到达)、确认应答、超时重发、连接管理、流量控制、拥塞控制
提高性能:
滑动窗口、快速重传、延迟应答、捎带应答
其他:
定时器(超时重传定时器, 保活定时器, TIME_WAIT 定时器等)
基于 TCP 应用层协议的如下:
HTTP、HTTPS、SSH、Telnet、FTP、SMTP 当然也包括我们自己写 TCP 程序时自定义的应用层协议;
TCP vs UDP 🦁 我们说了 TCP 是可靠连接, 那么是不是 TCP 一定就优于 UDP 呢? TCP 和 UDP 之间的优点和缺点, 不能简单, 绝对的进行比较。
👻 归根结底, TCP 和 UDP 都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.
用 UDP 实现可靠传输(经典面试题) 参考 TCP 的可靠性机制, 在应用层实现类似的逻辑; 🧑💻 例如: