当前位置: 首页 > news >正文

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

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

#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套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);  
if (server_fd == -1) {  perror("socket创建失败");  exit(1);  
}  

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

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

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
参数说明:
  • sockfdsocket() 返回的套接字描述符。
  • addr:指向地址结构体的指针(需填充IP、端口等信息)。
    • IPv4使用 struct sockaddr_in(需强制转换为 sockaddr*):
    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()

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

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

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

4. 接受连接:accept()

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

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

5. 客户端连接:connect()

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

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
参数说明:
  • sockfd:客户端创建的套接字描述符。
  • addr:服务器的地址信息(需填充服务器IP和端口,端口和IP需转换为网络字节序)。
  • addrlen:服务器地址结构体的大小。
示例:客户端连接服务器
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四次挥手。

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

三、核心基础概念

1. 网络字节序

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

函数功能适用场景
htons()主机字节序 → 网络字节序(16位)端口号转换(short
htonl()主机字节序 → 网络字节序(32位)IP地址转换(int
ntohs()网络字节序 → 主机字节序(16位)解析端口号
ntohl()网络字节序 → 主机字节序(32位)解析IP地址
示例:端口和IP转换
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)现代推荐
现代用法示例:
// 字符串→二进制(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 的实际数据。
    // 发送"hello"(长度5)  
    int len = htonl(5);  // 长度转换为网络字节序  
    send(fd, &len, 4, 0);       // 先发送长度  
    send(fd, "hello", 5, 0);    // 再发送数据  
    
  2. 接收方

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

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

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

  1. 头文件和库

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

    // 初始化套接字库  
    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复用等技术实现高效的多客户端通信。这些基础是网络编程的核心。

http://www.dtcms.com/a/283936.html

相关文章:

  • 微信小程序列表数据上拉加载,下拉刷新
  • ASP .NET Core 8实现实时Web功能
  • Python 网络爬虫 —— 提交信息到网页
  • AI算法之图像识别与分类
  • 电力载波通信技术(PLC)发展全解析:从历史演进到未来趋势
  • RabbitMQ概述和工作模式
  • 永磁同步电机MTPA与MTPV曲线具体仿真实现
  • Python学习之——序列化与反序列化
  • 常用的100个opencv函数
  • [RAG] LLM 交互层 | 适配器模式 | 文档解析器(`docling`库, CNN, OCR, OpenCV)
  • 加速度传感器方向校准方法
  • RGBA图片格式转换为RGB格式(解决convert转换的失真问题)
  • OpenCV中VideoCapture 设置和获取摄像头参数和Qt设计UI控制界面详解代码示例
  • (四)OpenCV——特征点检测与匹配
  • 分布式分片策略中,分片数量的评估与选择
  • MacOS安装linux虚拟机
  • GPU的barrier
  • OpenCV中常用特征提取算法(SURF、ORB、SIFT和AKAZE)用法示例(C++和Python)
  • Linux的Ext系列文件系统
  • 一文掌握Harbor的配额管理和GC机制
  • Kubernetes架构原理与集群环境部署
  • VMware Workstation Pro 17下载安装
  • C++ AVL树实现详解:平衡二叉搜索树的原理与代码实现
  • [yotroy.cool] 记一次 spring boot 项目宝塔面板部署踩坑
  • LeetCode|Day16|387. 字符串中的第一个唯一字符|Python刷题笔记
  • 高光谱相机(Hyperspectral Camera)
  • 虚拟内存管理-抖动和工作集
  • 告别手动报表开发!描述数据维度,AI 自动生成 SQL 查询 + Java 导出接口
  • Python暑期学习笔记3
  • 100201组件拆分_编辑器-react-仿低代码平台项目