TCP 协议基础特性
TCP(Transmission Control Protocol,传输控制协议)是网络传输层的核心协议,用于实现不同网络互联主机间进程的可靠通信,是互联网数据传输的基础协议之一。
核心特点
- 面向连接:通信前必须建立专属连接,通信结束后需正常断开
- 端到端可靠:确保数据无丢失、无重复、按序到达,能自动恢复传输错误
- 一对一通信:仅支持单点到单点连接,不提供广播和组播服务
- 面向字节流:数据以连续字节流形式传输,不保留应用层数据的边界
- 全双工通信:连接建立后,双方可同时发送和接收数据
协议依赖与工作环境
TCP 工作在 IP 协议之上,依赖 IP 协议提供的底层数据传输服务。IP 协议仅负责将数据报从源主机送达目标主机,但无法保证传输可靠性(可能出现丢包、乱序、重复、损坏),TCP 通过自身机制弥补这些缺陷,实现可靠传输。
TCP 可靠性保障机制
TCP 通过多重机制实现数据传输的可靠性,核心包括序列号、确认应答、校验和、超时重传四大核心机制。
序列号机制
- 每个数据字节分配唯一 32 位序列号,标识数据在字节流中的位置
- 解决数据乱序问题:接收端通过序列号对数据重新排序
- 解决数据重复问题:接收端通过序列号识别重复数据并丢弃
- 初始序列号(ISN):连接建立时随机生成,通过 SYN 报文传递给对方
确认应答机制(ACK)
- 接收端成功收到数据后,必须发送确认应答报文
- 确认应答号字段标识期望接收的下一个字节序列号(即已正确接收数据的最后一个字节序列号 + 1)
- 连接建立后,ACK 字段始终有效,确保双方实时同步数据接收状态
- 超时重传:发送端未在规定时间内收到 ACK,会自动重传对应数据段
校验和机制
- 每个 TCP 数据段都包含 16 位校验和字段,由发送端计算生成
- 发送端:基于数据内容和 TCP 头部信息计算校验和,填入报头
- 接收端:收到数据后重新计算校验和,与报头字段对比
- 校验失败时:直接丢弃损坏数据段,不发送 ACK,触发发送端重传
流量控制机制
- 通过窗口号字段实现,窗口号表示接收端当前可用的缓冲区大小(字节数)
- 发送端根据接收端的窗口大小调整发送速率,避免接收端缓冲区溢出
- 窗口号动态更新:接收端随缓冲区使用情况实时调整窗口值并通过 ACK 报文告知发送端
TCP 报首格式详解
TCP 报头是 TCP 协议工作的核心载体,包含控制通信的关键字段,最小长度为 20 字节(无选项字段时),带选项字段时最长可达 60 字节。
报头核心字段

各字段详细说明
- 源端口号(16 位):发送端进程对应的端口号,范围 0-65535,0-1023 为知名端口
- 目标端口号(16 位):接收端进程对应的端口号,用于定位目标主机上的具体通信进程
- 序列号(32 位):数据段中第一个字节的序号,SYN 标志位存在时为初始序列号(ISN),数据起始序号为 ISN+1
- 确认应答号(32 位):期望接收的下一个数据段序列号,仅当 ACK 标志位为 1 时有效
- 头部长度(4 位):TCP 头部总长度,以 32 位(4 字节)为单位,最小值为 5(对应 20 字节)
- 保留位(6 位):预留字段,暂未使用,默认填充 0
- 控制位(6 位):关键控制标志,决定 TCP 连接状态和操作
- URG:紧急指针有效,标识报文包含紧急数据
- ACK:确认应答有效,连接建立后始终为 1
- PSH:推送功能,要求接收端立即将数据交给应用层
- RST:重置连接,用于强制断开异常连接
- SYN:同步序列号,用于建立连接
- FIN:结束连接,标识发送端无更多数据发送
- 窗口大小(16 位):接收端可用缓冲区大小,用于流量控制
- 校验和(16 位):用于校验 TCP 头部和数据的完整性
- 紧急指针(16 位):URG 为 1 时有效,指向紧急数据的最后一个字节位置
- 选项字段(可选):变长字段,用于扩展 TCP 功能(如窗口缩放、时间戳等)
- 填充字段:确保 TCP 头部长度为 32 位的整数倍,填充 0
TCP 连接机制
连接建立条件
- 通信双方必须创建 TCP 套接字,每个连接由一对套接字(源 IP + 源端口 + 目标 IP + 目标端口)唯一标识
- 基于不可靠网络环境,采用序列号握手机制避免错误的连接初始化
- 仅支持点对点连接,不支持多播和广播模式
三次握手机制(连接建立过程)
TCP 通过三次报文交换建立可靠连接,确保双方序列号同步和通信能力确认:

