C++网络编程 2.TCP套接字(socket)编程详解

TCP套接字编程详解:从API到实战细节

一、TCP通信整体流程

TCP是面向连接的可靠传输协议,其通信流程需严格遵循"服务器绑定监听→客户端连接→数据交互→断开连接"的步骤。以下是服务器和客户端的核心流程对比:

角色 核心流程(常用API)
服务器 创建套接字 socket() → 绑定地址端口 bind() → 监听连接 listen() → 接受连接 accept() → 数据交互 read()/write() → 关闭连接 close()
客户端 创建套接字 socket() → 连接服务器 connect() → 数据交互 read()/write() → 关闭连接 close()

二、核心API详解(Linux环境)

1. 创建套接字:socket()

功能:创建一个网络套接字(文件描述符),用于后续网络通信。

c 复制代码
#include <sys/socket.h>  
int socket(int domain, int type, int protocol);  
参数说明:
  • domain(协议族) :指定网络协议类型
    • AF_INET:IPv4协议(最常用);AF_INET6:IPv6协议;AF_UNIX:本地套接字(进程间通信)。
  • type(套接字类型) :指定传输方式
    • SOCK_STREAM:流式套接字(对应TCP协议,可靠连接);
    • SOCK_DGRAM:数据报套接字(对应UDP协议,无连接)。
  • protocol(协议) :一般填 0,表示使用 type 对应的默认协议(TCP用 IPPROTO_TCP,UDP用 IPPROTO_UDP,但通常省略)。
示例:创建TCP套接字
c 复制代码
int server_fd = socket(AF_INET, SOCK_STREAM, 0);  
if (server_fd == -1) {  
    perror("socket创建失败");  
    exit(1);  
}  

2. 绑定地址和端口:bind()

功能:将套接字与本地IP地址和端口号绑定(服务器必须绑定,客户端通常由系统自动分配端口)。

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
参数说明:
  • sockfdsocket() 返回的套接字描述符。

  • addr :指向地址结构体的指针(需填充IP、端口等信息)。

    • IPv4使用 struct sockaddr_in(需强制转换为 sockaddr*):
    c 复制代码
    struct sockaddr_in serv_addr;  
    serv_addr.sin_family = AF_INET;                  // 协议族(IPv4)  
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);   // 绑定所有本地IP(INADDR_ANY)  
    serv_addr.sin_port = htons(8080);                // 端口号(需转换为网络字节序)  
  • addrlen :地址结构体的大小(sizeof(struct sockaddr_in))。

关键细节:
  • INADDR_ANY:表示绑定本地所有网络接口的IP(如服务器有多个网卡时,可接收任意网卡的连接)。
  • 端口号范围:0~65535,其中 0~1023 是知名端口(如HTTP 80),建议使用 1024~65535 避免冲突。

3. 监听连接:listen()

功能:将套接字设置为监听状态,允许接收客户端连接请求。

c 复制代码
int listen(int sockfd, int backlog);  
参数说明:
  • sockfd:已绑定的套接字描述符(监听套接字)。
  • backlog :未完成连接队列的最大长度(处于 SYN_RCVD 状态的连接数),超过则新连接被拒绝(Linux通常建议设为 5~10,具体受系统限制)。
注意:

listen() 仅标记套接字为监听状态,不主动接收连接,需配合 accept() 使用。

4. 接受连接:accept()

功能 :从监听队列中取出一个已完成的连接请求,返回一个新的套接字描述符(用于与该客户端通信)。

c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  
参数说明:
  • sockfd :监听套接字描述符(listen() 后的套接字)。
  • addr:输出参数,存储客户端的IP和端口信息(需提前分配空间)。
  • addrlen :输入输出参数,传入 addr 大小,返回实际存储的地址长度。
核心特性:
  • 阻塞性 :若没有客户端连接,accept() 会一直阻塞等待。
  • 新套接字:返回的新描述符是"通信套接字",用于与客户端收发数据;原监听套接字仍继续监听新连接。

5. 客户端连接:connect()

功能:客户端向服务器发起连接请求,完成TCP三次握手。

c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
参数说明:
  • sockfd:客户端创建的套接字描述符。
  • addr:服务器的地址信息(需填充服务器IP和端口,端口和IP需转换为网络字节序)。
  • addrlen:服务器地址结构体的大小。
示例:客户端连接服务器
c 复制代码
struct sockaddr_in serv_addr;  
serv_addr.sin_family = AF_INET;  
serv_addr.sin_port = htons(8080);  
// 将点分十进制IP转换为网络字节序二进制  
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);  

