一、UDP协议:
1. 协议格式:源端端口号,对端端口号,数据报长度,校验和,数据
字段的功能:
源端端口+对端端口:描述了通信两端的进程
数据报长度:描述包含UDP头部在内的报长度
长度:2个字节,范围:0~65535
因此,UDP报文最大长度64K;数据报最大长度64k-8
校验和:用于校验收到的数据和发送的数据是否一致
算法:二进制反码求和算法
1.填充报文数据,将校验和置0
2.针对报文所有数据逐字节取反相加求和
3.得到的值填充到校验和中
接收方收到数据之后,逐字节反码求和,得到的结果是0,则表示数据一致
2.协议特性:无连接,不可靠,面向数据报
无连接:通信双方不需要管理对方的状态,只需要知道对方的地址就可以发送数据
连接:本质是通信双方对于对方的状态维护
不可靠:不保证数据安全,有序的到达对方
面向数据报:有最大长度限制的传输方式(64k-8),数据传输是一种块式传输
比如:当发送方要发送数据的时候,此时发送方在应用层要发送一个Hello,一旦这个Hello到达传输层,传输层就会在这个数据前面追加一个UDP报头,在这个报头中,此时数据报长度保存的值就是13,因为头部字段是8个字节,Hello是5个字节。这个数据经过网络发送给了对方,对方接收到数据之后,知道UDP报头是8个字节,所以先取出8个字节的报头,从这个报头中看数据报长度13,知道报头的长度是8个字节,所以数据长度就是5,接下来就在缓冲区中直接取出5个字节长度的数据。
数据报传输的时候,报文中明确标识了报文长度,接收方接收数据的时候,不允许出现半个,多个,一次性取出的情况
3.UDP编程注意事项:
1.UDP不保证数据安全有序的到达对端,因此若需要的话,则需要程序员在应用层进行安全管理,包序管理
2.UDP报文有最大长度限制,因此发送数据的时候若数据过大,则需程序员在应用层进行分包,将大数据分割成多个小段进行发送
3.UDP无连接,因此支持广播传输:通过一次sendto发送数据,可以将数据发送给当前局域网网络中所有主机
a. UDP 无连接
TCP 是面向连接:通信前必须先三次握手,建立点对点的专属通道,只能你和对方两个主机通信,不能一次性发给一堆主机。 UDP 不用握手,发数据前不需要和接收方建立连接。发包的时候,直接在数据包里填好目的地址就往外扔,发完就不管了。
无连接这个特性,是 UDP 能实现广播的前提。
b. 广播是什么
广播:向同一个局域网内的所有主机发送一份数据。 局域网有一个广播地址,把 UDP 数据包的目的 IP 设为广播地址,交换机就会把这个包转发给本网段里每一台主机。
c. sendto 函数
UDP 编程用sendto()发送数据(而不是 TCP 的send/write)。sendto的特点:每次发送都要指定本次的目标地址(IP + 端口)。
- 普通单播:目标 IP 填某一台单独主机 IP → 只发给这一台
- 广播:目标 IP 填局域网广播地址→ 只调用一次
sendto,生成单个 UDP 数据包,网络会自动把这个包投递到局域网全部主机。
二、TCP协议:
1.协议格式:16位源端端口,16位对端端口,32位序号,32位确认序号,4位头长,6位保留,6位标志位,16位窗口大小,16位校验和,16位紧急指针,选项数据(0~40字节),数据
2. 字段功能:
(1)16位源端端口+对端端口:描述通信两端进程
(2).32位序号:实现有序传输的核心字段(描述了数据的序号)描述了我当前进程的数据,从哪个序号开始到哪个序号结束,接收方接收到数据之后,根据这个序号来进行排序
(3)32位的确认序号:实现有序传输的核心字段
(4)4位头部长度:以4字节为单位,描述整个TCP头部长度
TCP头部长度最小20字节,最大是60字节(因为正常头部是占20个字节,如果有选项数据40个字节,就是60个字节)
TCP报文解析核心字段
(5)6位保留字段:暂时没有用到
(6)6位标志位:
URG:紧急指针有效位->用于表示后边的紧急指针数据是否有效
紧急指针:本质是一个偏移量,指向了一个带外(紧急)数据位置
也就是说TCP里面在我正常传输数据的时候,此时,我突然有一个优先级更高的数据,需要优先传输给对端,TCP是支持这个操作的,这个紧急指针就是描述紧急数据的结束位置,从我们的报头开始到紧急数据结束这之间就是我们一个紧急的优先级数据。
ACK:确认应答标志->标识当前报文是一个确认应答报文:在建立连接的时候置位
PSH:立即传输标志位->TCP默认有延迟发送机制,该标志位用于标识立即发送,不要延迟
TCP中有一个机制,叫做延迟发送机制,就是我现在调用一个send接口向对端发送数据,这个数据在发送的时候实际上是把数据放到发送缓冲区里面,操作系统选择合适的时机将数据发送到对端主机上,也就是说,数据发送到缓冲区之后,并不会被立即进行发送,而是有一个延迟时间,延迟发送
为什么要延迟发送:
IO缓冲区,当我们向文件中写入数据的时候,数据并不会被立即写入文件,而是先放到缓冲区里面,因为每一次写文件都意味着你要操作一次磁盘,从内存跟硬盘之间进行一个数据交互,网卡中也是同样的道理,我们每次发送数据都会涉及到内存跟网卡之间的一个数据交互,那么我们的数据每一次数据通过send就直接通过网卡发送出去,那么就意味着,如果在大量的这种小数据发送的时候,就显得我们需要很多次IO操作才能完成我们的数据发送,但是IO操作的效率是比较低的,他涉及到两个设备之间的一个数据传输,所以 大佬们就在磁盘上用了一个缓冲区,先把数据给他堆积起来,堆积成一个大数据,再一次性写入磁盘里面,网卡也是同样的道理
延迟发送机制,是为了减少IO次数来提高IO效率
RST:重置连接标志位->通知对方当前连接有异常,若要通信则重新建立连接
SYN:建立连接标志位->标识当前报文是一个连接建立请求报文
FIN:连接断开标志位->标识当前报文是一个连接断开报文(用于通知对方我要连接断开了)
用于通知对方我不再给你发送数据了
FIN = Finish,连接断开标志。当报文头里FIN=1,代表:发送方这边,不会再往这个连接里发新数据了,通知对方准备关闭连接。
重点:FIN 只是说「我不再发数据」,不等于我立刻不收你的数据。
(7)16位窗口大小:用于TCP的滑动窗口机制,实现流量控制(避免发送方发送数据过快,而来不及接收,导致数据丢包的情况)
因为缓冲区的大小固定,socket的接收缓冲区和发送缓冲区,默认情况下是64K,那么如果我们的发送方发送数据过快,但是接收方接收数据之后要处理它就接收的特别慢,那么发送的多了对方的接收缓冲区就容易被塞满,他的接收缓冲区一旦被塞满,那么后续的数据就没有地方可以放了,就出现了数据丢失
(8)16位校验和:二进制反码求和算法实现校验接收到的数据与发送数据是否一致
(9)16位紧急指针:存储一个偏移量,指向紧急数据的末尾
(10)0~40字节选项数据:通常用于连接建立时的一些协商数据........
3.协议特性:面向连接,可靠传输,面向字节流传输
(1)面向连接:
通信双方需要维护对方的状态以便于保证数据的传输是有效的,有意义的
连接管理:三次握手建立连接,四次挥手断开连接
三次握手建立连接过程:
四次挥手断开连接过程:
典型面试题:
(1)三次握手的过程:
客户端向服务端发送SYN请求
服务端回复ACK+SYN
客户端回复ACK
(2) 握手为什么是三次,为什么不是两次或四次:
两次是不够安全的,当前的SYN有可能是正常的,也有可能是延迟到达的无效报文
四次是没有的必要的,因为每个报文都要传输报头占据带宽
(3)挥手为什么是四次,而不是三次
FIN包主要是用于表示,我不给对方发送数据了(并不代表我不接收数据)
但是不表示对方不再给我发送数据,因此对方依然可以继续发送数据给我
因此从设计上将两个操作分开了(题外话:只不过很多时候如果对方也关闭的比较快,两个可能会合并到一起发送)
连接中的状态迁移变化:
(1)客户端在向服务端发送SYN连接请求会进入一个状态叫做:SYN_SEND表示此时已经给对方发送了连接建立请求,等待对方确认回复
(2)(服务端在接收SYN之前的状态是LISTEN,才会开始处理连接请求)
(3)服务端收到这个请求并且确认回复之后会进入一个状态:SYN_RCVD 这个状态就是 等待对方给我最后一次确认回复
(4)客户端一旦收到了我们服务端的ACK和SYN之后,他就会进入一个状态:ESTABLISHED 也就是就绪状态
(5)服务端收到最后一次ACK,他也会进入一个状态:ESTABLISHED,表示就绪 就绪状态才允许进行数据通信
也就是说两方的状态进入就绪状态才能进行数据通信
(4)三次握手中,超时了双方是如何处理的
①客户端发送SYN后,等待超时:客户端会重新传SYN请求(次数限制)
②服务端发送ACK+SYN之后,等待超时(客户端不对服务端进行确认会怎么样):
服务端会给客户端回复一个RST报文,然后释放socket
服务端收到 SYN 后,会把这个连接信息放到SYN 半连接队列,就是存这些处于SYN_RCVD状态的半连接。 如果大量客户端只发 SYN,不回复最后的 ACK,就会塞满半连接队列 →SYN 洪水攻击(SYN Flood),服务端无法处理正常新连接。此时这个问题服务端给客户端回复RST解决不了
客户端收到 SYN+ACK,并且发出了 ACK,但是 ACK 报文在网络丢包: 服务端看不到 ACK,依旧走重传逻辑;但客户端自己已经进入 ESTABLISHED,客户端认为连接好了,服务端还在 SYN_RCVD。后续客户端发数据的时候,服务端会回复 RST 重置连接。
断开中状态迁移变化:服务端和客户端都可以进行断开连接
主动关闭方给服务端主发送一个FIN包,然后此时会进入一个状态:FIN_WAIT_1状态-》此时意味着我给对方发送了一个FIN包,等待对方确认回复
对方收到FIN包之后会给对方确认回复,确认回复之后,就会进入一个状态:CLOSE_WAIT状态-》等待我们的上层关闭连接
主动关闭方收到ACK之后 会进入一个状态:FIN_WAIT_2状态-》表示我收到了ACK,并且等待对方给我发送FIN包的状态
对方调用close,发送FIN之后会进入一个状态:LAST_ACK状态-》等待对方最后一次回复
主动关闭方收到FIN包之后,给对方进行一个ACK的确认,确认之后会进入一个状态:TIME_WAIT状态 他会等待2个MSL时间然后进入一个状态叫做CLOSED状态
对方收到ACK之后会进入一个状态:CLOSE状态
(5)TIME_WAIT状态有什么用:
主动关闭方,收到了对方的FIN包,并进行回复之后进入的状态
等待两个MSL时间之后,进入CLOSED状态,释放连接
MSL:报文最大生命周期
因为主动关闭方,最后一次ACK有可能会丢失,对方收不到ACK,超时后就会重传FIN包,一旦我们的主动关闭方,直接进入CLOSED释放连接,就有可能有新的连接使用了相同的地址,收到这个重传的FIN包,就对新连接造成了影响
因此为了避免产生这种情况,因此主动关闭方等待两个MSL之后再进入CLOSED,释放连接
两个MSL的原因:一个MSL是等待ACK用一个最大生命周期的时间到达对端的时间(但是ACK可能没有到达对端,丢失了),接下来又用一个MSL重新传输FIN
(6)一台主机上,出现了大量的TIME_WAIT连接,是什么原因?怎么解决?
TIME_WAIT是主动方才会出现的状态
主机上出现了大量的TIME_WAIT 意味着主机上频繁的且大量的在进行主动的连接关闭操作
这种情况常见于主机上,运行了爬虫程序
解决方案:
(1)使用setsockop接口进行套接字选项设置:开启地址重用(复用处于TIME_OUT状态连接地址)
(2)进行系统配置设置,将TIME_WAIT等待时间调短
(7)一台主机上出现了大量的CLOSE_WAIT是什么原因
被动关闭方,收到FIN包并进行ACK回复后进入的状态,等待上层关闭连接
若产生大量的CLOSE_WAIT,有可能是代码中漏掉了CLOSE关闭连接操作,连接断开了但是没有CLOSE
FIN请求,一定是一段调用了close(fd)或者调用了shutdown(fd,write)才会向对方发送FIN
对端(客户端)已经发起关闭、内核收到 FIN 并 ack,客户端到服务端这个方向的数据流已经断了(半断开),但服务端应用层没有执行 close () 系统调用,所以完整连接无法释放,卡在 CLOSE_WAIT
半关闭本身不是 bug;一直卡在 CLOSE_WAIT 不释放 socket,才是 bug。
我们拆开讲:
- ✅ TCP 半关闭特性:客户端发 FIN,代表客户端不再发送数据,但服务端依然可以继续往客户端发数据。这个机制本身完全正常,是 TCP 设计的能力。
- ✅ 什么时候正常:服务端业务处理完剩余数据之后,调用
close(),内核发出服务端的 FIN,走完四次挥手,连接释放。此时 CLOSE_WAIT 只是一个短暂过渡状态,一闪而过,你几乎看不到。 - ❌ 什么时候变成 BUG: 服务端收到客户端 FIN,内核 ACK,进入 CLOSE_WAIT。但是服务端应用程序一直不去调用 close ()。
哪怕服务端已经没有数据要发给客户端了,代码也没有 close。 这个 socket 就永久卡在 CLOSE_WAIT,不会消失。
连接断开在程序中的表现:
recv取完数据后,返回0,而不再阻塞
send触发异常SIG_PIPE,让程序直接退出(避免退出,忽略或自定义SIGPIPE信号的处理方式)
TCP的连接保活机制:
服务端与客户端若长时间(7200s)没有数据往来,则会每隔一段时间(75s)会给客户端发送一个保洁探测数据包,要求客户端进行确认回复,若连续多次(9)都没有收到回复,则认为连接断开,释放资源
这些默认数据,可以通过setsockopt套接字选项设置,也可以改变系统配置
2.可靠传输:保证数据安全,有序的到达对端
核心:发送方和接收方如何发现出现了丢包;丢包后该如何处理;如何尽量避免丢包
1.发送方和接收方如何发现出现了丢包
(1)确认应答机制:
发送方针对发送的每一个字节数据,都要求接收方收到之后进行确认回复,发送数据后,若长时间没有收到回复,则认为丢包
停等协议:
发送方收到确认回复之后才会发送下一条数据,适用于网络极差的场景
原理
停等是最简单的可靠传输协议,属于 ARQ(自动重传请求)。
- 发送方发送 1 个分组,然后停止发送,停下来等待接收方的 ACK 确认帧。
- 接收方收到正确分组 → 返回 ACK 确认。
- 发送方收到 ACK 之后,才可以继续发下一个分组。
- 如果超时没收到 ACK,发送方认为分组丢了,重传当前这个分组。
快速重传协议:
在管线化传输中,接收方收到了后边的数据,但是没有收到前边的数据,则会对前边的数据进行应答,发送方,若连续三次,都收到同一个确认序号的确认报文,则会立即从指定的确认序号开始重传数据(不再等超时)
属于 TCP 的差错恢复机制,不是独立协议,是对超时重传的优化。
核心思想
不等计时器超时,提前重传丢失的报文段。 超时重传要等漫长的超时时间,很慢;快速重传靠重复 ACK来提前感知丢包。
原理规则
- 接收方收到失序报文(序号不是期望收到的那个),就立即发送重复 ACK(ACK 仍然确认期望收到的序号)。
- 发送方如果连续收到 3 个重复 ACK(一共 4 个相同 ACK),判定这个报文段丢包,立刻重传该报文,不用等超时定时器到期。
重点:3 个冗余 ACK(duplicate ACK)触发快速重传。
举个例子: 发送方发送 1、2、3、4 号报文
- 报文 2 丢了;报文 3、4 到达接收方
- 接收方收到 3:不是想要的 2 → 回复 ACK=2(重复 ACK#1)
- 接收方收到 4:不是想要的 2 → 回复 ACK=2(重复 ACK#2) 发送方收到第 3 个 ACK=2(总共 3 个重复 ACK)→直接重传报文 2,不等超时。
回退n步协议:
从丢掉的报文开始往后的数据都重传,适用于网络一般的场景
属于流水线 ARQ 自动重传请求,在停等协议基础上改进,不用发一个等一个,可以连续发送多个分组。
核心思想
发送方维持发送窗口,可以一次性连续发送窗口内的多个报文,不用每发一个就停下等 ACK。 接收方只按序接收:
- 收到期望序号的报文 → 上交上层,返回 ACK
- 收到失序报文,直接丢弃,不缓存;持续发送重复 ACK
一旦发现某个报文丢失,从丢失的那个报文开始,后面所有已经发出去的报文全部重传(回退 N 个,这就是名字来源)。
选择重传协议:
针对丢掉的单个报文进行重传,哪个包丢了,就重传哪个包,适用于网络较好的场景
超时重传机制:
发送方等待超时,发现丢包之后,就会对原来的数据进行重传
这两个机制,依赖于协议中的序号和确认序号字段
延迟应答机制:
接收方收到数据之后,并不会立即给予确认回复,可能会进行一个延迟,不用每发接收一条数据之后进行一次ACK,延迟回复,会从我们接收到的最后一个数据进行ACK确认回复,节省一部分的确认回复
捎带应答机制:
接收方收到数据以后,如果刚好当前要给对方发送数据,会把确认回复和要发送的数据合并到一起进行发送(确认回复,协议中的确认序号)节省确认回复所导致的空报文
延时发送数据:
发送方send将数据放到发送缓冲区之后,并不会立即发送数据,延迟一点时间,就可能会将大量的小数据攒成大数据,通过依次IO实现传输
2.避免没必要的丢包:
(1)滑动窗口机制:
通过协议中的窗口大小字段,告诉接收方最多给自己发送多少数据,实现流量控制,避免发送方发送数据过多,导致接收方缓冲区溢出,导致丢包
MTU:最大传输单元,链路层根据物理硬件,限制的最大数据帧大小(包含报头在内)
若一个报文他特别大(UDP=64k-8),但是受限于链路层MTU限制,在网络层就会被分片割成多个小的数据帧进行传输。TCP在传输的时候,三次握手阶段就会协商MSS(最大数据段大小)
MSS=MTU-IP报头大小-TCP报头大小=1460(以太网限制)
双方会在TCP选项中,携带自己所支持的MSS大小,选择其中较小的值作为实际的MSS传输大小,往后的数据发送的时候都会以该大小进行发送。
在进行数据传输的过程中,接收方回复ACK之后才会将刚刚发送的数据从发送缓冲区中删除出来
接收窗口的滑动:
接收窗口是接收方用来告诉发送方:我现在最多还能接收多少字节的数据。 窗口有两个边界:后沿(左边界)、前沿(右边界)
窗口大小=前沿-后沿,取决于对方数据中的窗口大小字段,接收方通知给发送方的数据,必须小于等于接收方缓冲区剩余空间的大小,发送方通过窗口控制自己接收数据的位置
滑动:窗口的移动,主要用于描述发送方发送数据的序号:描述接收方接受的数据序号
窗口大小由接收方返回的报文段里的窗口大小字段决定,不是发送方定的。
- 后沿:已经确认完毕的最后一个字节序号,窗口的左边界
- 前沿:后沿 + 接收窗口大小,窗口的右边界
在【后沿,前沿)这个区间内的序号,是接收方允许发送方继续发送的数据。
后沿的移动:当接收到了后沿序号的确认应答
后沿:收到后沿序号的确认应答 ACK,才会向右移动。
- 发送方发数据 → 接收方收到完整数据 → 返回 ACK 确认。
- 当 ACK 确认号等于当前窗口后沿序号,后沿向右滑动。
- ✅一句话:后沿跟着 ACK 确认号走,确认了旧数据,后沿才往前挪。
前沿的移动:响应的窗口大小-后沿
前沿 = 后沿 + 响应报文里的窗口大小 接收方每次回复 ACK 的时候,会带上自己当前剩余接收缓冲区大小(窗口值)。
- 如果接收方缓冲区空闲变多 → 窗口变大 → 前沿右移
- 如果接收方缓冲区快满了 → 窗口变小 → 前沿左移(窗口收缩)
前沿由接收方每次通告的窗口大小决定。
发送窗口的大小,一定等于相应数据中的窗口大小字段
WIN<=接收缓冲区剩余空闲空间大小
WIN=前沿-后沿
(2)拥塞机制:
发送方本身在本地通过维护一个拥塞窗口(变量),用于控制当前所能发送的数据量,用于控制发送方发送数据量,避免因为网络状态不好,出现丢包,发送方又不断大量重传,导致网络更加拥塞的情况
核心:
拥塞窗口,描述了当前发送方可以发送多少数据
拥塞窗口的增长节奏:慢启动,快增长(指数)
3、字节流传输:
以字节为单位,进行先进先出的传输方式
(1)TCP的粘包问题:
场景:
发送方可能会将多次send的数据当做一条报文进行发送
接收方可能会将多条数据,当做依次recv交付给应用层
原因:
TCP并不会对数据进行缓冲区进行边界管理
解决方案:
TCP没有在传输层进行数据边界管理,程序员,可以在应用层进行边界管理
1.使用特殊字符数据添加结尾标志:
容易与数据中的特殊字符产生二义性->对数据中的特殊字符进行转义处理
2.使用固定长度传输数据:
容易造成资源浪费,且必须明确最大数据长度
3.自制LV通信协议实现边界标记:
L:length
V:value
使用固定的长度的长度字段描述长度字段,接收方先接收指定长度的长度字段,根据长度字段的值,获取数据
struct lv{ unit64_t length; char data[]; };若recv接收数据的时候,缓冲区没有数据则会阻塞
若send发送数据的时候,发送缓冲区中数据满了,则会阻塞
当连接断开的时候(或者对端的写操作关闭),recv接收完缓冲区数据后,继续recv 则不在阻塞,返回0
当连接断开的时候(或对端关闭了读操作),send发送数据就会触发异常(SIGPIPE)