解码TCP

news/2025/11/23 17:47:03/文章来源:https://www.cnblogs.com/YouEmbedded/p/19248155

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 字节。

报头核心字段

image

各字段详细说明

  • 源端口号(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 通过三次报文交换建立可靠连接,确保双方序列号同步和通信能力确认:

image

  • 客户端(主动连接端)发送 SYN 报文:包含客户端初始序列号 ISNc,进入 SYN-SENT 状态
  • 服务器(被动连接端)回复 SYN+ACK 报文:包含服务器初始序列号 ISNs 和对客户端 ISNc 的确认(ISNc+1),进入 SYN-RECEIVED 状态
  • 客户端发送 ACK 报文:确认服务器 ISNs(ISNs+1),进入 ESTABLISHED 状态;服务器收到后也进入 ESTABLISHED 状态,连接建立完成

注意:仅第三次握手的报文可以携带数据,前两次握手仅用于连接协商

四次挥手机制(连接断开过程)

TCP 连接是全双工的,需双方分别关闭发送通道,通过四次报文交换完成断开:

image

  • 主动关闭方发送 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 有限状态机

image

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 通信中,发送端和接收端均设有内核缓冲区,用于临时存储数据,协调应用层与网络层的传输速度差异。

image

接收缓冲区

  • 作用:暂存从网络接收的数据,等待应用层调用 recv 函数读取
  • 大小范围:Linux 系统默认 2304-425984 字节,不同主机可能不同
  • 核心特性:
    • 缓冲区大小可通过 setsockopt 函数设置,需在 listen 或 connect 前设置才生效

    • 系统会自动将设置的缓冲区大小加倍(预留管理空间),getsockopt 返回的是加倍后的值

    • 水位线机制:默认最小水位线为 1 字节,接收数据量超过水位线即触发读就绪,应用层可读取

      image

      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);

image

发送缓冲区

  • 作用:暂存应用层发送的数据,等待网络层发送;数据发送失败时用于重传
  • 大小范围: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 函数设置超时属性,支持接收超时和发送超时。

image

实现要点

  • 前置准备:包含 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/974090.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

死亡笔记 (Wordpress cms渗透)

死亡笔记 (Wordpress cms渗透)cms1.内容管理系统(content management system,CMS),是一种位于WEB前端(Web 服务器)和后端办公系统或流程(内容创作、编辑)之间的软件系统。内容的创作人员、编辑人员、发布人员…

iso 安装linux

在 Linux 系统中,使用 ISO 安装 Linux 通常涉及以下几个步骤:一、准备 ISO 文件下载 ISO 文件:从官方 Linux 发行版网站(如 Ubuntu、Debian、CentOS、Fedora 等)下载 ISO 文件。也可以使用 wget 或 curl 命令下载…

isnotnull在oracle中的语法和使用技巧

在Oracle数据库中,IS NOT NULL是一个用于检查字段值是否为NULL的条件表达式。以下是关于其在Oracle中的语法和使用技巧的详细解释: 语法IS NOT NULL的语法结构相对简单,直接应用于某个字段名或表达式,以判断其是否…

2025最新东莞AI搜索优化、GEO优化服务商TOP5评测:引领企业AI搜索增长新范式

随着AI搜索技术的飞速发展,GEO优化已成为企业提升品牌曝光和市场竞争力的关键。本榜单基于技术实力、服务经验、客户满意度及行业影响力四大维度,结合本地企业实际需求,综合评选出2025年东莞地区五大GEO优化服务商,…

2025东莞最新数字人克隆厂商TOP5评测,客服数字人克隆 老板IP数字人克隆定制,全场景落地服务商行业口碑榜,专业选择指南。

随着数字人技术在客服、IP运营等场景的深度渗透,市场对专业化克隆服务的需求持续攀升。本榜单基于技术还原度、场景适配性、定制化能力三大核心维度,结合行业案例落地效果与客户满意度调研,权威发布2025年五大数字人…

P14225 [ICPC 2024 Kunming I] 左移 2 个人题解

