目录
1、TCP服务器-客户端基本模型
1.1.服务器初始化:被动监听准备
1.2.连接建立:三次握手(同步序列号与确认通信能力)
telnet
1.3.数据传输:全双工可靠通信
1.4.连接断开:四次挥手(确保双方数据传输完成)
2、TCP 与 UDP 的缓冲区机制对比
2.1.TCP 接收缓冲区 vs UDP 接收缓冲区
2.2TCP 发送缓冲区 VS UDP 发送缓冲区
2.3读写操作的本质
3.服务器端口复用
4.信号处理与写失败
1、TCP服务器-客户端基本模型

1.1.服务器初始化:被动监听准备
服务器通过socket API完成“被动打开”,被动等待客户端连接
- 应用层通过socket API(如connect()/accept())触发TCP层动作(如发送SYN/SYN+ACK),通过函数返回值感知TCP层状态(如connect()返回表示三次握手完成、read()返回0表示收到FIN)
- accept()/connect()/read()等函数默认阻塞,等待TCP层的响应(如SYN、数据、FIN),是同步IO的核心特征
关键函数:listen 与 accept
| 函数名 | listen |
| 头文件 | <sys/socket.h> |
| 原型 | int listen(int sockfd, int backlog); |
| 参数1 | sockfd:已绑定的套接字文件描述符 |
| 参数2 | backlog:未完成连接队列的最大长度(等待 accept 的连接数) |
| 返回值 | 成功返回 0,失败返回 -1,设置 errno |
| 功能 | 将套接字由主动态转变为被动态,开始监听客户端连接请求 |
| 函数名 | accept |
| 头文件 | <sys/socket.h> |
| 原型 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
| 参数1 | sockfd:监听套接字(listen 后的套接字) |
| 参数2 | addr:指向协议地址结构的指针,用于返回客户端的地址信息 |
| 参数3 | addrlen:地址结构的长度(值-结果参数) |
| 返回值 | 成功返回新的已连接套接字文件描述符,失败返回 -1 |
| 功能 | 从已完成连接队列中取出第一个连接,返回一个新套接字用于与客户端通信 |
1.2.连接建立:三次握手(同步序列号与确认通信能力)
客户端主动发起“主动打开”,通过三次握手确保双方具备收发能力
三次握手确保连接建立的可靠性(避免“已失效的连接请求”干扰)
ACK机制保证数据传输的可靠性(丢失重传)
| 1 | 调用socket()创建fd → 调用connect()发送SYN段(同步序列号) | – | 客户端:CLOSED→SYN_SENT;服务器:LISTEN→SYN_RCVD |
| 2 | – | 收到SYN后,发送SYN+ACK段(同步+确认) | 服务器:SYN_RCVD→ESTABLISHED(等待客户端ACK) |
| 3 | 收到SYN+ACK后,发送ACK段确认 | 收到ACK后,完成连接建立 | 客户端:SYN_SENT→ESTABLISHED(connect()返回);服务器:ESTABLISHED(accept()返回,获得连接套接字connfd) |
- 无需显示绑定:客户端调用connect时,内核会自动为套接字分配临时端口(隐式绑定)
- 关键函数:connect
| 函数名 | connect |
| 头文件 | <sys/socket.h> |
| 原型 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
| 参数1 | sockfd:socket 函数返回的套接字文件描述符 |
| 参数2 | addr:指向服务器地址结构的指针(IP + 端口) |
| 参数3 | addrlen:地址结构的大小 |
| 返回值 | 成功返回 0,失败返回 -1 |
| 功能 | 客户端主动向服务器发起连接请求(TCP 三次握手) |
-
telnet
| 全称 | Teletype Network Protocol(远程终端协议) |
| 类型 | 应用层协议,属于TCP/IP协议族的一部分 |
| 设计目的 | 提供远程终端访问服务,允许用户通过网络连接到远程主机并执行命令,如同直接操作本地终端一样 |
| 工作原理 | 基于客户端-服务器模型,客户端通过Telnet协议连接到远程服务器,服务器响应客户端请求并执行相应操作 |
| 端口号 | 默认使用TCP端口23 |
| 传输方式 | 明文传输(数据以纯文本形式在网络上传输,存在安全隐患) |
| 安全性 | 较低,易受到中间人攻击、数据窃听等安全威胁。现代应用中常被更安全的协议(如SSH)替代 |
| 主要功能 |
|
| 替代协议 | SSH(Secure Shell),提供加密通信和更强大的安全功能,是Telnet的现代替代方案 |
| 优缺点 |
|
| 现状 | 由于安全性问题,Telnet在现代网络环境中已逐渐被淘汰,但在某些特定场景或旧系统中仍可能使用 |
| 连接远程主机 | telnet [远程主机IP或域名] [端口号] |
| 退出 Telnet 会话 | exit 或 Ctrl + ] + quit |
| 测试端口连通性 | telnet example.com 80 |
| 发送特殊字符(如中断) | 按 Ctrl + ] 返回 Telnet 提示符,再输入命令 |
1.3.数据传输:全双工可靠通信
连接建立后,TCP提供全双工通信(双方可同时收发数据)通过ACK确认机制保证数据可靠
- 服务器侧:accept()返回后,循环执行read(connfd)(阻塞等待客户端数据)→ 处理请求 → write(connfd)(发送应答)→ 再次read()等待下一条请求;
- 客户端侧:connect()返回后,循环执行write(fd)(发送请求)→ read(fd)(阻塞等待服务器应答)→ 收到应答后发送下一条请求;
- TCP层核心作用:自动处理数据分段、ACK确认、超时重传(若数据丢失),应用层无需关心底层细节,仅通过read()/write()的返回值感知状态(如read()返回0表示收到FIN段,即对方关闭连接)
1.4.连接断开:四次挥手(确保双方数据传输完成)
当一方无数据传输时,通过四次挥手释放连接(以客户端主动关闭为例)
四次挥手确保连接释放的可靠性(避免数据丢失)
ACK机制保证数据传输的可靠性(丢失重传)
| 1 | 调用close(fd),发送FIN报文(seq=u,结束“客户端→服务器”的发送方向) | 收到FIN后,立即发送ACK报文(ack=u+1,确认收到FIN) | 客户端:ESTABLISHED→FIN_WAIT_1;服务器:ESTABLISHED→CLOSE_WAIT |
| 2 | 收到服务器的ACK,进入FIN_WAIT_2(等待服务器关闭“服务器→客户端”的发送方向) | 调用read(connfd),返回0(表示收到客户端FIN,读管道“无数据”);处理剩余数据后,调用close(connfd)发送FIN报文(seq=v,结束“服务器→客户端”的发送方向) | 客户端:FIN_WAIT_1→FIN_WAIT_2;服务器:CLOSE_WAIT→LAST_ACK |
| 3 | 收到服务器的FIN,发送ACK报文(ack=v+1,确认收到服务器FIN) | 收到客户端的ACK,连接释放 | 客户端:FIN_WAIT_2→TIME_WAIT(等待2MSL,确保ACK到达);服务器:LAST_ACK→CLOSED |
| 4 | 等待2MSL(最大报文段寿命的2倍,约1-4分钟)后,进入CLOSED | – | 客户端:TIME_WAIT→CLOSED |
2、TCP 与 UDP 的缓冲区机制对比
2.1.TCP 接收缓冲区 vs UDP 接收缓冲区
| 存在性 | 有(内核中维护,用于存储按序到达但未被应用读取的数据) | 有(内核中维护,用于临时存储收到的数据报) |
| 主要目的 | 1. 按序重组:确保数据按发送顺序交付给应用层。 2. 流量控制:通过窗口机制通知发送方接收能力,避免缓冲区溢出。 3. 解耦应用层与网络:应用层可按需读取数据,无需实时处理。 | 1. 临时存储:仅保存收到的数据报,不保证顺序。 2. 无连接解耦:应用层需自行处理数据报的顺序和完整性。 3. 简单缓冲:仅缓解网卡与应用层之间的速度差异。 |
| 数据排序 | 严格按序:TCP 通过序列号确保数据按序交付,乱序数据会暂存缓冲区,等待缺失数据到达后再组装。 | 无序:UDP 不保证数据顺序,每个数据报独立处理,应用层需自行处理顺序问题。 |
| 缓冲区满时行为 | 1. 通告窗口减小:通过 TCP 窗口机制通知发送方降低发送速率(流量控制)。 2. 极端情况下可能丢弃数据(如持续超限,但会优先保证可靠性)。 | 1. 丢弃新数据:若缓冲区满,新到达的数据报直接丢弃。 2. 无重传机制:发送方不会因接收缓冲区满而重传(UDP 本身无重传)。 |
| 数据生命周期 | 直到应用读取或超时:数据在缓冲区中保留,直到应用层通过 read() 读取,或因超时(如 RTO)被丢弃。 | 直到应用读取或被覆盖:数据报在缓冲区中保留,直到应用层读取;若缓冲区满且新数据到达,旧数据可能被覆盖或丢弃。 |
| 应用层交互 | 背压传递:通过滑动窗口机制动态调整发送速率,避免接收缓冲区溢出。 | 无背压:应用层需自行监控缓冲区状态(如通过 recv() 返回值),可能因缓冲区满导致数据丢失。 |
2.2TCP 发送缓冲区 VS UDP 发送缓冲区
| 存在性 | 有(内核中) | 有(内核中) |
| 主要目的 | 可靠传输、流量控制、拥塞控制 | 平滑突发、解耦应用层与网卡 |
| 重传机制 | 有(超时重传) | 无(丢了就丢了) |
| 缓冲区满时 | 阻塞应用层写入(背压) | 丢弃新数据 或 返回错误 (EAGAIN) |
| 数据生命周期 | 直到收到 ACK 才删除 | 交给网卡后即删除(或失败时丢弃) |
┌─────────────────────────────────────────────────────────────────────┐
│ 进程空间 (用户空间) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 读缓冲区 │ │ 写缓冲区 │ │
│ │ char recv_buf[1024]│ │ char send_buf[1024]│ │
│ │ (应用程序分配) │ │ (应用程序分配) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ │ recv()/recvfrom() │ send()/sendto() │
│ │ 从内核拷贝到用户 │ 从用户拷贝到内核 │
│ ▼ ▼ │
└─────────────┼────────────────────────────────────┼───────────────────┘
│ │
↓ 数据拷贝 ↓ 数据拷贝
┌─────────────┼────────────────────────────────────┼───────────────────┐
│ 内核空间 │ │ │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │
│ │ 接收缓冲区 │ │ 发送缓冲区 │ │
│ │ (struct sk_buff队列)│ │ (struct sk_buff队列)│ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ sk_buff1 │ │ │ │ sk_buff1 │ │ │
│ │ ├───────────────┤ │ │ ├───────────────┤ │ │
│ │ │ sk_buff2 │ │ │ │ sk_buff2 │ │ │
│ │ ├───────────────┤ │ │ ├───────────────┤ │ │
│ │ │ … │ │ │ │ … │ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ │ │ │ │ │
│ │ • 已收到但未读 │ │ • 已写入但未发送 │ │
│ │ • 等待应用程序读取 │ │ • 等待网络协议栈发送│ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ │ 协议栈处理 │ 协议栈处理 │
│ ▼ ▼ │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │
│ │ TCP/IP协议栈 │ │ TCP/IP协议栈 │ │
│ │ • 重组数据包 │ │ • 分段 │ │
│ │ • 校验和检查 │ │ • 添加头部 │ │
│ │ • 顺序控制 │ │ • 计算校验和 │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ↓ 数据来自网络 ↓ 数据发往网络 │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │
│ │ 网卡驱动 │ │ 网卡驱动 │ │
│ │ (接收中断处理) │ │ (发送DMA传输) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ↓ DMA拷贝 ↑ DMA拷贝 │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │
│ │ 网卡硬件缓冲区 │ │ 网卡硬件缓冲区 │ │
│ │ (Ring Buffer) │ │ (Ring Buffer) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ↓ 网络传输 ↑ 网络传输 │
│ ────┴────────────────────────────────────┴─── │
│ 网络 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 进程空间 (用户空间) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 读缓冲区 │ │ 写缓冲区 │ │
│ │ char recv_buf[1024]│ │ char send_buf[1024]│ │
│ │ (应用程序分配) │ │ (应用程序分配) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ │ recvfrom() │ sendto() │
│ │ 从内核拷贝到用户 │ 从用户拷贝到内核 │
│ │ (等待数据到达) │ (立即返回)① │
│ ▼ ▼ │
└─────────────┼────────────────────────────────────┼───────────────────┘
│ │
↓ 数据拷贝 ↓ 数据拷贝
┌─────────────┼────────────────────────────────────┼───────────────────┐
│ 内核空间 │ │ │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │
│ │ 接收缓冲区 │ │ UDP协议层 │ │
│ │ (socket接收队列) │ │ • 添加UDP头 │ │
│ │ │ │ • 计算校验和 │ │
│ │ ┌───────────────┐ │ │ • 封装成数据报 │ │
│ │ │ 数据报1 │ │ │ • 无拥塞控制 │ │
│ │ ├───────────────┤ │ │ • 无重传 │ │
│ │ │ 数据报2 │ │ └──────────┬──────────┘ │
│ │ ├───────────────┤ │ │ │
│ │ │ … │ │ │ 加入协议栈 │
│ │ └───────────────┘ │ ▼ │
│ │ │ ┌─────────────────────────┐ │
│ │ • 已到达但未读 │ │ IP协议层 │ │
│ │ • 每个数据报独立 │ ◀══════════ │ • 路由查找 │ │
│ │ • 可能丢失或乱序 │ 回退路径 │ • 分片处理 │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ↓ 数据来自网络 │ 加入Qdisc队列 │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐ │
│ │ UDP协议层 │ │ Qdisc (排队规则) │◀═════② │
│ │ • 去除UDP头 │ │ • 系统的发送缓冲区 │ │
│ │ • 校验和检查 │ │ • 默认pfifo_fast │ │
│ │ • 根据端口分发 │ │ • 大小由txqueuelen │ │
│ │ • 无顺序保证 │ │ 决定(默认1000) │ │
│ └──────────┬──────────┘ │ • 如果满了:丢包③ │ │
│ │ └──────────┬──────────┘ │
│ ↓ │ │
│ ┌──────────▼──────────┐ ↓ │
│ │ IP协议层 │ ┌──────────▼──────────┐ │
│ │ (网络层处理) │◀══════════════│ 网卡驱动 │ │
│ └──────────┬──────────┘ 从Qdisc │ (发送DMA传输) │ │
│ │ 取出数据报 └──────────┬──────────┘ │
│ ↓ │ │
│ ┌──────────▼──────────┐ ↓ DMA拷贝 │
│ │ 网卡驱动 │ ┌──────────▼──────────┐ │
│ │ (接收中断处理) │ │ 网卡硬件缓冲区 │ │
│ └──────────┬──────────┘ │ (Ring Buffer) │ │
│ │ │ • 如果满了:丢包④ │ │
│ ↓ DMA拷贝 └──────────┬──────────┘ │
│ ┌──────────▼──────────┐ │ │
│ │ 网卡硬件缓冲区 │ ↓ 网络传输 │
│ │ (Ring Buffer) │ ┌──────────▼──────────┐ │
│ │ • 接收数据暂存 │ │ 物理网线 │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ↓ 网络传输 │ │
│ ────┴────────────────────────────────────┴─── │
│ 网络 │
└─────────────────────────────────────────────────────────────────────┘
2.3读写操作的本质
TCP/UDP读写操作本质上是对内核中发送缓冲区和接收缓冲区的数据拷贝
写操作:用户空间 -> 内核空间(拷贝)
- TCP:拷贝到发送缓冲区,由协议栈负责后续重传、排序、流量控制
- UDP:拷贝到发送缓冲区,协议栈尽快发送,不排队等待 ACK
读操作:内核空间 -> 用户空间(拷贝)
- 两者都从接收缓冲区拷贝。若无数据,行为取决于 I/O 模式(阻塞/非阻塞/信号驱动)
写操作将用户数据拷贝到内核发送缓冲区等待网络发送,读操作从内核接收缓冲区拷贝数据到用户空间,这两个缓冲区完全独立、互不干扰,因此读写操作可以同时进行而不会冲突;当接收缓冲区为空时读操作会阻塞(除非设置非阻塞),当发送缓冲区满时写操作会阻塞,这种设计实现了全双工通信,使得网络数据的收发可以并发处理,大大提高了通信效率
3.服务器端口复用
服务器主动关闭后,端口可能进入TIME_WAIT状态,导致立即重启时绑定失败
绑定失败的原因:当端口被标记为TIME_WAIT状态,操作系统会拒绝服务器尝试重启并绑定到同一端口的操作
TIME_WAIT状态的作用:确保最后一个ACK能够到达对端(客户端),并让网络中剩余的旧连接数据包自然消亡,避免干扰新连接
解决方法:通过设置套接字选项SO_REUSEADDR,可以允许服务器在端口处于TIME_WAIT状态时重新绑定到该端口
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
4.信号处理与写失败
SIGPIPE信号:当对端关闭连接后,继续向该连接写入数据,内核会发送SIGPIPE信号,默认行为是终止进程。通常需要忽略或处理该信号
signal(SIGPIPE, SIG_IGN);
触发场景:最常见于网络编程中,客户端关闭连接后,服务器仍尝试写入数据
常见做法是在服务器初始化时调用 signal(SIGPIPE, SIG_IGN),在关键写入操作后检查 errno == EPIPE,处理连接断开逻辑
网硕互联帮助中心




评论前必须登录!
注册