文章目录
- 1.udp协议
-
- [1.1 udp协议端格式](#1.1 udp协议端格式)
- [1.2 udp特点](#1.2 udp特点)
- [1.3 udp缓冲区](#1.3 udp缓冲区)
- 2.tcp协议
-
- [2.1 tcp协议端格式](#2.1 tcp协议端格式)
- [2.2 三次握手四次挥手](#2.2 三次握手四次挥手)
-
- [2.2.1 三次握手](#2.2.1 三次握手)
- [2.2.2 四次挥手](#2.2.2 四次挥手)
- [3. tcp特性](#3. tcp特性)
-
- [3.1 可靠性](#3.1 可靠性)
-
- [3.1.1 超时重传](#3.1.1 超时重传)
- [3.1.2 流量控制](#3.1.2 流量控制)
- [3.1.3 拥塞控制](#3.1.3 拥塞控制)
- [3.2 提高性能](#3.2 提高性能)
-
- [3.2.1 快速重传](#3.2.1 快速重传)
- [3.2.2 滑动窗口](#3.2.2 滑动窗口)
- [3.2.3 延迟应答](#3.2.3 延迟应答)
- [3.2.4 捎带应答](#3.2.4 捎带应答)
- [3.3 粘包问题](#3.3 粘包问题)
- 希望读者们多多三连支持
- 小编会继续更新
- 你们的鼓励就是我前进的动力!
sock 编程主要是应用层的,应用层只负责把数据从缓冲区读,或写到缓冲区,数据怎么发,发多少,什么时候发,出错了怎么办,都是由传输层协议决定的,下面将详细介绍常见协议的传输层控制
1.udp协议
1.1 udp协议端格式

UDP 协议端格式: 16 位 UDP 长度,表示整个数据报(UDP 首部 + UDP 数据)的最大长度
即整个报文数据包为 16 字节,报头 8 字节,正文数据 8 字节
1.2 udp特点
- 无连接: 知道对端的
IP和端口号就直接进行传输,不需要建立连接 - 不可靠: 没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,
UDP协议层也不会给应用层返回任何错误信息,不保证可靠性,所以网络不好丢包可能性很大 - 面向数据报: 不能够灵活的控制读写数据的次数和数量,应用层交给
UDP多长的报文,UDP原样发送,既不会拆分,也不会合并
用 UDP 传输 100 个字节的数据:如果发送端调用一次 sendto,发送 100 个字节,那么接收端也必须调用对应的一次 recvfrom,接收 100 个字节;而不能循环调用 10 次 recvfrom,每次接收 10 个字节
1.3 udp缓冲区
UDP 没有真正意义上的发送缓冲区,但是在物理意义上是有的,因为 UDP 是无连接、不可靠的协议,调用 sendto() 函数时,内核会尽可能将数据拷贝到发送缓冲区,然后立刻交给网络层进行封装和发送,不会像 TCP 那样在缓冲区中排队等待确认
UDP 具有接收缓冲区,但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致; 如果缓冲区满了,再到达的 UDP 数据就会被丢弃、
尽管如此,UDP 还是能收能发,是全双工的
2.tcp协议
面向字节流: 把传输的数据看作连续、无边界的字节序列,而不是一个个独立的报文
举个例子:
- 发送
100字节数据时:
你可以调用1次write直接写100字节;
也可以调用100次write,每次只写1字节。 - 接收这
100字节数据时:
你完全不用关心发送端是怎么写的。
可以一次调用read读100字节;
也可以每次读1字节,重复100次。
这背后的原因是,TCP 会把发送端的多次 write 数据合并成连续的字节流,再传递给接收端;接收端的 read 操作只是从这个字节流中读取任意长度的数据,和发送端的写入次数无关。
2.1 tcp协议端格式

16 位源端口号: 标识发送该报文的应用程序(进程)
16位目的端口号: 标识接收该报文的应用程序(进程)
32 位序号 (seq): 用来标识从发送端发出的字节流。TCP 将每个字节都进行了编号,这个字段表示本报文段数据部分的第一个字节的编号,用于解决乱序问题 。注意起始发送的序号是随机的,这能避免历史重复可能引发的问题
32 位确认序号 (ack): 期望收到对方下一个报文段的第一个数据字节的序号。若确认号为 N,则表明序号 N-1 为止的所有数据都已正确收到。用于解决丢包问题
4 位首部长度: 指出 TCP 报头有多长(以 4 字节为单位)。最小值为 5(即 20 字节),最大值为 15(即 60 字节)
保留: 占 6 位,保留为今后使用,目前应置为 0,为 TCP 协议的未来扩展预留空间
标志位: 用于控制连接的状态和行为
-
URG: 表示紧急指针有效,紧急指针的取值不是绝对的字节地址,而是一个相对偏移量,假设当前报文段的序列号是1000,紧急指针的值是50,那么紧急数据的范围就是序列号1000到1049的字节,1050及之后的字节则为普通数据 -
ACK: 表示确认序号有效(连接建立后所有报文的ACK都要置1) -
PSH: 通常TCP会先把数据存在缓冲区,攒够了一定数量再交给上层。但如果PSH=1,接收方的TCP栈就不会等待缓冲区填满,而是直接把数据向上交付 -
RST: 表示连接出错,需要复位重连 -
SYN: 表示同步序号,用于发起一个新连接,只有在三次握手的前两次(客户端请求连接、服务端响应连接)时,SYN才会置为1 -
FIN: 表示发送方数据发送完毕,请求释放连接
16 位窗口大小: 告诉对方目前我这边还有多少接收缓冲区空间。用于流量控制,防止发送方发送过快导致接收方处理不过来
16位校验和: 对首部和数据进行差错检测,确保数据传输并未受损
16位紧急指针: 只有当 URG 标志置 1 时才有效,指出本报文段中紧急数据的字节数
选项: 长度可变,用于支持如"最大报文段长度 (MSS)"等扩展功能,这个基本可以忽略,很少用到
数据: 实际传输的应用层数据(如 HTTP 请求内容等)
2.2 三次握手四次挥手

2.2.1 三次握手
第一步:客户端请求连接(SYN)
- 动作 :客户端想和服务端建立连接,它会随机生成一个初始序列号
seq = x。 - 标志位变化 :
SYN = 1:表示这是一个连接请求。ACK = 0:因为还没收到对方的消息,所以确认号无效。
- 含义 :"你好,我是客户端,我想和你建立连接,我的序列号是
x。" - 状态 :客户端进入
SYN_SENT状态。
第二步:服务端回应并反向请求(SYN + ACK)
- 动作 :服务端收到了请求。它需要确认客户端的请求,同时也需要向客户端发起连接。它会生成自己的初始序列号
seq = y。 - 标志位变化 :
SYN = 1:表示服务端也想建立连接。ACK = 1:确认收到了客户端的消息。- 确认号
ack = x + 1:表示"我收到了你的x,下次请发x + 1给我"。
- 含义 :"收到!我知道你想连我(确认客户端发送能力正常)。我也想连你,我的初始序列号是
y。" - 状态 :服务端进入
SYN_RCVD状态。
第三步:客户端确认(ACK)
- 动作 :客户端收到了服务端的回复。此时客户端通过服务端的
ACK知道连接已建立,但还需要回复服务端,确认收到了服务端的SYN。 - 标志位变化 :
SYN = 0:连接已经建立,不再需要同步标志。ACK = 1:确认收到了服务端的消息。- 序号
seq = x + 1:这是第一步承诺要发的下一个序号。 - 确认号
ack = y + 1:表示"我收到了你的y,下次请发y + 1"。
- 含义:"收到!我也听清你了(确认服务端发送能力正常)。我们要开始传数据了!"
- 状态 :双方都进入
ESTABLISHED(已建立连接)状态,可以开始传输数据。
第三次握手时,客户端在发出这个 ACK 报文后,就会进入 ESTABLISHED 状态,认为连接已经可以使用(仅对客户端而言,服务端要收到 ACK 后进入 ESTABLISHED 状态才行),此时客户端可以顺带带上数据进行发送,这叫捎带应答
🤔为什么不是一次握手,两次握手呢?
一次握手的问题在于: 发起方(客户端)只发送 SYN 报文,接收方(服务端)无法确认客户端能否收到自己的响应,客户端也无法确认服务端是否收到了自己的请求,双方连基本的连通性验证都无法完成,更无法同步序列号,后续数据传输的可靠性无从谈起。甚者还会有引起 SYN 洪水的可能性,就是不断有客户端发送连接,但是不发送数据,那么真正想发数据的就没位置发了
两次握手的缺陷:
缺陷一:无法防止"失效的历史连接"
这是最主要的原因。网络中经常会有延迟。
- 场景:
- 客户端发送了第一个连接请求
SYN A,但由于网络拥堵,这个包在某个节点滞留了,并没有丢失,也没有按时到达。 - 客户端超时重传,发送了新的连接请求
SYN B。 - 服务端收到了
SYN B,建立了连接,传输数据,然后关闭了连接。 - 关键点来了: 此时,那个滞留许久的旧请求
SYN A终于到达了服务端。
- 如果是两次握手: 服务端收到
SYN A,会误以为客户端又要发起一个新连接,于是立即回复确认,并开启资源等待客户端发数据。但客户端并没有发起新连接,它会忽略服务端的确认。结果就是服务端一直傻等,浪费了资源(CPU、内存)。 - 如果是三次握手: 服务端收到
SYN A后回复SYN+ACK。客户端收到后,发现这不是自己当前想要的连接(或者自己处于Closed状态),就会发送一个RST(复位报文)告诉服务端:"我没有请求连接,请关闭",从而避免了错误的连接建立。
简单来说两次握手会把错误链接的代价嫁接到服务端,这当然是不行的,服务端连接着成百上千的客户端,会导致很大的损失,而三次握手会把错误链接的代价嫁接到客户端,只会影响一方,代价较小
缺陷二:无法确认双方的接收能力
TCP 是全双工通信,双方都能收发数据。
- 握手过程解析:
- 第一次(C -> S): 服务端确认了"客户端能发"且"服务端能收"。
- 第二次(S -> C): 客户端确认了"服务端能收"且"服务端能发",同时也确认了"客户端自己能收"。
- 如果是两次握手,到这里就结束了: 此时服务端并不知道"客户端能收"以及"服务端发出的数据对方是否能收到"。服务端发出的序列号没有得到客户端的确认。
简单来说,三次握手的主要目的有两个:
-
同步双方的初始序列号
-
防止历史重复连接的初始化造成混乱(防止"已失效的连接请求"被服务端误认为是新的)
补充: listen() 的第二个参数 backlog,表示全连接队列的长度 ,表示服务端套接字所能容纳的未被 accept() 处理的连接请求的最大数量(实际值为 backlog + 1),这里要区分好全连接队列和半连接队列
-
半连接队列: 存放未完成三次握手的连接。客户端发送
SYN后,服务端回复SYN+ACK,此时连接处于SYN_RCVD状态,进入半连接队列。半连接队列并不会长时间被维护 -
全连接队列: 存放已完成三次握手、但还未被服务端
accept()函数取走的连接。客户端发送ACK完成三次握手后,连接从半连接队列转移到全连接队列,状态变为ESTABLISHED
backlog 设置要合理,如果太小,并发连接请求过多时,新的连接请求会被拒绝;如果太大,本来系统处理资源就很忙了,还要再空出位置给新的全连接预留资源,那这不是雪上加霜了吗
2.2.2 四次挥手
第一步:客户端发起关闭请求(FIN + ACK)
- 动作 :客户端主动发起关闭连接请求,此时客户端已无数据要发送,发送最后一个数据的序列号为
seq = u。 - 标志位变化 :
FIN = 1:表示客户端没有数据要发送了,请求关闭从客户端到服务端的传输通道。ACK = 1:确认之前收到的服务端数据,确认号ack = v(v是服务端最后发送的数据序列号 + 1)。
- 含义 :"我已经没有数据要发给你了,我要关闭向你发送数据的通道,你之前发给我的数据我都收到了,下次请发
v及以后的数据(如果还有的话)。" - 状态 :客户端进入
FIN_WAIT_1状态,等待服务端的确认。
第二步:服务端确认客户端的关闭请求(ACK)
- 动作 :服务端收到客户端的
FIN报文后,立即回复确认报文,此时服务端的发送序列号为seq = v。 - 标志位变化 :
ACK = 1:确认收到客户端的关闭请求。- 确认号
ack = u + 1:表示"我已收到你发送的FIN报文,对应的序列号是u,下次请发u + 1(但你已经没有数据了)"。
- 含义:"收到你要关闭发送通道的请求了,我知道了。我这边可能还有数据没发完,你先等一等。"
- 状态 :服务端进入
CLOSE_WAIT状态(等待服务端应用层处理完剩余数据);客户端收到确认后,进入FIN_WAIT_2状态,等待服务端的关闭请求。
第三步:服务端发起关闭请求(FIN + ACK)
- 动作 :服务端处理完所有剩余数据后,向客户端发送关闭请求报文,此时服务端的发送序列号为
seq = w(w是服务端最后发送的数据序列号,可能等于v)。 - 标志位变化 :
FIN = 1:表示服务端也没有数据要发送了,请求关闭从服务端到客户端的传输通道。ACK = 1:保持确认状态,确认号ack = u + 1(与第二步的确认号一致)。
- 含义:"我这边的数据也发完了,现在我要关闭向你发送数据的通道,你之前的请求我早就确认过了。"
- 状态 :服务端进入
LAST_ACK状态,等待客户端的最终确认。
第四步:客户端确认服务端的关闭请求(ACK)
- 动作 :客户端收到服务端的
FIN报文后,回复最终确认报文,此时客户端的发送序列号为seq = u + 1。 - 标志位变化 :
ACK = 1:确认收到服务端的关闭请求。- 确认号
ack = w + 1:表示"我已收到你发送的FIN报文,对应的序列号是w,下次请发w + 1(但你已经没有数据了)"。
- 含义:"收到你要关闭发送通道的请求了,我们的连接可以彻底关闭了。"
- 状态 :客户端发送确认后,进入
TIME_WAIT状态(等待2MSL时间,确保服务端收到确认报文,避免因报文丢失导致服务端重发FIN);服务端收到确认后,立即进入CLOSED状态;客户端等待2MSL时间后,也进入CLOSED状态,连接彻底关闭。
🤔为什么有时候无法快速重复绑定同一个端口?
这需要我们理解 TIME_WAIT 状态,客户端需要返回最后的 ACK,该状态大约持续 2MSL(MSL 是报文段最大生存时间),确保客户端发送的最后一个 ACK 报文能被服务端收到。如果该 ACK 丢失,服务端会重发 FIN 报文,客户端在 2MSL 时间内还能收到并重发 ACK。等待足够长的时间,让网络中所有与该连接相关的旧报文段都自然过期,避免这些旧报文段被后续使用相同端口的新连接错误接收。所以无法快速重复绑定是因为确保下一次连接不会被上一次影响
在服务端的 TCP 连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的,我们可以通过设置系统内的 MSL 缩短 TIME_WAIT 等待时间,或者使用 setsockopt() ,选项 SO_REUSEADDR 为 1 ,表示允许创建端口号相同,但 IP 地址可以有不同的多个 socket 描述符
3. tcp特性
3.1 可靠性
3.1.1 超时重传

发送方在发送数据报文段后启动计时器,由于网络问题,若在计时器超时前未收到接收方的 ACK,则判定报文段丢失,并重新发送该数据
Linux 中,超时以 500ms 为⼀个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍,如果多次重传无果就会关闭连接
🤔要是之前由于网络阻塞,重发后未发送到达的报文又到达了呢,岂不是会重复?
其实是不会的,因为 tcp 协议是有去重机制的,因为基于序列号的唯一性校验,接收方通过对比 "已确认序列号" 和报文段序列号,判断是否为重复数据;重复报文段会被直接丢弃,但仍会回复 ACK 避免发送方超时重传
3.1.2 流量控制

接收端的接受能力是有限的,如果不控制发送速度,接收端的缓冲区会满,然后造成丢包,进一步造成重传等的连锁反应
所以无论是客户端还是服务端,每次返回 ACK 时都会把接收端的可接收的窗口大小返回,窗口大小越大,说明网络吞吐量越大
如果接收缓冲区满了,就会将窗口置为 0,但是发送端会定期发送一个窗口探测报文,用于获取当前接收端的窗口大小
3.1.3 拥塞控制

当传输过程中出现少量丢包,这是很正常的情况,可能是网络抖动导致的,只要进行重发即可;当传输过程中出现大量丢包,这就不正常了,可能是网络状态很差,也有可能是硬件问题或者数据包太大,此时就不能使用重发的策略了,本来网络问题就严重,你还继续重发,岂不是加重网络状态吗

所以引入了一个叫拥塞窗口的概念,刚开始这个窗口的值为 1,此后以指数增长,直到达到一个慢启动阈值,这个慢启动阈值一般为上一次网络传输拥塞窗口的一半,达到阈值以后就线性增长,最后如果再次网络拥堵就会超时重传,窗口再置为 1
简单来说,接收方要考虑流量控制(即接收缓冲区的窗口大小),发送方要考虑拥塞窗口(即发送的网络状况),通常滑动窗口的大小为这两者取小
3.2 提高性能
3.2.1 快速重传

不等待计时器计时,当接收方发现发送方的包丢了的时候,就会连发三条 ACK 告诉发送方,你的包丢了,发送方就会立刻判定丢包并发起重传,保证可靠性的同时,也不用过度依赖超时重传机制的计时器等待,提高了效率,所以说超时重传是快传的保底机制
3.2.2 滑动窗口

为了提高效率,每次会一次性发送多个报文,把多个报文一次性拷贝到缓冲区里,以此减少上层和缓冲区之间的拷贝 IO 次数,既然需要一次性发多个,那么就需要划分好已发送和未发送的区域

滑动窗口大小指的是无需等待确认应答就可以继续发送数据的最大值,缓冲区大致可分为已发送且已确认 ,已发送但未确认(即滑动窗口的有效数据) ,未发送但可发送 ,未发送且不可发送四个区域,发送完一部分,左边界就会右移,右边界根据网络状态以及接收方缓冲区窗口大小决定右移多少
🤔那如果出现了丢包情况呢?
- 如果只是中间有
ACK丢失,那没有关系,只要有最后报文的ACK收到就行,因为你收到了最后一个报文的ACK就说明前面的数据都有正常收到 - 如果中间有数据丢失,那么就进行快速重传
3.2.3 延迟应答

接收方一般收到报文不立刻回复 ACK,而是等待一小段时间(通常 200ms)或积累一定数量的报文后再回复,这样可以让返回的窗口更大,从而提升网络吞吐量。
🤔为什么需要延迟确认?
我们用一个例子来理解:
- 假设接收端缓冲区总大小为
1M。 - 如果接收方刚收到
500K数据就立刻回复ACK,此时返回的窗口大小就是500K(表示还剩500K缓冲区空间)。 - 但实际上,接收方的应用层可能在
10ms内就把这500K数据消费掉了,缓冲区又空出了500K。 - 如果稍等
200ms再回复ACK,此时缓冲区已经完全空闲,返回的窗口大小就是 1M。
窗口越大,发送方一次能发送的数据就越多,网络吞吐量和传输效率也就越高。我们的目标是在不引发拥塞的前提下,尽可能提升传输效率。
延迟确认不能无限制等待,否则会导致发送方超时重传。因此,TCP 协议为延迟确认设置了两个硬性限制:
- 数量限制 :每收到
N个报文就必须应答一次(通常N=2)。 - 时间限制 :超过最大延迟时间(通常
200ms)就必须应答一次。
3.2.4 捎带应答

捎带应答就是 "借车搭便车",利用双向传输的机会,把确认信息附在要发送的数据报文里,避免单独发 ACK 浪费带宽
3.3 粘包问题
对于传输层来说,是一个个按照序号排好队的报文,而对于应用层来说,就是一串字节流,上层不知道怎么处理这一串字节流,因此明确报文和报文之间的边界很重要,这也是Encode、Decode 的原因
一般使用定长报文 、特殊字符 、自描述字段+定长报头(常用) 、自描述字段+特殊字符这几种方法,通过每个发送报文的长度以及特殊边界划定,就能准确发送每次的信息,尽管可能报文太大,需要拆开多次发送,也没关系
举个例子:
发送端一次 write() 11 字节的 hello world(假设很大) → TCP 拆成两次发送 → 接收端 TCP 按序号重组为 11 字节的连续流 → 应用层按约定的边界规则读取 11 字节 → 展示完整的 hello world
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!