题目传送门 题目大意: 给定一个字符串 \(s\),进行一次左移,即使字符串 \(s\) 变为 \(s_{(d+0)\bmod n},s_{(d+1)\bmod n},\cdots,s_{(d+n-1)\bmod n}\),然后求最少更改几个字符可以变成美丽字符串(即使字符串内相…

【URP】Unity[相机]渲染顺序

URP相机渲染流程核心机制‌ ‌基础渲染管线顺序‌ ‌Depth Pre-Pass‌:可选深度预渲染(需手动开启) ‌Opaque Rendering‌:不透明物体从近到远排序渲染 ‌Skybox D【从UnityURP开始探索游戏渲染】专栏-直达URP相机…

PySpark - OneHotEncoder

PySpark - OneHotEncoder from pyspark.ml.feature import OneHotEncoderdata = [(0.0, 1.0), (1.0, 0.0), (2.0, 1.0)] columns = ["input1", "input2"] df = spark.createDataFrame(data, colum…

.NET 10 中 C# 14 和 F# 10 的新情况

C# 14 和 F# 10 将随 .NET 10 正式发布,这两个微软主力.NET语言的更新聚焦于生产力、代码清晰度和性能提升。本次发布通过协调一致的改进,优化了常见编码模式,同时实现了整个.NET工具链的增强。C#14 亮点C# 14版本增…

P10687 True Liars 个人题解

题目传送门 题目大意: 给你两个神圣种族和邪恶种族的人数以及询问次数,其中神圣种族的人说真话,邪恶种族的人说假话,请你判断那几个是神圣种族的人。 Solution: 题解区已经有很多带边权并查集的做法了,这里我用的…

Kali资料

Kali资料解决Kali Linux APT更新中的GPG签名错误问题

题解:Luogu P14522 【MX-S11-T3】空之碎物

题意 定义 \(\ominus\) 为二进制不退位减法。对于一个可重集 \(S\),你可以进行若干次操作,每次操作可以选择 \(S\) 中的两个数 \(x,y\),合并成 \(x\ominus y\) 或 \(y\ominus x\)。定义 \(f(S)\) 为将 \(S\) 合并至…

10分钟,无需公网 IP!零门槛搭建 NapCatQQ 趣味 AI 人机,聊天互动超简单

10分钟,无需公网 IP!零门槛搭建 NapCatQQ 趣味 AI 人机,聊天互动超简单无需公网 IP 即可打造 QQ 智能人机:核心依赖 NapCat(接收 QQ 消息)与 AstrBot(提供 AI 能力)容器,通过 WebSocket 建立连接,配置硅基流…

1088. Rational Arithmetic (20)

1088. Rational Arithmetic (20)#include <iostream>using namespace std;long long getsame(long long a, long long b) {if(b != 0){return getsame(b, a % b);}else{return a;} }void simplify(long long &am…

1087. All Roads Lead to Rome (30)

1087. All Roads Lead to Rome (30)#include <iostream> #include <vector> #include <string.h>using namespace std;struct node {int next, cost; };vector<node> v[27000]; vector<int…

解码UDP

UDP 协议基础认知 UDP(User Datagram Protocol,用户数据报协议)是传输层核心协议之一,基于 IP 协议实现跨网络主机进程间的无连接数据传输。它面向事务提供简单通信服务,不保证数据交付、有序性和重复防护,也不提…

人工智能之数据分析 numpy:第六章 数组基本操作

人工智能之数据分析 numpy:第六章 数组基本操作人工智能之数据分析 numpy 第六章 数组基本操作@目录人工智能之数据分析 numpy前言一、修改数组形状(Reshaping)1. reshape()2. resize()3. ravel() 与 flatten()二、…

2025中山办公场地租赁优选:中山西区金嘉创新港,一站式创业空间,赋能企业成长新机遇

随着中山市产业升级与创新创业浪潮的蓬勃发展,优质办公空间已成为企业发展的重要基石。在2025年中山商业地产市场中,中山西区金嘉创新港凭借多元化的空间解决方案、完善的配套服务体系及卓越的区位优势,成为各类企业…

国产数据库替代MongoDB:政务电子证照新选择 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

读书笔记《投资的未来》,估算收益率

比较IBM和新泽西标准石油两家公司,一个新兴的,受追捧的,一个传统的,但结果,是石油公司胜出。尽管两只股票的业绩都不错,但是1950~2003年,新泽西标准石油的投资者每年可以取得14.42%的年收益率,这比IBM提供的1…