解码TCP

深入解析TCP协议核心机制

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 1024

int 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 1024

int 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");  // 本地测试IP

    if (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; // 绑定所有本地IP
    server_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;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值