- 客户端(主动连接端)发送 SYN 报文:包含客户端初始序列号 ISNc,进入 SYN-SENT 状态
- 服务器(被动连接端)回复 SYN+ACK 报文:包含服务器初始序列号 ISNs 和对客户端 ISNc 的确认(ISNc+1),进入 SYN-RECEIVED 状态
- 客户端发送 ACK 报文:确认服务器 ISNs(ISNs+1),进入 ESTABLISHED 状态;服务器收到后也进入 ESTABLISHED 状态,连接建立完成
注意:仅第三次握手的报文可以携带数据,前两次握手仅用于连接协商
四次挥手机制(连接断开过程)
TCP 连接是全双工的,需双方分别关闭发送通道,通过四次报文交换完成断开:

- 主动关闭方发送 FIN 报文:标识自身无更多数据发送,进入 FIN-WAIT-1 状态
- 被动关闭方回复 ACK 报文:确认收到 FIN,进入 CLOSE-WAIT 状态,此时仍可发送未完成数据
- 被动关闭方发送 FIN 报文:所有数据发送完毕后,发送 FIN 标识关闭,进入 LAST-ACK 状态
- 主动关闭方回复 ACK 报文:确认收到 FIN,进入 TIME-WAIT 状态(等待 2MSL),之后进入 CLOSED 状态;被动关闭方收到 ACK 后直接进入 CLOSED 状态
MSL(Maximum Segment Lifetime):报文最大生存时间,Linux 系统默认 MSL=30 秒,2MSL=60 秒,确保网络中残留的报文被丢弃,避免端口复用导致的错误
TCP 有限状态机

