前言
Socket 是网络编程的基础,面试中 TCP 和 UDP Socket 的调用流程、内核行为、函数区别、缓冲区机制属于高频考点。很多同学容易混淆 accept 返回的 fd 作用、三次握手发生时机、UDP sendto/recvfrom 时序、内核缓冲区行为等细节。本文结合调用流程图,逐步骤拆解 TCP、UDP Socket 流程,梳理易错点和对比总结。
一、TCP Socket(面向连接、可靠字节流)
TCP 是面向连接的通信模型,通信前需要完成三次握手建立连接,通信结束通过四次挥手断开连接,数据是流式、无边界的,内核提供确认、重传、排序、流量控制机制。
1. 服务端调用流程
socket() → bind() → listen() → accept() 阻塞等待连接 → read()/write() 收发数据 → close()
-
socket():创建套接字文件描述符。此时仅仅创建一个通信"外壳",没有绑定地址,也不具备监听能力。
-
bind():绑定固定 IP + 端口。客户端依靠这个地址找到服务端,是服务端标配;客户端一般不需要手动 bind。
-
listen() :将 socket 转为监听 socket,内核创建连接等待队列,开始准备接收客户端连接请求。
-
accept() :阻塞等待客户端连接。三次握手由内核协议栈自动完成 ,不是 accept 函数本身组装报文。
握手成功之后,accept返回一个全新的通信 fd :- 监听 fd(listen fd):只负责接收新连接请求,不传输业务数据,持续监听新客户端;
- accept 返回的通信 fd:专门和这一个客户端做业务数据读写。
⚠️ 重点:accept 返回的 fd 只留在服务端本地使用,不会发送给客户端!
-
read() / write():使用新的通信 fd 双向读写业务数据,TCP 是全双工,两端随时可以收发。
-
close() :关闭 socket,触发 TCP 四次挥手断开连接;对端调用
read会返回 0,代表读到 EOF。
2. 客户端调用流程
socket() → connect() → write()/read() → close()
- socket():创建客户端套接字 fd。客户端全程只有这一个 fd,后续 connect、读写都复用它。
- connect():主动发起连接,发送 SYN 报文,触发三次握手第一步;内核完成剩余握手流程,连接就绪后 connect 返回成功。
- write() / read():和服务端双向传输流数据。
- close():关闭连接,向对端发送 EOF 标记。
3. 通俗类比
监听 fd = 公司前台,一直留守接待访客;
每一个客户端连接进来,前台(listen fd)分配一个业务员(accept 返回的通信 fd)一对一对接;
业务员属于服务端内部,不会交给客户端;客户端手里只有自己自带的通讯工具(客户端 socket fd)。
4. TCP 核心底层标识:四元组
一条 TCP 连接由四元组唯一标识:
客户端IP:客户端端口 <--> 服务端IP:服务端端口
内核使用通信 fd 和这条四元组关联,两端内核协议栈共同维护同一条连接状态。
5. TCP 关键考点汇总
- listen fd 和 accept 返回的通信 fd 不是同一个,职责分离;
- 三次握手发生在
connect发起之后、accept返回之前,在内核完成; - connect 发 SYN(第一次握手),握手全部完成后 accept 才解除阻塞返回;
- EOF:一方 close,内核向对端缓冲区放置 EOF,对端 read 返回值为 0;
- TCP 是字节流,存在粘包问题。
二、UDP Socket(无连接、面向数据报)
UDP 是无连接、面向数据报的协议,没有握手、挥手流程,不维护连接状态,不保证可靠交付,每个报文都携带完整目标地址,典型应用:DNS、TFTP。
UDP 没有 listen、accept、connect(普通数据报场景),收发函数固定为 sendto / recvfrom。
1. UDP 服务端流程
socket(SOCK_DGRAM) → bind() → recvfrom() → sendto()
- socket() :创建 UDP 套接字,类型为
SOCK_DGRAM; - bind():绑定固定 IP+端口,客户端知道发包目标地址,UDP 服务端一般必须 bind;
- recvfrom():阻塞等待接收报文,同时通过出参拿到发送方的 IP+端口地址;
- sendto():回复数据,必须指定目标地址(就是 recvfrom 拿到的客户端地址)。
关键点:同一个 UDP socket,可以同时接收成千上万个不同客户端的报文,不需要新建 fd。
2. UDP 客户端流程
socket(SOCK_DGRAM) → [可选 bind()] → sendto() → recvfrom()
- socket():创建 UDP 套接字;
- bind():可选!绝大多数 UDP 客户端不手动 bind;调用 sendto 时内核自动分配随机端口;只有需要固定客户端端口时才手动 bind;
- sendto():发送数据,参数携带服务端 IP+端口;
- recvfrom():接收服务端应答,获取对端地址。
3. UDP 收发时序(高频坑点)
UDP 不存在连接,sendto 和 recvfrom 没有内核强制的先后顺序,分为两种业务场景:
- 一问一答(主流,DNS) :客户端 sendto → 服务端 recvfrom(拿到客户端地址)→ 服务端 sendto 应答 → 客户端 recvfrom。
为什么服务端一般先 recvfrom?服务端启动时不知道客户端地址,只能收到报文之后才能拿到地址回包,属于业务逻辑约束,不是内核强制。 - 主动推送场景:如果服务端提前写死客户端 IP+端口,可以直接 sendto 推送消息,客户端提前阻塞在 recvfrom 等待接收。
4. UDP 内核接收缓冲区机制(面试重点)
每个 socket 在内核都有独立的接收缓冲区,用来存放已经到达、用户进程还没有 recvfrom 取走的数据报。
类比:内核接收缓冲区 = 快递柜;recvfrom = 用户取快递;sendto = 快递员投递。
✅ 正确结论:
客户端先 sendto 发包,服务端进程还没调用 recvfrom,报文不会直接丢失;报文先存入内核接收缓冲区排队,后续调用 recvfrom 依旧可以读到。缓冲区是按数据报存储,不会粘包,一个 recvfrom 读取一个完整数据报。
❌ 错误认知:没有 recvfrom 等待,sendto 的数据包直接丢了。
缓冲区满时行为:
缓冲区存在上限(可通过 setsockopt 设置 SO_RCVBUF,受内核全局上限限制)。缓冲区装满之后,后续新来的 UDP 报文直接丢弃,无任何通知;缓冲区中已经存入的包保留,等 recvfrom 取走腾出空间后,才能接收新包。
5. UDP 和 TCP 缓冲区满行为对比
- UDP:接收缓冲区满 → 直接丢包,发送方无感知;
- TCP:接收缓冲区满 → 通过滑动窗口通知对端停止发送,发送方暂缓发送数据,不会丢包。
6. UDP 核心考点汇总
- 无连接、无握手挥手,不维护连接状态;
- 每个数据报独立,自带目标地址;
- recvfrom 两个作用:读取数据 + 获取发送端地址,用于 sendto 回包;
- 不提供确认、重传、排序,报文可能丢失、乱序;
- UDP 没有粘包问题,数据有边界。
三、TCP Socket VS UDP Socket 总对比表
| 对比项 | TCP | UDP |
|---|---|---|
| 连接特性 | 面向连接,三次握手建立,四次挥手断开 | 无连接,每个报文独立投递 |
| 套接字函数 | socket、bind、listen、accept、read/write、close | socket、bind(服务端常用)、sendto、recvfrom |
| fd模型 | 服务端:监听fd + 每个客户端独立通信fd;客户端单fd | 服务端单socket,接收所有客户端报文 |
| 数据形式 | 字节流,存在粘包 | 数据报,自带边界,无粘包 |
| 可靠性 | 可靠:确认、重传、排序、流量控制 | 不可靠,可能丢包、乱序 |
| 满缓冲区行为 | 通知对方停发,不丢包 | 直接丢弃新来报文,无通知 |
| 典型场景 | HTTP、MySQL、文件传输 | DNS、直播、音视频、TFTP |
四、一句话时序总结
TCP:服务端 socket-bind-listen-阻塞accept;客户端 socket-connect;握手成功后双向读写,最后 close 挥手断开。
UDP:无连接通道,数据包独立寻址;一问一答模式下客户端先发 sendto,服务端 recvfrom 获取地址后 sendto 应答;报文先存放内核缓冲区,缓冲区溢出丢包。
五、常见面试追问清单
- accept 阻塞的时候,三次握手完成了吗? 答:完成了,内核完成握手,连接放入就绪队列之后 accept 才返回。
- 为什么 UDP 客户端可以不 bind? 答:sendto 发送时内核自动分配临时随机端口。
- read 返回 0 代表什么? 答:读到 EOF,对端调用 close 关闭连接。
- UDP sendto 对方进程没有 recvfrom,包一定丢吗? 答:不一定,报文存入内核接收缓冲区,缓冲区没满就可以后续读取;缓冲区满才丢包。
- TCP 监听 fd 能不能用来收发业务数据? 答:不能,listen fd 职责只有接收新连接请求。