// 发起连接  
if (connect(client_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {  
    perror("连接失败");  
    exit(1);  
}  

6. 数据交互:send()/recv()read()/write()

TCP通信中,数据通过套接字的读写缓冲区传输,常用函数如下:

函数 原型 功能
send() ssize_t send(int sockfd, const void *buf, size_t len, int flags); 发送数据到套接字缓冲区
recv() ssize_t recv(int sockfd, void *buf, size_t len, int flags); 从套接字缓冲区接收数据
write() ssize_t write(int fd, const void *buf, size_t count); 等同于 send(flags=0)
read() ssize_t read(int fd, void *buf, size_t count); 等同于 recv(flags=0)
参数说明:
  • sockfd/fd :通信套接字描述符(accept()connect() 返回的描述符)。
  • buf:数据缓冲区(发送时存数据,接收时存结果)。
  • len/count:数据长度(字节)。
  • flags :传输标志(通常为 0,表示默认行为)。
核心特性:
  • 阻塞性:若缓冲区满(发送)或空(接收),函数会阻塞等待。
  • 返回值 :成功返回实际传输的字节数;0 表示连接关闭;-1 表示出错。

7. 关闭连接:close()

功能:关闭套接字,释放资源,触发TCP四次挥手。

c 复制代码
int close(int fd);  
  • 服务器需关闭 通信套接字 (与客户端的连接)和 监听套接字(停止接收新连接)。
  • 客户端关闭套接字后,会向服务器发送 FIN 包,终止连接。

三、核心基础概念

1. 网络字节序

不同计算机的字节存储顺序(大端/小端)不同,TCP协议规定网络中必须使用 大端字节序(高位字节存低地址),需通过转换函数统一格式:

函数 功能 适用场景
htons() 主机字节序 → 网络字节序(16位) 端口号转换(short
htonl() 主机字节序 → 网络字节序(32位) IP地址转换(int
ntohs() 网络字节序 → 主机字节序(16位) 解析端口号
ntohl() 网络字节序 → 主机字节序(32位) 解析IP地址
示例:端口和IP转换
c 复制代码
uint16_t port = 8080;  
uint16_t net_port = htons(port);  // 主机→网络(端口)  

uint32_t ip = inet_addr("192.168.1.1");  // 点分十进制→网络字节序(IPv4)  

2. IP地址格式转换

IP地址在代码中需在"点分十进制字符串"和"二进制整数"间转换,常用函数:

函数 功能 适用协议
inet_addr() 点分十进制字符串 → 网络字节序整数 IPv4(过时)
inet_ntoa() 网络字节序整数 → 点分十进制字符串 IPv4(非线程安全)
inet_pton() 字符串 → 二进制(支持IPv4/IPv6) 现代推荐
inet_ntop() 二进制 → 字符串(支持IPv4/IPv6) 现代推荐
现代用法示例:
c 复制代码
// 字符串→二进制(IPv4)  
struct in_addr ip_bin;  
inet_pton(AF_INET, "127.0.0.1", &ip_bin);  

// 二进制→字符串(IPv4)  
char ip_str[INET_ADDRSTRLEN];  
inet_ntop(AF_INET, &ip_bin, ip_str, INET_ADDRSTRLEN);  
printf("IP: %s\n", ip_str);  // 输出 "127.0.0.1"  

3. 文件描述符与缓冲区

  • 服务器的两个套接字描述符
    • 监听套接字(listen() 后的fd):仅用于接收新连接,不参与数据交互。
    • 通信套接字(accept() 返回的fd):每个客户端连接对应一个,用于收发数据。
  • 缓冲区机制 :每个套接字对应内核中的 读缓冲区写缓冲区
    • 发送数据:send()/write() 将数据写入写缓冲区,内核异步发送到网络。
    • 接收数据:内核将网络数据存入读缓冲区,recv()/read() 从缓冲区读取。
    • 缓冲区满时:send() 阻塞(直到有空间);缓冲区空时:recv() 阻塞(直到有数据)。

四、TCP粘包问题及解决方案

什么是粘包?

TCP是流式传输,数据无边界,若发送方连续发送小数据包,接收方可能将多个数据包合并为一个"大数据块",导致无法区分边界(如发送"hi"和"jack",接收方可能收到"hijack")。

粘包原因

  • 发送方:数据太小,内核合并发送(Nagle算法)。
  • 接收方:数据到达速度快于处理速度,缓冲区累积多个数据包。

解决方案:固定格式分包

核心思路:先发送数据长度,再发送实际数据,接收方按长度解析。

步骤示例:
  1. 发送方

    • 将数据长度(len)转换为网络字节序,先发送 len(4字节整数)。
    • 再发送长度为 len 的实际数据。
    c 复制代码
    // 发送"hello"(长度5)  
    int len = htonl(5);  // 长度转换为网络字节序  
    send(fd, &len, 4, 0);       // 先发送长度  
    send(fd, "hello", 5, 0);    // 再发送数据  
  2. 接收方

    • 先读取4字节获取数据长度 len(转换为主机字节序)。
    • 再读取 len 字节的实际数据。
    c 复制代码
    int len;  
    recv(fd, &len, 4, 0);       // 先读长度  
    len = ntohl(len);           // 转换为主机字节序  
    char buf[len];  
    recv(fd, buf, len, 0);      // 再读实际数据  

五、跨平台差异(Windows环境)

Windows套接字编程与Linux有以下核心区别:

  1. 头文件和库

    c 复制代码
    #include <winsock2.h>       // 核心头文件  
    #pragma comment(lib, "ws2_32.lib")  // 链接套接字库  
  2. 初始化和清理

    c 复制代码
    // 初始化套接字库  
    WSADATA wsaData;  
    WSAStartup(MAKEWORD(2, 2), &wsaData);  
    
    // 程序结束时清理  
    WSACleanup();  
  3. 函数差异

    • 关闭套接字用 closesocket() 而非 close()
    • 错误码获取用 WSAGetLastError() 而非 errno

六、高频面试考点总结

  1. TCP通信核心流程

    • 服务器:socket() 创建套接字 → bind() 绑定IP和端口 → listen() 监听连接 → accept() 阻塞等待客户端连接 → read()/write() 数据交互 → close() 关闭连接。
    • 客户端:socket() 创建套接字 → connect() 发起连接 → read()/write() 数据交互 → close() 关闭连接。
  2. INADDR_ANY 的作用

    绑定服务器本地所有网络接口的IP地址(如服务器有多个网卡时),使服务器能接收来自任意网卡的连接请求,无需硬编码具体IP,增强灵活性。

  3. listen()backlog 的含义

    表示未完成连接队列(处于 SYN_RCVD 状态)的最大长度,超过该值的新连接会被拒绝。实际有效长度可能受系统内核参数(如 net.core.somaxconn)限制。

  4. accept() 的特性

    • 阻塞性:无新连接时会一直阻塞,直到有客户端连接或被信号中断。
    • 返回新套接字:accept() 返回的是与客户端通信的套接字,原监听套接字仍可继续接收其他连接(实现多客户端通信需多线程/多进程)。
  5. 网络字节序与转换函数

    • 网络字节序为 大端字节序,主机字节序可能为大端或小端(取决于CPU架构)。
    • 必须转换的场景:端口号(htons()/ntohs())、IP地址(htonl()/ntohl())。
    • 现代推荐用 inet_pton()/inet_ntop() 转换IP地址(支持IPv4/IPv6,线程安全)。
  6. TCP粘包问题

    • 原因:TCP是流式传输,无数据边界,多个小数据包可能被合并传输。
    • 解决方案:长度前缀法(先发送数据长度,再发送实际数据,接收方按长度解析)。
  7. 套接字缓冲区机制

    • 每个套接字对应内核中的读缓冲区和写缓冲区,数据通过缓冲区间接传输。
    • send()/write() 成功仅表示数据写入写缓冲区,不代表已发送到对端;recv()/read() 从读缓冲区取数据,缓冲区为空时阻塞。
  8. 文件描述符的角色

    • 服务器有两类描述符:监听描述符 (负责接收连接)和 通信描述符(每个客户端一个,负责数据交互)。
    • 描述符是内核管理套接字的索引,关闭描述符会触发TCP四次挥手释放连接。

总结

TCP套接字编程的核心是理解"连接建立→数据交互→连接关闭"的全流程,掌握 socket()/bind()/listen()/accept()/connect() 等API的参数和特性,以及网络字节序、粘包处理等细节。实际开发中需注意跨平台差异(如Windows的 WSAStartup()),并通过多线程/IO复用等技术实现高效的多客户端通信。这些基础是网络编程的核心。

相关推荐
tomato091 分钟前
河南萌新联赛2025第(一)场:河南工业大学(补题)
c++·算法
baynk1 小时前
wireshark的常用用法
网络·测试工具·wireshark·ctf
快乐觉主吖1 小时前
Unity网络通信的插件分享,及TCP粘包分包问题处理
tcp/ip·unity·游戏引擎
莫到空离1 小时前
LVS三种模式实战
linux·服务器·网络
云计算运维-小白白2 小时前
基于阿里云云服务器-局域网组网软件
运维·服务器·网络
guganly2 小时前
H3C防火墙基于VRF和路由复制实现AWS DX多租户隔离配置手册
网络·云计算·aws
每一天都要努力^3 小时前
C++拷贝构造
开发语言·c++
上海云盾-高防顾问3 小时前
电商行业如何做好网络安全工作?
网络·安全·web安全
cui_win3 小时前
深入理解 Kafka 核心:主题、分区与副本的协同机制
网络·分布式·kafka
iblade3 小时前
网络:TCP序列号和滑动窗口,顺序保证
网络·tcp/ip·php