TCP 连接的生命周期可通过有限状态机描述,核心状态及转换如下:
- 关闭状态(CLOSED):初始状态,无连接
- 监听状态(LISTEN):服务器调用 listen 后进入,等待客户端连接
- 同步已发送(SYN-SENT):客户端发送 SYN 后进入,等待服务器响应
- 同步已接收(SYN-RECEIVED):服务器收到 SYN 并发送 SYN+ACK 后进入
- 已建立(ESTABLISHED):连接建立成功,可进行数据传输
- 关闭等待(CLOSE-WAIT):被动关闭方收到 FIN 后进入,准备关闭
- 最后确认(LAST-ACK):被动关闭方发送 FIN 后进入,等待确认
- 时间等待(TIME-WAIT):主动关闭方发送最后 ACK 后进入,等待 2MSL
TCP 通信流程与核心函数
TCP 通信采用 C/S(客户端 / 服务器)架构,双方通过一系列系统调用实现数据传输,核心函数包括 socket、bind、listen、accept、connect、send、recv 等。
客户端通信流程
客户端流程:创建套接字 → 连接服务器 → 发送 / 接收数据 → 关闭连接
核心函数
/*** Socket创建函数,初始化TCP通信端点* @brief 建立客户端与服务器的通信接口,指定TCP协议类型* @param domain 协议族,TCP通信固定使用AF_INET(IPv4)或AF_INET6(IPv6)* @param type 套接字类型,TCP必须使用SOCK_STREAM(面向连接的字节流)* @param protocol 具体协议,TCP通信填0(默认匹配SOCK_STREAM对应的TCP协议)* @return int 成功返回套接字文件描述符(非负整数),失败返回-1* @note 套接字是跨主机通信的唯一接口,每个套接字对应一个独立的通信通道*/
int socket(int domain, int type, int protocol);/*** 连接服务器函数,建立TCP连接* @brief 主动向服务器发起连接请求,完成TCP三次握手* @param sockfd 套接字文件描述符,由socket函数创建返回* @param addr 服务器地址结构体指针,包含服务器IP和端口号* @param addrlen 服务器地址结构体长度,通过sizeof计算* @return int 成功返回0,失败返回-1* @note 连接失败时,当前套接字状态不可预测,需关闭后重新创建* 基于连接的套接字只能成功连接一次,多次调用会失败*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);/*** 发送数据函数,向已连接的服务器传输数据* @brief 将应用层数据通过TCP连接发送到服务器,依赖已建立的连接* @param sockfd 已连接的套接字文件描述符* @param buf 待发送数据的缓冲区指针,存储要发送的内容* @param len 待发送数据的长度(字节数)* @param flags 发送标志,0表示默认模式(与write函数功能相同),MSG_OOB表示发送紧急数据* @return ssize_t 成功返回实际发送的字节数,失败返回-1* @note 仅能用于已连接的套接字,未连接或监听状态的套接字调用会失败* 数据过大时会阻塞,直到缓冲区有空间(非阻塞模式下会返回EAGAIN错误)*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);/*** 通用文件写入函数,适用于所有文件类型(含TCP套接字)* @param fd 文件描述符:TCP场景中为已连接的套接字描述符(conn_fd/sock_fd)* @param buf 待发送数据的缓冲区指针:存储要传输的应用层数据* @param len 待发送数据的长度(字节数):不能超过缓冲区实际有效数据长度* @return ssize_t 成功返回实际写入的字节数(TCP中即实际发送的字节数);* 失败返回-1(错误码存于errno);* 返回0表示未写入任何数据(TCP中极少出现,通常是len=0时)* @note TCP场景中,等价于 send(fd, buf, len, 0),无额外控制标志*/
ssize_t write(int fd, const void *buf, size_t len);/*** 套接字专用发送函数,支持显式指定目标地址(TCP场景需适配)* @param sockfd 已连接的TCP套接字描述符(客户端connect后、服务器accept后返回的conn_fd)* @param buf 待发送数据的缓冲区指针:存储应用层要传输的字节数据* @param len 待发送数据的长度(字节数):不能超过缓冲区有效数据长度,且需兼容TCP MSS(最大分段大小)* @param flags 发送控制标志:与send函数完全一致,TCP场景常用取值如下* - 0:常规发送,等价于write/send(无特殊控制);* - MSG_OOB:发送带外数据(紧急数据,仅1字节有效);* - MSG_DONTWAIT:临时非阻塞模式(仅本次调用有效,缓冲区满时直接返回EAGAIN);* - MSG_NOSIGNAL:禁用SIGPIPE信号(对端关闭连接时不触发程序退出);* @param dest_addr 目标地址结构体指针:TCP场景中需与已连接的地址完全一致(IP+端口),* 可设为NULL(无需验证地址时,等价于send函数)* @param addrlen dest_addr结构体的长度(字节数):传入sizeof(struct sockaddr_in),* 若dest_addr为NULL,需设为0* @return ssize_t 成功返回实际发送的字节数(可能小于len,因内核缓冲区不足);* 失败返回-1(错误码存于errno);* 返回0仅当len=0时(无实际数据发送)* @note TCP场景中必须先建立连接,地址参数仅用于验证,不影响数据传输的目标(由连接维护)*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);/*** 接收数据函数,从已连接的服务器获取数据* @brief 从TCP连接中读取服务器发送的数据,存储到应用层缓冲区* @param sockfd 已连接的套接字文件描述符* @param buf 接收数据的缓冲区指针,用于存储读取到的数据* @param len 缓冲区最大容量(字节数),避免数据溢出* @param flags 接收标志,0表示默认模式(与read函数功能相同),MSG_OOB表示接收紧急数据* @return ssize_t 成功返回实际接收的字节数,失败返回-1,对端关闭连接返回0* @note 无数据时会阻塞,直到有数据到达(非阻塞模式下会返回EAGAIN错误)* 接收的数据先存储在内核缓冲区,调用此函数后拷贝到应用层缓冲区*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);/*** 通用文件读取函数,适用于所有文件类型(含TCP套接字)* @param fd 文件描述符:TCP场景中为已连接的套接字描述符(conn_fd/sock_fd)* @param buf 接收数据的缓冲区指针:用于存储从TCP内核缓冲区读取的 data* @param len 缓冲区最大容量(字节数):避免数据溢出,需预留1字节存字符串结束符'\0'(文本数据)* @return ssize_t 成功返回实际读取的字节数(TCP中即接收的有效数据字节数);* 失败返回-1(错误码存于errno);* 返回0表示对端正常关闭连接(TCP四次挥手完成,无更多数据)* @note TCP场景中,等价于 recv(fd, buf, len, 0),无额外控制标志*/
ssize_t read(int fd, void *buf, size_t len);/*** 套接字专用接收函数,支持获取对端地址(TCP场景需适配)* @param sockfd 已连接的TCP套接字描述符(客户端sock_fd/服务器conn_fd)* @param buf 接收数据的缓冲区指针:存储接收的TCP数据* @param len 缓冲区最大容量(字节数):避免溢出* @param flags 接收控制标志:TCP场景常用0或MSG_OOB,含义与send函数一致* @param src_addr 输出型参数:TCP场景中存储对端(客户端/服务器)的地址信息(IP+端口),* 可设为NULL(无需获取地址时)* @param addrlen 输入输出型参数:传入src_addr结构体的大小(sizeof(struct sockaddr_in)),* 函数返回时存储实际地址信息的长度* @return ssize_t 成功返回实际接收的字节数;* 失败返回-1(错误码存于errno);* 返回0表示对端正常关闭连接* @note 原本为UDP无连接场景设计,TCP中需先建立连接,地址参数仅用于获取对端信息*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
服务器通信流程
服务器流程:创建套接字 → 绑定端口 → 监听连接 → 接受连接 → 发送 / 接收数据 → 关闭连接
核心函数
/*** 绑定函数,关联套接字与本地端口* @brief 将服务器套接字与指定的IP地址和端口号绑定,确保客户端能通过该端口连接* @param sockfd 套接字文件描述符,由socket函数创建返回* @param addr 本地地址结构体指针,包含服务器IP和端口号* @param addrlen 本地地址结构体长度,通过sizeof计算* @return int 成功返回0,失败返回-1* @note 端口号范围:0-65535,1024以下为知名端口,建议使用1024以上端口避免冲突* 一个端口同一时间只能绑定一个套接字,绑定失败需更换端口*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);/*** 监听函数,设置套接字为监听状态* @brief 让服务器套接字进入监听模式,准备接收客户端连接请求* @param sockfd 已绑定的套接字文件描述符* @param backlog 等待连接队列的最大长度,默认最大值为128(由系统参数限制)* @return int 成功返回0,失败返回-1* @note 此函数仅设置监听状态,不会阻塞,调用后服务器可接收连接请求* backlog是等待accept处理的连接数,不是最大连接数,超过后新连接可能被拒绝*/
int listen(int sockfd, int backlog);/*** 接受连接函数,建立与客户端的连接* @brief 从监听队列中取出一个客户端连接请求,创建新的套接字用于数据通信* @param sockfd 监听状态的套接字文件描述符* @param addr 客户端地址结构体指针,用于存储连接客户端的IP和端口(可设为NULL)* @param addrlen 客户端地址结构体长度指针,传入时为结构体大小,返回时为实际地址长度* @return int 成功返回新的已连接套接字描述符,失败返回-1* @note 监听套接字仅用于接收连接,数据通信需使用此函数返回的新套接字* 无连接请求时会阻塞,直到有客户端发起连接(非阻塞模式下会返回EAGAIN错误)*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP 数据缓冲区
TCP 通信中,发送端和接收端均设有内核缓冲区,用于临时存储数据,协调应用层与网络层的传输速度差异。

接收缓冲区
- 作用:暂存从网络接收的数据,等待应用层调用 recv 函数读取
- 大小范围:Linux 系统默认 2304-425984 字节,不同主机可能不同
- 核心特性:
-
缓冲区大小可通过 setsockopt 函数设置,需在 listen 或 connect 前设置才生效
-
系统会自动将设置的缓冲区大小加倍(预留管理空间),getsockopt 返回的是加倍后的值
-
水位线机制:默认最小水位线为 1 字节,接收数据量超过水位线即触发读就绪,应用层可读取

Linux系统中接收缓冲区和发送缓冲区的最小字节数都被初始化为1,并且Linux系统的发送缓冲区的最小字节数不可以被修改,只有接收缓冲区在内核2.4版本允许被修改。
-
缓冲区操作函数
/*** 获取套接字选项函数,用于读取缓冲区相关配置* @brief 获取TCP套接字的接收缓冲区大小、水位线等属性值* @param sockfd 套接字文件描述符* @param level 协议级别,缓冲区设置固定使用SOL_SOCKET* @param optname 选项名称,SO_RCVBUF(接收缓冲区大小)、SO_RCVLOWAT(接收水位线)* @param optval 存储选项值的缓冲区指针* @param optlen 选项值缓冲区长度指针,传入时为缓冲区大小,返回时为实际值长度* @return int 成功返回0,失败返回-1* @note 读取接收缓冲区大小时,返回的是系统加倍后的值(包含管理空间)*/
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);/*** 设置套接字选项函数,用于配置缓冲区参数* @brief 设置TCP套接字的接收缓冲区大小、水位线等属性* @param sockfd 套接字文件描述符* @param level 协议级别,缓冲区设置固定使用SOL_SOCKET* @param optname 选项名称,SO_RCVBUF(接收缓冲区大小)、SO_RCVLOWAT(接收水位线)* @param optval 待设置的选项值指针* @param optlen 选项值的长度* @return int 成功返回0,失败返回-1* @note 必须在listen或connect前调用才能生效,否则设置无效* 接收水位线仅内核2.4及以上版本支持修改,发送水位线不可修改*/
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

