云计算百科
云计算领域专业知识百科平台

Linux 之 【TCP套接字编程】(TCP服务器-客户端基本模型、TCP 与 UDP 的缓冲区机制对比、服务器端口复用、信号处理与写失败)

目录

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():创建套接字文件描述符(fd),分配内核资源(如TCP控制块)
  • bind():将fd与服务器的IP+端口绑定(若端口已被占用则失败)
  • listen():将fd标记为监听状态,指定连接队列长度(如listen(fd, 5)表示最多缓存5个未处理的连接请求)
  • accept():阻塞等待客户端连接(此时服务器TCP层处于LISTEN状态,收到客户端SYN后进入SYN_RCVD,后续通过三次握手建立连接
    • 应用层通过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
    功能 从已完成连接队列中取出第一个连接,返回一个新套接字用于与客户端通信
  • accept返回的新的文件描述符用于与客户端通信,原监听套接字继续等待新连接
  • accept的两个fd各司其职:监听fd只负责接受连接,新fd负责数据读写,提高并发效率
  • 1.2.连接建立:三次握手(同步序列号与确认通信能力)

    客户端主动发起“主动打开”,通过三次握手确保双方具备收发能力

    三次握手确保连接建立的可靠性(避免“已失效的连接请求”干扰)

    ACK机制保证数据传输的可靠性(丢失重传)

    步骤客户端操作服务器操作TCP层状态变化
    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的现代替代方案
    优缺点
    • 优点:简单易用,广泛支持
    • 缺点:安全性低,明文传输数据;功能相对有限,不如SSH等现代协议强大
    现状 由于安全性问题,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机制保证数据传输的可靠性(丢失重传)

    步骤客户端操作/报文服务器操作/报文TCP层状态变化(客户端→服务器)
    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 接收缓冲区

    特性TCP 接收缓冲区UDP 接收缓冲区
    存在性 有(内核中维护,用于存储按序到达但未被应用读取的数据) 有(内核中维护,用于临时存储收到的数据报)
    主要目的 1. 按序重组:确保数据按发送顺序交付给应用层。 2. 流量控制:通过窗口机制通知发送方接收能力,避免缓冲区溢出。 3. 解耦应用层与网络:应用层可按需读取数据,无需实时处理。 1. 临时存储:仅保存收到的数据报,不保证顺序。 2. 无连接解耦:应用层需自行处理数据报的顺序和完整性。 3. 简单缓冲:仅缓解网卡与应用层之间的速度差异。
    数据排序 严格按序:TCP 通过序列号确保数据按序交付,乱序数据会暂存缓冲区,等待缺失数据到达后再组装。 无序:UDP 不保证数据顺序,每个数据报独立处理,应用层需自行处理顺序问题。
    缓冲区满时行为 1. 通告窗口减小:通过 TCP 窗口机制通知发送方降低发送速率(流量控制)。 2. 极端情况下可能丢弃数据(如持续超限,但会优先保证可靠性)。 1. 丢弃新数据:若缓冲区满,新到达的数据报直接丢弃。 2. 无重传机制:发送方不会因接收缓冲区满而重传(UDP 本身无重传)。
    数据生命周期 直到应用读取或超时:数据在缓冲区中保留,直到应用层通过 read() 读取,或因超时(如 RTO)被丢弃。 直到应用读取或被覆盖:数据报在缓冲区中保留,直到应用层读取;若缓冲区满且新数据到达,旧数据可能被覆盖或丢弃。
    应用层交互 背压传递:通过滑动窗口机制动态调整发送速率,避免接收缓冲区溢出。 无背压:应用层需自行监控缓冲区状态(如通过 recv() 返回值),可能因缓冲区满导致数据丢失。

    2.2TCP 发送缓冲区 VS UDP 发送缓冲区

    特性TCP 发送缓冲区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,处理连接断开逻辑

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux 之 【TCP套接字编程】(TCP服务器-客户端基本模型、TCP 与 UDP 的缓冲区机制对比、服务器端口复用、信号处理与写失败)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!