发送缓冲区
- 作用:暂存应用层发送的数据,等待网络层发送;数据发送失败时用于重传
- 大小范围:Linux 系统默认 4608-425984 字节,不同主机可能不同
- 核心特性:
- 与接收缓冲区类似,设置时系统会自动加倍,用于存储重传数据和管理信息
- 发送数据时,若接收缓冲区已满,send 函数会阻塞(非阻塞模式下返回错误)
- 数据发送成功后,需等待接收端 ACK 确认后才会从缓冲区删除
TCP OOB 带外数据
OOB(Out of Band)带外数据是 TCP 的紧急数据机制,用于传输优先级高于普通数据的紧急信息,不受缓冲区和水位线限制。
核心特性
- 每次仅能发送 1 字节紧急数据,但可多次发送
- 需通过 send 函数的 MSG_OOB 标志发送,recv 函数的 MSG_OOB 标志接收
- 接收端收到带外数据时,内核会发送 SIGURG 信号通知应用程序
- 带外数据仍通过内核缓冲区传输,需及时读取避免占用缓冲区资源
实现要点
- 接收端需先通过 signal 函数绑定 SIGURG 信号的处理函数
- 通过 fcntl 函数设置套接字的 "所有者",确保 SIGURG 信号能被正确接收
- 信号处理函数需简洁高效,仅完成紧急数据的接收操作,避免中断主程序逻辑过久
- 带外数据的接收不受普通数据读取顺序影响,可优先处理
TCP 带外数据(OOB)传输示例
带外数据服务器(信号处理 OOB)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <pthread.h>#define PORT 8889
#define OOB_SIZE 1 // OOB 仅1字节有效
#define BUF_SIZE 1024int conn_fd; // 全局变量:信号处理函数和子线程共享(单客户端安全)// 信号处理函数:接收 OOB 数据(signal 绑定)
void sigurg_handler(int sig) {char oob_buf[OOB_SIZE];memset(oob_buf, 0, OOB_SIZE);// 接收 OOB 数据(MSG_OOB 标志,仅读1字节)ssize_t len = recv(conn_fd, oob_buf, OOB_SIZE, MSG_OOB);if (len == -1) {perror("recv OOB failed");return;}// 强制刷新缓冲,避免日志延迟显示printf("\n【OOB 接收成功】数据:%c(字节数:%ld)\n", oob_buf[0], len);fflush(stdout);
}// 子线程函数:专门接收普通数据
void *recv_normal_data(void *arg) {char buf[BUF_SIZE] = {0};while (1) {memset(buf, 0, BUF_SIZE);ssize_t len = recv(conn_fd, buf, BUF_SIZE - 1, 0);if (len == -1) {// 忽略 SIGURG 导致的中断(继续接收普通数据)if (errno == EINTR) continue;perror("recv normal data failed");break;} else if (len == 0) {printf("【客户端断开连接】\n");break;}printf("【普通数据接收成功】:%s(字节数:%ld)\n", buf, len);// 客户端输入 quit 时退出子线程if (strcmp(buf, "quit") == 0) break;}pthread_exit(NULL);
}int main() {int listen_fd;struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);pthread_t recv_thread;// 创建套接字listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1) { perror("socket failed"); exit(1); }// 绑定端口并监听int opt = 1;setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));listen(listen_fd, 3);printf("服务器启动,监听端口 %d...\n", PORT);// 接受客户端连接conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &addr_len);if (conn_fd == -1) { perror("accept failed"); exit(1); }printf("客户端连接成功(conn_fd: %d)\n", conn_fd);// 关键:用 signal 绑定 SIGURG 信号处理函数(Signal Urgent(直译为 “紧急信号”))signal(SIGURG, sigurg_handler);// 设置套接字所有者,确保 SIGURG 信号投递到当前进程fcntl(conn_fd, F_SETOWN, getpid());// 创建子线程:接收普通数据(分离线程,自动回收资源)if (pthread_create(&recv_thread, NULL, recv_normal_data, NULL) != 0) {perror("pthread_create failed");exit(1);}pthread_detach(recv_thread);// 主线程挂起,等待信号或子线程结束(可添加其他逻辑)pause();// 关闭资源close(conn_fd);close(listen_fd);return 0;
}
带外数据客户端(发送 OOB)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 8889
#define OOB_SIZE 1
#define BUF_SIZE 1024int main() {int sockfd;struct sockaddr_in server_addr;char input[BUF_SIZE];// 创建套接字并连接服务器sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) { perror("socket failed"); exit(1); }memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地测试IPif (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect server failed");exit(1);}// 操作说明printf("连接服务器成功!\n");printf("操作说明:\n1. 直接输入内容 → 发送普通数据\n2. 输入 oob:X → 发送OOB数据(X为单个字符)\n3. 输入 quit → 退出\n");// 交互发送数据while (1) {printf("\n请输入:");memset(input, 0, BUF_SIZE);fgets(input, BUF_SIZE, stdin);input[strcspn(input, "\n")] = '\0'; // 去除换行符// 发送 OOB 数据(格式:oob:X)if (strncmp(input, "oob:", 4) == 0) {if (strlen(input) != 5) { // 必须是 oob:单个字符(共5个字符)printf("错误:OOB格式应为 oob:X(如 oob:!)\n");continue;}char oob_char = input[4];send(sockfd, &oob_char, OOB_SIZE, MSG_OOB);printf("【OOB 发送成功】数据:%c\n", oob_char);}// 退出else if (strcmp(input, "quit") == 0) {send(sockfd, input, strlen(input), 0);printf("退出客户端\n");break;}// 发送普通数据else {send(sockfd, input, strlen(input), 0);printf("【普通数据发送成功】:%s\n", input);}}close(sockfd);return 0;
}
TCP 套接字超时控制
超时控制用于避免套接字因长期无数据或无连接而阻塞,通过 setsockopt 函数设置超时属性,支持接收超时和发送超时。

实现要点
- 前置准备:包含 sys/socket.h、sys/time.h、errno.h 等核心头文件;创建 TCP 套接字并校验有效性;服务器场景可选设置地址复用属性,避免端口占用。
- 参数校验:确认超时时间参数合法性,秒数需≥0,微秒数需在 0~999999 范围内(超过需进位到秒级),非法参数直接返回错误。
- 配置超时载体:根据场景选择载体,接收 / 发送超时使用 struct timeval 结构体,分别赋值秒级(tv_sec)和微秒级(tv_usec)时间;Accept 超时需初始化 select 读集合,将监听套接字加入集合。
- 生效超时设置:接收 / 发送超时通过 setsockopt 函数,指定 SOL_SOCKET 层级和对应选项(SO_RCVTIMEO 接收 / SO_SNDTIMEO 发送),传入超时结构体;Accept 超时通过 select 函数,绑定读集合和超时时间结构体。
- 把握调用时机:接收 / 发送超时需在套接字连接建立后(客户端 connect 后、服务器 accept 后)调用;Accept 超时需在服务器绑定端口并开始监听后,主循环等待新连接时调用。
- 超时判断与处理:调用 recv/send/accept 后,通过返回值和 errno 区分超时与真错误;接收 / 发送超时 errno 为 EAGAIN 或 EWOULDBLOCK,Accept 超时表现为 select 返回 0;超时后可根据业务需求选择重试、关闭连接或提示用户。
#include <sys/time.h> // 必须包含的头文件// 时间结构体:秒 + 微秒
struct timeval {time_t tv_sec; /* 秒数(seconds),整数类型 */suseconds_t tv_usec; /* 微秒数(microseconds),整数类型,范围 0~999999 */
};
超时机制说明
- 超时时间设置为 0 时,操作永不超时(默认行为)
- 仅对套接字的 I/O 操作(recv、send、accept 等)生效,对 select、poll 等函数无效
- 非阻塞模式下,超时设置仍有效,超时后函数立即返回错误
TCP 客户端 - 服务器基础通信示例(阻塞模式)
服务器端代码(多线程处理多客户端)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#define PORT 8888 // 服务器监听端口
#define BUF_SIZE 1024 // 数据缓冲区大小
#define MAX_CONN 10 // 最大并发连接数
// 线程处理函数:与单个客户端通信
void *client_handler(void *arg) {int conn_fd = *(int *)arg; // 已连接套接字描述符free(arg); // 释放传入的动态内存char buf[BUF_SIZE];ssize_t recv_len, send_len;// 设置线程分离,自动释放资源(无需主线程pthread_join)pthread_detach(pthread_self());printf("客户端已连接,开始通信(conn_fd: %d)\n", conn_fd);while (1) {// 接收客户端数据memset(buf, 0, BUF_SIZE); // 清空缓冲区recv_len = recv(conn_fd, buf, BUF_SIZE - 1, 0); // 留1字节存'\0'if (recv_len == -1) {perror("recv failed");break;} else if (recv_len == 0) {printf("客户端主动断开连接(conn_fd: %d)\n", conn_fd);break;}printf("收到客户端数据(conn_fd: %d):%s\n", conn_fd, buf);// 回复客户端(echo模式:原样返回数据)send_len = send(conn_fd, buf, recv_len, 0);if (send_len == -1) {perror("send failed");break;}printf("已回复客户端(conn_fd: %d):%s\n", conn_fd, buf);// 若客户端发送"quit",主动断开连接if (strcmp(buf, "quit") == 0) {printf("客户端请求退出(conn_fd: %d)\n", conn_fd);break;}}// 关闭连接套接字close(conn_fd);printf("连接已关闭(conn_fd: %d)\n", conn_fd);return NULL;
}int main() {int listen_fd, *conn_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len = sizeof(client_addr);pthread_t tid;// 创建TCP套接字listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1) {perror("socket create failed");exit(EXIT_FAILURE);}printf("套接字创建成功(listen_fd: %d)\n", listen_fd);// 设置地址复用(避免端口占用错误)int opt = 1;if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {perror("setsockopt SO_REUSEADDR failed");close(listen_fd);exit(EXIT_FAILURE);}// 绑定IP和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET; // IPv4协议族server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地IPserver_addr.sin_port = htons(PORT); // 端口号转换为网络字节序if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(listen_fd);exit(EXIT_FAILURE);}printf("已绑定端口 %d,等待客户端连接...\n", PORT);// 开始监听连接if (listen(listen_fd, MAX_CONN) == -1) {perror("listen failed");close(listen_fd);exit(EXIT_FAILURE);}// 循环接受客户端连接(服务器核心逻辑)while (1) {// 动态分配conn_fd(避免线程间共享栈内存)conn_fd = (int *)malloc(sizeof(int));if (conn_fd == NULL) {perror("malloc failed");continue;}// 接受连接(阻塞,直到有客户端连接)*conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (*conn_fd == -1) {perror("accept failed");free(conn_fd);continue;}// 创建线程处理该客户端通信if (pthread_create(&tid, NULL, client_handler, conn_fd) != 0) {perror("pthread_create failed");close(*conn_fd);free(conn_fd);continue;}}// 实际不会执行到这里(需信号处理退出)close(listen_fd);return 0;
}
客户端代码(带超时控制)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1" // 服务器IP(本地测试用回环地址)
#define SERVER_PORT 8888 // 服务器端口(需与服务器一致)
#define BUF_SIZE 1024 // 数据缓冲区大小
#define RECV_TIMEOUT_SEC 5
// 接收超时时间(5秒)// 设置接收超时(复用之前的超时控制函数)
int set_recv_timeout(int sockfd, int sec, int usec) {struct timeval timeout;timeout.tv_sec = sec;timeout.tv_usec = usec;return setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
}int main() {int sock_fd;struct sockaddr_in server_addr;char send_buf[BUF_SIZE], recv_buf[BUF_SIZE];ssize_t send_len, recv_len;// 创建TCP套接字sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("socket create failed");exit(EXIT_FAILURE);}printf("客户端套接字创建成功(sock_fd: %d)\n", sock_fd);// 设置接收超时(5秒)if (set_recv_timeout(sock_fd, RECV_TIMEOUT_SEC, 0) == -1) {perror("set recv timeout failed");close(sock_fd);exit(EXIT_FAILURE);}// 配置服务器地址信息memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;// IP地址转换:点分十进制字符串 → 网络字节序if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {perror("invalid server IP");close(sock_fd);exit(EXIT_FAILURE);}server_addr.sin_port = htons(SERVER_PORT); // 端口号转换// 连接服务器(阻塞,直到连接成功或失败)if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("connect server failed");close(sock_fd);exit(EXIT_FAILURE);}printf("已连接服务器 %s:%d\n", SERVER_IP, SERVER_PORT);printf("输入消息发送给服务器(输入quit退出):\n");// 循环发送/接收数据while (1) {// 读取用户输入memset(send_buf, 0, BUF_SIZE);if (fgets(send_buf, BUF_SIZE, stdin) == NULL) {perror("fgets failed");break;}// 去除fgets读取的换行符(避免服务器收到多余字符)send_buf[strcspn(send_buf, "\n")] = '\0';// 发送数据到服务器send_len = send(sock_fd, send_buf, strlen(send_buf), 0);if (send_len == -1) {perror("send failed");break;}printf("已发送:%s(字节数:%ld)\n", send_buf, send_len);// 若发送quit,退出循环if (strcmp(send_buf, "quit") == 0) {printf("客户端请求退出\n");break;}// 接收服务器回复(超时5秒)memset(recv_buf, 0, BUF_SIZE);recv_len = recv(sock_fd, recv_buf, BUF_SIZE - 1, 0);if (recv_len == -1) {perror("recv failed(可能超时)");break;} else if (recv_len == 0) {printf("服务器已断开连接\n");break;}printf("收到服务器回复:%s(字节数:%ld)\n", recv_buf, recv_len);}// 关闭套接字close(sock_fd);printf("客户端已退出,连接关闭\n");return 0;
}