C++ 网络编程详解(全集超详细)
一、网络编程基础
- 计算机网络体系结构
OSI七层模型
OSI(Open Systems Interconnection)七层模型是一个理论上的网络通信框架,由国际标准化组织(ISO)提出。它将网络通信分为七个层次,每一层都有特定的功能和协议:
- 物理层(Physical Layer)
- 负责传输原始比特流(0和1)。
- 定义物理介质(如电缆、光纤)的特性,如电压、传输速率等。
- 典型协议:Ethernet(物理层部分)、RS-232。
- 数据链路层(Data Link Layer)
- 将比特流组织成帧(Frame),并进行错误检测(如CRC校验)。
- 管理物理地址(MAC地址)和局域网(LAN)通信。
- 典型协议:Ethernet(MAC层)、PPP。
- 网络层(Network Layer)
- 负责数据包的路由和转发,实现不同网络之间的通信。
- 使用逻辑地址(如IP地址)标识设备。
- 典型协议:IP、ICMP、ARP。
- 传输层(Transport Layer)
- 提供端到端的数据传输服务,确保数据的可靠性和顺序。
- 支持流量控制和错误恢复。
- 典型协议:TCP(可靠传输)、UDP(不可靠传输)。
- 会话层(Session Layer)
- 管理通信会话的建立、维护和终止。
- 支持同步和数据交换控制。
- 典型协议:NetBIOS、RPC。
- 表示层(Presentation Layer)
- 处理数据的格式转换(如加密、压缩、编码)。
- 确保不同系统的数据格式兼容。
- 典型协议:SSL/TLS(加密)、JPEG(图像编码)。
- 应用层(Application Layer)
- 直接为用户应用程序提供网络服务(如文件传输、电子邮件)。
- 典型协议:HTTP、FTP、SMTP。
TCP/IP四层模型
TCP/IP模型是实际应用中广泛使用的网络协议栈,由四层组成:
- 网络接口层(Network Interface Layer)
- 对应OSI的物理层和数据链路层。
- 负责数据帧的传输和物理介质访问。
- 典型协议:Ethernet、Wi-Fi(IEEE 802.11)。
- 网络层(Internet Layer)
- 对应OSI的网络层。
- 处理数据包的路由和寻址(IP地址)。
- 典型协议:IP、ICMP、ARP。
- 传输层(Transport Layer)
- 对应OSI的传输层。
- 提供端到端的数据传输服务(可靠或不可靠)。
- 典型协议:TCP、UDP。
- 应用层(Application Layer)
- 对应OSI的应用层、表示层和会话层。
- 直接为用户提供网络服务。
- 典型协议:HTTP、FTP、DNS、SMTP。
主要区别
- 层次划分
- OSI是理论模型(7层),TCP/IP是实用模型(4层)。
- TCP/IP的应用层合并了OSI的应用层、表示层和会话层。
- 协议支持
- OSI是通用标准,TCP/IP是实际实现的协议栈(如互联网基础)。
- 适用范围
- OSI用于教学和理论分析,TCP/IP用于实际网络通信(如互联网)。
各层协议概述
HTTP (Hypertext Transfer Protocol)
- 定义:HTTP 是一种应用层协议,用于在客户端和服务器之间传输超文本(如网页)。
- 特点:
- 无状态:每次请求独立,服务器不保存客户端状态(但可通过 Cookie/Session 实现状态管理)。
- 基于请求-响应模型:客户端发送请求,服务器返回响应。
- 常用方法:
GET(获取资源)、POST(提交数据)、PUT(更新资源)、DELETE(删除资源)。 - 默认端口:80(HTTP)、443(HTTPS)。
- 版本:
- HTTP/1.1:支持持久连接(Keep-Alive)。
- HTTP/2:多路复用、头部压缩。
- HTTP/3:基于 QUIC 协议(UDP 实现)。
TCP (Transmission Control Protocol)
- 定义:TCP 是传输层协议,提供可靠的、面向连接的字节流传输服务。
- 特点:
- 可靠性:通过确认应答(ACK)、超时重传、流量控制(滑动窗口)、拥塞控制(慢启动、拥塞避免)保证数据不丢失、不重复、有序。
- 面向连接:通信前需三次握手建立连接,结束时四次挥手释放连接。
- 全双工:双方可同时收发数据。
- 头部开销:20 字节(不含选项字段)。
- 适用场景:文件传输、网页浏览等需要可靠传输的服务。
IP (Internet Protocol)
- 定义:IP 是网络层协议,负责将数据包从源主机路由到目标主机。
- 特点:
- 无连接:不预先建立连接,每个数据包独立路由。
- 不可靠:不保证数据包到达、不保证顺序(依赖上层协议如 TCP 纠错)。
- 地址标识:使用 IP 地址(IPv4 32 位,IPv6 128 位)唯一标识主机。
- 分片与重组:根据 MTU(最大传输单元)拆分数据包,目标主机重组。
- 版本:
- IPv4:32 位地址,如
192.168.1.1。 - IPv6:128 位地址,如
2001:0db8::ff00:0042。
- IPv4:32 位地址,如
协议间关系
- 分层协作:HTTP 依赖 TCP 提供可靠传输,TCP 依赖 IP 实现路由。
- 数据封装:HTTP 数据 → TCP 段 → IP 数据包 → 物理帧。
- 套接字(Socket)基础
Socket概念
Socket(套接字)是网络通信的基本操作单元,是支持TCP/IP协议的网络通信的基本操作单元。它是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字由一个IP地址和一个端口号唯一标识。
Socket类型
流式套接字(SOCK_STREAM)
- 特点:
- 提供面向连接的、可靠的数据传输服务
- 数据无差错、无重复地发送,且按发送顺序接收
- 基于TCP协议实现
- 传输的数据是字节流,没有长度限制
- 典型应用:
- 需要可靠传输的应用,如文件传输、网页浏览等
- 需要按顺序接收数据的应用
- 工作流程:
- 服务器创建套接字
- 绑定IP和端口
- 监听连接
- 接受客户端连接
- 进行数据收发
- 关闭连接
数据报套接字(SOCK_DGRAM)
- 特点:
- 提供无连接的服务
- 不保证可靠传输,数据可能丢失或重复
- 不保证数据按发送顺序到达
- 基于UDP协议实现
- 传输的是数据报(消息),有长度限制
- 典型应用:
- 实时性要求高但可靠性要求不高的应用,如视频会议、在线游戏
- 广播/多播应用
- DNS查询等简单查询/应答应用
- 工作流程:
- 创建套接字
- 绑定IP和端口(可选)
- 直接发送/接收数据
- 关闭套接字
两种套接字的主要区别
- 连接方式:
- 流式套接字需要建立连接
- 数据报套接字不需要连接
- 可靠性:
- 流式套接字保证数据可靠传输
- 数据报套接字不保证可靠性
- 传输单位:
- 流式套接字传输的是字节流
- 数据报套接字传输的是独立的数据报
- 传输效率:
- 数据报套接字通常效率更高
- 流式套接字由于需要建立连接和维护可靠性,开销较大
Socket地址结构
Socket地址结构是用来表示网络通信中端点地址的数据结构。在C++网络编程中,最常用的是sockaddr及其相关结构。
sockaddr
sockaddr是一个通用的地址结构,定义如下:
structsockaddr{unsignedshort sa_family;// 地址家族,如AF_INETchar sa_data[14];// 协议地址};sockaddr_in
对于IPv4地址,更常用的是sockaddr_in结构:
structsockaddr_in{shortint sin_family;// 地址家族,通常为AF_INETunsignedshort sin_port;// 端口号structin_addr sin_addr;// IP地址unsignedchar sin_zero[8];// 填充字段,通常置0};structin_addr{unsignedlong s_addr;// IPv4地址(32位)};sockaddr_in6
对于IPv6地址,使用sockaddr_in6结构:
structsockaddr_in6{u_int16_t sin6_family;// 地址家族,AF_INET6u_int16_t sin6_port;// 端口号u_int32_t sin6_flowinfo;// IPv6流信息structin6_addr sin6_addr;// IPv6地址u_int32_t sin6_scope_id;// 接口范围ID};structin6_addr{unsignedchar s6_addr[16];// IPv6地址(128位)};字节序转换
网络字节序是大端序(Big-Endian),而主机字节序可能是大端序或小端序。因此需要进行字节序转换。
常用转换函数
ntohl() - 网络字节序转主机字节序(32位)
uint32_tntohl(uint32_t netlong);示例:
uint32_t net_ip =0x0100007F;// 127.0.0.1的网络字节序uint32_t ip =ntohl(net_ip);ntohs() - 网络字节序转主机字节序(16位)
uint16_tntohs(uint16_t netshort);示例:
uint16_t net_port =0x901F;// 8080的网络字节序uint16_t port =ntohs(net_port);htonl() - 主机字节序转网络字节序(32位)
uint32_thtonl(uint32_t hostlong);示例:
uint32_t ip =0x7F000001;// 127.0.0.1uint32_t net_ip =htonl(ip);htons() - 主机字节序转网络字节序(16位)
uint16_thtons(uint16_t hostshort);示例:
uint16_t port =8080;uint16_t net_port =htons(port);字符串与网络地址转换
inet_ntop() - 将网络字节序转换为点分十进制字符串(支持IPv4和IPv6)
constchar*inet_ntop(int af,constvoid*src,char*dst,socklen_t size);示例:
structin_addr addr; addr.s_addr =0x0100007F;char ip_str[INET_ADDRSTRLEN];inet_ntop(AF_INET,&addr, ip_str, INET_ADDRSTRLEN);inet_pton() - 将点分十进制字符串转换为网络字节序(支持IPv4和IPv6)
intinet_pton(int af,constchar*src,void*dst);示例:
constchar*ip_str ="127.0.0.1";structin_addr addr;inet_pton(AF_INET, ip_str,&addr);inet_ntoa() - 将网络字节序的32位整数转换为点分十进制字符串
char*inet_ntoa(structin_addr in);示例:
structin_addr addr; addr.s_addr =0x0100007F;// 127.0.0.1的网络字节序char*ip_str =inet_ntoa(addr);// 返回"127.0.0.1"inet_addr() - 将点分十进制IP字符串转换为网络字节序的32位整数
unsignedlonginet_addr(constchar*cp);示例:
constchar*ip_str ="127.0.0.1";unsignedlong ip =inet_addr(ip_str);// 返回网络字节序- 网络协议详解
TCP协议特性
连接建立(三次握手)
TCP协议在传输数据前需要通过三次握手建立连接:
- 第一次握手:客户端发送SYN(同步序列编号)包到服务器,进入SYN_SENT状态。
- 第二次握手:服务器收到SYN包后,发送SYN+ACK(确认)包,进入SYN_RCVD状态。
- 第三次握手:客户端收到SYN+ACK包后,发送ACK包,双方进入ESTABLISHED状态,连接建立完成。
可靠传输
TCP通过以下机制确保数据的可靠传输:
- 确认应答(ACK):接收方收到数据后发送ACK确认。
- 超时重传:发送方未收到ACK时重传数据。
- 序列号:每个数据包都有唯一序列号,确保按序到达。
流量控制
TCP通过滑动窗口机制实现流量控制:
- 接收窗口:接收方通过窗口字段告知发送方可接收的数据量。
- 动态调整:根据网络状况和接收方处理能力动态调整窗口大小,避免拥塞。
UDP协议特性
- 无连接性
UDP不需要在通信前建立连接,直接发送数据包。相比TCP的三次握手,减少了连接建立的开销。 - 不可靠传输
- 不保证数据包的顺序、完整性或是否到达目标。
- 没有确认机制(ACK)、重传机制或流量控制。
- 面向数据报
- 发送端每次写入一个报文,接收端每次读取一个完整的报文。
- 报文边界保留,不会像TCP那样合并或拆分。
- 头部开销小
UDP头部仅8字节(源端口、目标端口、长度、校验和),远小于TCP的20字节。 - 支持广播和多播
UDP可以直接向多个主机发送数据包(广播或多播地址),而TCP仅支持点对点通信。
UDP适用场景
- 实时性要求高的应用
- 音视频流媒体(如视频会议、直播):容忍少量丢包,但延迟低更重要。
- 在线游戏:快速响应比数据完整性优先。
- 简单查询/响应模型
- DNS查询:通常只需一次请求和响应,重试成本低。
- DHCP:基于广播的地址分配。
- 广播/多播通信
- 如路由器发现协议(RIP)、网络时间协议(NTP)等。
- 容忍丢包的场景
- 传感器数据上报(如IoT设备),偶尔丢失不影响整体趋势。
- 自定义可靠性层
- 应用层可自行实现重传、排序等逻辑(如QUIC协议基于UDP优化)。
注意事项
- 若需可靠传输,需在应用层实现确认、重传等机制。
- 适合小数据包频繁传输,避免大报文(受MTU限制,可能分片)。
二、C++ Socket编程
- 基础Socket API
创建套接字(socket)
socket()函数用于创建一个套接字,它是网络通信的基础。在C++中通常使用<sys/socket.h>头文件中的socket()函数。
intsocket(int domain,int type,int protocol);- domain: 指定通信域/协议族
- AF_INET: IPv4
- AF_INET6: IPv6
- AF_UNIX: 本地通信
- type: 指定套接字类型
- SOCK_STREAM: 面向连接的TCP套接字
- SOCK_DGRAM: 无连接的UDP套接字
- protocol: 通常为0,表示自动选择默认协议
成功时返回套接字描述符,失败返回-1。
绑定套接字(bind)
bind()函数将套接字与特定的IP地址和端口号绑定。
intbind(int sockfd,conststructsockaddr*addr, socklen_t addrlen);- sockfd: socket()返回的套接字描述符
- addr: 指向包含地址信息的sockaddr结构体指针
- addrlen: 地址结构体的长度
对于IPv4,通常使用sockaddr_in结构体:
structsockaddr_in{short sin_family;// AF_INETunsignedshort sin_port;// 端口号(网络字节序)structin_addr sin_addr;// IP地址char sin_zero[8];// 填充};监听连接(listen)
listen()函数使套接字进入被动监听模式,等待客户端连接请求。
intlisten(int sockfd,int backlog);- sockfd: 已绑定的套接字描述符
- backlog: 等待连接队列的最大长度
成功返回0,失败返回-1。调用listen()后,套接字变为被动套接字。
接受连接(accept)
accept()函数从已完成连接队列中取出第一个连接请求,创建一个新的套接字用于与客户端通信。
intaccept(int sockfd,structsockaddr*addr, socklen_t *addrlen);- sockfd: 处于监听状态的套接字
- addr: 用于存储客户端地址信息的结构体指针
- addrlen: 地址结构体的长度(输入输出参数)
成功返回新的套接字描述符(用于与客户端通信),失败返回-1。原监听套接字继续等待其他连接请求。
连接与数据传输(connect/send/recv)
connect
connect() 是用于建立网络连接的函数,通常在客户端使用。它将套接字与远程服务器的地址和端口关联起来。
函数原型:
intconnect(int sockfd,conststructsockaddr*addr, socklen_t addrlen);参数说明:
sockfd: 套接字描述符,由socket()函数创建。addr: 指向目标服务器地址结构的指针,通常是sockaddr_in或sockaddr_in6。addrlen: 地址结构的长度。
返回值:
- 成功返回
0,失败返回-1并设置errno。
典型用法:
structsockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port =htons(8080);inet_pton(AF_INET,"127.0.0.1",&server_addr.sin_addr);if(connect(sockfd,(structsockaddr*)&server_addr,sizeof(server_addr)){perror("connect failed");exit(EXIT_FAILURE);}send
send() 用于通过已连接的套接字发送数据。
函数原型:
ssize_t send(int sockfd,constvoid*buf, size_t len,int flags);参数说明:
sockfd: 已连接的套接字描述符。buf: 指向要发送数据的缓冲区。len: 要发送的数据长度(字节数)。flags: 可选标志,通常设为0(如MSG_DONTWAIT表示非阻塞发送)。
返回值:
- 成功返回实际发送的字节数,失败返回
-1并设置errno。
注意事项:
- 返回值可能小于
len,需要检查并处理部分发送的情况。 - 在非阻塞模式下可能返回
EAGAIN或EWOULDBLOCK错误。
典型用法:
constchar*msg ="Hello Server"; ssize_t bytes_sent =send(sockfd, msg,strlen(msg),0);if(bytes_sent ==-1){perror("send failed");}recv
recv() 用于从已连接的套接字接收数据。
函数原型:
ssize_t recv(int sockfd,void*buf, size_t len,int flags);参数说明:
sockfd: 已连接的套接字描述符。buf: 用于存储接收数据的缓冲区。len: 缓冲区的最大容量。flags: 可选标志(如MSG_WAITALL表示等待所有数据到达)。
返回值:
- 成功返回接收的字节数,
0表示连接已关闭,失败返回-1并设置errno。
注意事项:
- 返回值可能小于
len,需多次调用recv读取完整数据。 - 在非阻塞模式下可能返回
EAGAIN或EWOULDBLOCK错误。
典型用法:
char buffer[1024]; ssize_t bytes_received =recv(sockfd, buffer,sizeof(buffer),0);if(bytes_received ==-1){perror("recv failed");}elseif(bytes_received ==0){printf("Connection closed by peer\n");}else{ buffer[bytes_received]='\0';// 添加字符串终止符printf("Received: %s\n", buffer);}总结:
connect用于建立连接,send和recv是面向连接的数据传输函数(如TCP)。- 需处理部分发送/接收和错误情况,尤其在非阻塞模式下。
- 高级I/O模型
阻塞I/O
阻塞I/O是指当程序执行I/O操作时,如果数据没有准备好(例如网络数据未到达或文件未读取完成),程序会一直等待(阻塞),直到数据准备好并完成操作后才继续执行后续代码。
特点:
- 同步执行:I/O操作完成前,程序无法继续执行其他任务
- 简单直观:编程模型简单,易于理解
- 效率较低:在等待期间会浪费CPU资源
- 典型函数:
read(),write(),accept()等默认都是阻塞模式
示例:
int n =read(socket_fd, buffer,sizeof(buffer));// 会阻塞直到数据到达非阻塞I/O
非阻塞I/O是指当程序执行I/O操作时,如果数据没有准备好,函数会立即返回一个错误(通常是EWOULDBLOCK或EAGAIN),而不是等待数据准备就绪。
特点:
- 异步执行:无论I/O是否完成都会立即返回
- 需要轮询:程序需要不断检查I/O操作是否完成
- 效率较高:可以同时处理多个I/O操作
- 编程复杂:需要处理部分完成的情况
- 需要设置:通常需要使用
fcntl()设置O_NONBLOCK标志
示例:
fcntl(socket_fd, F_SETFL, O_NONBLOCK);// 设置为非阻塞模式int n =read(socket_fd, buffer,sizeof(buffer));// 立即返回if(n <0&& errno == EWOULDBLOCK){// 数据尚未准备好}主要区别
- 行为差异:阻塞I/O会等待操作完成,非阻塞I/O会立即返回
- 资源使用:阻塞I/O会占用线程资源等待,非阻塞I/O可以释放CPU做其他工作
- 编程模型:阻塞I/O通常是同步的,非阻塞I/O需要配合事件循环
- 适用场景:阻塞I/O适合简单应用,非阻塞I/O适合高并发场景
在网络编程中,非阻塞I/O通常与I/O多路复用(select/poll/epoll)结合使用,以实现高性能的网络服务器。
多路复用技术(select/poll/epoll)
基本概念
多路复用技术是一种允许单个线程或进程同时监控多个文件描述符(fd)的机制,用于检测哪些fd可读、可写或出现异常。这样可以避免为每个连接创建单独的线程,提高服务器处理并发连接的能力。
select
intselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,structtimeval*timeout);- 最早的多路复用实现
- 使用fd_set位掩码来管理fd集合
- 每次调用都需要重新设置fd集合
- 有fd数量限制(FD_SETSIZE,通常1024)
- 采用轮询方式检查fd状态
- 时间复杂度O(n)
poll
intpoll(structpollfd*fds,nfds_t nfds,int timeout);- 改进select的fd数量限制问题
- 使用pollfd结构数组代替fd_set
- 没有最大fd数量限制(但受系统资源限制)
- 仍然采用轮询方式
- 时间复杂度O(n)
- 比select更高效,但本质类似
epoll(Linux特有)
intepoll_create(int size);intepoll_ctl(int epfd,int op,int fd,structepoll_event*event);intepoll_wait(int epfd,structepoll_event*events,int maxevents,int timeout);- Linux特有的高效多路复用机制
- 采用事件驱动方式,而非轮询
- 使用红黑树管理fd,效率更高
- 支持边缘触发(ET)和水平触发(LT)模式
- 时间复杂度O(1)
- 没有fd数量限制(仅受系统资源限制)
- 内核与用户空间共享内存,减少数据拷贝
比较
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大fd数 | 有限(FD_SETSIZE) | 无限制 | 无限制 |
| 效率 | O(n) | O(n) | O(1) |
| 触发方式 | LT | LT | LT/ET |
| 内核支持 | 所有平台 | 所有平台 | Linux |
| 内存拷贝 | 每次调用都需要 | 同select | 仅一次 |
适用场景
- select: 跨平台简单应用,fd数量少
- poll: 需要比select更好的性能,且需要跨平台
- epoll: Linux平台高性能服务器,大量并发连接
- 并发编程
多线程与多进程模型
多线程模型
- 定义:多线程模型是指在一个进程内创建多个线程,这些线程共享进程的资源(如内存、文件描述符等),但每个线程拥有独立的执行流和栈空间。
- 特点:
- 共享资源:线程间共享进程的全局变量、堆内存等,通信成本低。
- 轻量级:线程创建和切换的开销比进程小,因为不需要切换地址空间。
- 并发性:适合I/O密集型任务,可充分利用多核CPU。
- C++实现:
- 使用
std::thread(C++11起)创建线程。 - 需注意线程同步(如互斥锁
std::mutex)以避免数据竞争。
- 使用
- 缺点:
- 一个线程崩溃可能导致整个进程终止。
- 调试复杂,易出现死锁或竞态条件。
多进程模型
- 定义:多进程模型通过创建多个独立的进程来完成任务,每个进程有独立的地址空间和资源。
- 特点:
- 隔离性:进程间资源隔离,一个进程崩溃不会影响其他进程。
- 稳定性高:适合需要高可靠性的场景(如服务端守护进程)。
- 通信成本高:进程间通信(IPC)需通过管道、共享内存、消息队列等机制。
- C++实现:
- 使用
fork()系统调用创建子进程(Unix/Linux)。 - Windows下可用
CreateProcess()API。
- 使用
- 缺点:
- 创建和切换开销大,占用更多内存。
- 进程间同步复杂。
对比总结
| 特性 | 多线程 | 多进程 |
|---|---|---|
| 资源占用 | 低(共享内存) | 高(独立内存) |
| 通信效率 | 高(直接共享变量) | 低(需IPC机制) |
| 容错性 | 差(线程崩溃影响全局) | 强(进程隔离) |
| 适用场景 | I/O密集型、高并发任务 | CPU密集型、需高稳定性任务 |
线程池
线程池是一种多线程处理形式,它维护一组线程,等待监督管理者分配可并发执行的任务。线程池避免了频繁创建和销毁线程的开销,提高了系统性能。
主要组成部分
- 任务队列:存放待处理的任务
- 工作线程:从任务队列中取出任务并执行
- 线程管理器:负责创建、销毁线程,管理线程池大小
优点
- 降低资源消耗:复用已创建的线程
- 提高响应速度:任务到达时可以直接执行
- 提高线程可管理性:统一分配、调优和监控
C++实现示例
classThreadPool{public:ThreadPool(size_t);template<classF,class... Args>autoenqueue(F&& f, Args&&... args)-> std::future<typenamestd::result_of<F(Args...)>::type>;~ThreadPool();private: std::vector<std::thread> workers; std::queue<std::function<void()>> tasks; std::mutex queue_mutex; std::condition_variable condition;bool stop;};异步I/O
异步I/O是一种非阻塞的I/O操作方式,允许程序在I/O操作进行时继续执行其他任务,当I/O操作完成时会收到通知。
主要特点
- 非阻塞:调用立即返回,不等待I/O完成
- 回调机制:通过回调函数处理完成事件
- 高效利用CPU:避免线程阻塞等待I/O
C++实现方式
- std::async + std::future
auto future = std::async(std::launch::async,[]{// I/O操作return result;});// 其他工作auto result = future.get();// 获取结果- Boost.Asio库
boost::asio::io_service io; boost::asio::ip::tcp::socket socket(io); socket.async_connect(endpoint,[](const boost::system::error_code& ec){// 连接完成处理}); io.run();// 运行事件循环线程池与异步I/O结合
将异步I/O的回调处理放在线程池中执行,可以:
- 避免回调函数阻塞I/O事件循环
- 充分利用多核CPU
- 控制并发处理的数量
// 伪代码示例async_io_operation([&pool](Result result){ pool.enqueue([result]{// 处理I/O结果});});三、应用层协议实现
- HTTP服务器开发
HTTP请求解析
HTTP请求解析是指服务器接收并解析客户端发送的HTTP请求的过程。一个HTTP请求通常由以下几部分组成:
- 空行:分隔请求头和请求体
- 请求体:可选部分,主要用于POST请求传递数据
请求头:包含多个键值对,提供关于请求的附加信息
Host: www.example.com User-Agent: Mozilla/5.0 Accept: text/html 请求行:包含请求方法(GET、POST等)、请求的URI和HTTP协议版本
GET /index.html HTTP/1.1 在C++中,解析HTTP请求通常涉及:
- 读取原始请求数据
- 按行分割请求内容
- 解析请求行获取方法和URI
- 解析请求头键值对
- 处理可能的请求体
HTTP响应构建
HTTP响应构建是指服务器创建并发送回客户端的HTTP响应。一个HTTP响应包含:
- 空行:分隔响应头和响应体
- 响应体:实际的响应内容(如HTML页面)
响应头:包含多个键值对,提供关于响应的元信息
Content-Type: text/html Content-Length: 1234 状态行:包含协议版本、状态码和状态描述
HTTP/1.1 200 OK 在C++中构建HTTP响应通常包括:
- 设置状态码和状态描述
- 添加必要的响应头
- 准备响应体内容
- 按照HTTP协议格式组装响应
- 通过socket发送给客户端
示例响应构建代码片段:
std::string BuildResponse(int status,const std::string& content){ std::ostringstream response; response <<"HTTP/1.1 "<< status <<" OK\r\n"; response <<"Content-Type: text/html\r\n"; response <<"Content-Length: "<< content.length()<<"\r\n"; response <<"\r\n"; response << content;return response.str();}静态文件服务器实现
1. 基本概念
静态文件服务器是一种专门用于提供静态内容(如HTML、CSS、JavaScript、图片等)的网络服务器。它不需要处理动态内容生成,而是直接返回存储在服务器上的文件。
2. 核心功能
- 文件读取:从磁盘读取请求的文件内容。
- MIME类型识别:根据文件扩展名确定正确的Content-Type头。
- 缓存支持:可选的缓存机制(如ETag、Last-Modified)以提高性能。
- 目录列表:可选地提供目录浏览功能。
- 范围请求:支持HTTP范围请求(Range requests)以实现断点续传。
3. 实现步骤
- 创建HTTP服务器:使用socket API或更高级的库(如Boost.Asio)创建TCP服务器。
- 解析HTTP请求:解析客户端发送的HTTP请求,提取请求方法和路径。
- 验证路径:确保请求路径在服务器允许的范围内,防止目录遍历攻击。
- 读取文件:使用文件I/O操作读取请求的文件内容。
- 设置响应头:包括Content-Type、Content-Length等必要的HTTP头。
- 发送响应:将文件内容和HTTP头发送回客户端。
4. 安全考虑
- 路径验证:防止使用
../等字符访问服务器根目录之外的文件。 - 文件权限:确保服务器进程有权限读取请求的文件。
- 连接限制:防止过多的并发连接耗尽系统资源。
5. 性能优化
- 内存映射文件:对于大文件,使用内存映射可以提高读取效率。
- 发送文件:在支持的系统上,使用
sendfile系统调用可以避免用户空间和内核空间之间的数据拷贝。 - 压缩:对文本文件启用gzip压缩以减少传输数据量。
6. 示例代码结构(伪代码)
voidhandle_request(const Request& req, Response& res){// 验证路径安全if(!is_path_safe(req.path)){ res.status =403;return;}// 检查文件是否存在if(!file_exists(req.path)){ res.status =404;return;}// 读取文件内容auto content =read_file(req.path);// 设置响应头 res.headers["Content-Type"]=get_mime_type(req.path); res.headers["Content-Length"]= std::to_string(content.size());// 发送响应 res.body = std::move(content); res.status =200;}- 自定义协议设计
协议头与消息体设计
协议头(Header)
协议头是网络通信中位于消息前部的固定或可变长度的数据块,主要包含控制信息。常见设计要素:
- 魔数(Magic Number)
- 固定字节序列(如
0xACBF),用于快速识别协议有效性
- 固定字节序列(如
- 版本号(Version)
- 标识协议版本(如
v1.0用0x01表示)
- 标识协议版本(如
- 消息类型(Message Type)
- 区分请求/响应/心跳等(如
0x01=请求,0x02=响应)
- 区分请求/响应/心跳等(如
- 序列号(Sequence ID)
- 唯一标识请求-响应配对(4字节无符号整数)
- 消息体长度(Body Length)
- 记录消息体的字节数(通常4字节存储)
典型二进制协议头示例(14字节):
| 魔数(2B) | 版本(1B) | 类型(1B) | 序列号(4B) | 长度(4B) | 保留(2B) | 消息体(Body)
消息体承载实际业务数据,设计要点:
- 序列化格式
- 二进制:TLV(Type-Length-Value)结构
- 文本:JSON/XML(需定义字段名和类型)
- 字段排列
- 固定顺序:如
[userID][name][age] - 标签化:每个字段带标识(如Protobuf的tag)
- 固定顺序:如
- 边界处理
- 定长:所有消息体长度相同
- 变长:依赖头部的
Body Length字段
设计原则
- 扩展性
- 头部预留
保留字段,版本号支持升级
- 头部预留
- 对齐
- 按4/8字节对齐提升处理效率(如
int32从4的倍数地址开始)
- 按4/8字节对齐提升处理效率(如
- 字节序
- 明确使用网络字节序(大端序)
- 校验
- 可在尾部添加CRC32校验码(不属于消息体)
注:实际设计中需配合字节填充(padding)和位域(bit field)优化空间
序列化与反序列化技术
定义
序列化(Serialization)是将数据结构或对象转换为可存储或传输的格式(如字节流)的过程。反序列化(Deserialization)则是将序列化后的数据恢复为原始数据结构或对象的过程。
用途
- 数据持久化:将对象保存到文件或数据库中。
- 网络传输:在分布式系统中,通过网络传输对象。
- 进程间通信:在不同进程间传递对象。
常见格式
- 二进制格式:高效但可读性差,如Protocol Buffers。
- 文本格式:可读性好但效率较低,如JSON、XML。
C++中的实现方式
- 第三方库:
- Boost.Serialization:支持复杂对象的序列化。
- Protocol Buffers:高效的二进制序列化工具。
- JSON库(如nlohmann/json):用于JSON格式的序列化。
手动实现:通过重载<<和>>运算符或编写专门的序列化函数。
classMyClass{public:int a; std::string b;// 序列化voidserialize(std::ostream& os)const{ os << a <<" "<< b;}// 反序列化voiddeserialize(std::istream& is){ is >> a >> b;}};注意事项
- 版本兼容性:数据结构变化时需处理旧数据的反序列化。
- 安全性:反序列化不可信数据可能导致安全问题。
- 性能:二进制格式通常比文本格式更快。
示例(使用JSON)
#include<nlohmann/json.hpp>using json = nlohmann::json;// 序列化 MyClass obj; json j; j["a"]= obj.a; j["b"]= obj.b; std::string serialized = j.dump();// 反序列化auto j2 = json::parse(serialized); MyClass obj2; obj2.a = j2["a"]; obj2.b = j2["b"];四、网络编程进阶
- 网络安全
SSL/TLS基础
SSL (Secure Sockets Layer) 和 TLS (Transport Layer Security) 是用于在计算机网络中提供安全通信的加密协议。它们通过在传输层和应用层之间插入一个安全层来工作,确保数据在传输过程中的机密性、完整性和身份验证。
核心功能
- 加密:通过对称加密算法(如AES)保护数据传输的隐私。
- 身份验证:使用数字证书和公钥基础设施(PKI)验证通信双方的身份。
- 数据完整性:通过消息认证码(MAC)或哈希算法(如SHA-256)防止数据篡改。
协议版本
- SSL 3.0:已弃用,存在严重安全漏洞(如POODLE攻击)。
- TLS 1.2:目前广泛使用的版本,支持现代加密算法。
- TLS 1.3:最新版本,简化握手过程并移除不安全特性。
握手过程(以TLS 1.2为例)
- ClientHello:客户端发送支持的加密套件和随机数。
- ServerHello:服务器选择加密套件并返回随机数。
- 证书交换:服务器发送证书(可选客户端证书)。
- 密钥交换:通过DH或RSA协商预主密钥。
- 完成握手:双方生成会话密钥并验证加密通信。
OpenSSL库应用
OpenSSL是一个开源的SSL/TLS实现库,提供加密、证书管理和安全通信功能,广泛应用于C/C++网络编程中。
核心组件
- libcrypto:提供基础加密算法(如AES、RSA、SHA)。
- libssl:实现SSL/TLS协议栈。
- 命令行工具:如
openssl命令用于生成证书和测试。
常用API示例
#include<openssl/ssl.h>#include<openssl/err.h>// 初始化OpenSSL库SSL_library_init();SSL_load_error_strings();// 创建SSL上下文 SSL_CTX* ctx =SSL_CTX_new(TLS_server_method());// 加载证书和私钥SSL_CTX_use_certificate_file(ctx,"server.crt", SSL_FILETYPE_PEM);SSL_CTX_use_PrivateKey_file(ctx,"server.key", SSL_FILETYPE_PEM);// 创建SSL对象并绑定套接字 SSL* ssl =SSL_new(ctx);SSL_set_fd(ssl, sockfd);// 执行TLS握手SSL_accept(ssl);// 服务器端// 或 SSL_connect(ssl); // 客户端// 安全数据传输SSL_write(ssl, data, len);SSL_read(ssl, buffer,sizeof(buffer));// 清理资源SSL_free(ssl);SSL_CTX_free(ctx);关键函数说明
SSL_CTX_new():创建SSL上下文,指定协议版本(如TLS_server_method)。SSL_new():基于上下文创建SSL会话对象。SSL_set_fd():将SSL对象与TCP套接字绑定。SSL_accept()/SSL_connect():分别用于服务器和客户端握手。SSL_read()/SSL_write():替代常规的recv()/send()进行加密通信。
错误处理
if(SSL_get_error(ssl, ret)== SSL_ERROR_SSL){ERR_print_errors_fp(stderr);// 打印OpenSSL错误堆栈}证书管理
验证证书链:
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER,NULL);SSL_CTX_load_verify_locations(ctx,"ca.crt",NULL);生成自签名证书:
openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -days 365注意事项
- 内存管理:OpenSSL API需手动释放资源(如
SSL_free)。 - 线程安全:需调用
CRYPTO_set_locking_callback()设置锁回调。 - 性能优化:启用会话复用(
SSL_CTX_set_session_cache_mode)。
协议配置:禁用不安全选项(如SSLv3):
SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv3);数据加密
数据加密是指将明文数据通过特定的算法转换为密文的过程,目的是保护数据的机密性,防止未经授权的访问或泄露。
常见加密方式
- 对称加密
- 使用相同的密钥进行加密和解密。
- 常见算法:AES(高级加密标准)、DES(数据加密标准)。
- 优点:加密速度快,适合大数据量加密。
- 缺点:密钥分发和管理困难。
- 非对称加密
- 使用公钥和私钥配对,公钥加密的数据只能用私钥解密,反之亦然。
- 常见算法:RSA、ECC(椭圆曲线加密)。
- 优点:安全性高,解决了密钥分发问题。
- 缺点:加密速度慢,适合小数据量或密钥交换。
- 哈希加密
- 将数据转换为固定长度的哈希值(如MD5、SHA-256),不可逆。
- 用途:校验数据完整性或存储密码(需加盐)。
身份认证
身份认证是验证用户或系统身份的过程,确保通信双方的真实性。
常见认证方式
- 用户名/密码认证
- 最基础的方式,但需结合加密(如HTTPS)防止窃听。
- 数字证书
- 基于非对称加密,由可信第三方(CA)颁发证书,验证身份(如SSL/TLS)。
- 多因素认证(MFA)
- 结合密码、手机验证码、生物特征等两种以上方式。
- OAuth/OpenID Connect
- 第三方授权协议,允许用户通过其他平台(如Google)登录。
在C++网络编程中的应用
- 使用OpenSSL库实现TLS加密通信。
- 通过哈希加盐存储用户密码(如bcrypt)。
- 集成第三方认证服务(如OAuth2.0)。
- 性能优化
高并发处理策略
高并发处理策略是指在网络编程中,为了应对大量客户端同时请求服务而采取的一系列技术和方法。以下是几种常见的高并发处理策略:
1. 多线程/多进程
- 多线程:通过创建多个线程来处理并发请求。每个线程独立处理一个客户端请求,共享进程资源。
- 优点:轻量级,创建和切换开销较小。
- 缺点:线程间同步复杂,容易引发竞争条件或死锁。
- 多进程:通过创建多个进程来处理并发请求。每个进程独立运行,拥有自己的资源。
- 优点:进程间隔离性好,稳定性高。
- 缺点:创建和切换开销较大,进程间通信(IPC)复杂。
2. I/O多路复用
- 使用
select、poll、epoll(Linux)或kqueue(BSD)等机制,监控多个文件描述符(如套接字)的状态变化。 - 优点:单线程即可处理大量连接,减少线程/进程切换的开销。
- 缺点:编程复杂度较高,需要处理事件循环逻辑。
3. 异步I/O
- 通过回调或协程(如
async/await)实现非阻塞I/O操作,避免线程阻塞。 - 优点:高效利用CPU资源,适合高并发场景。
- 缺点:代码逻辑复杂,调试困难。
4. 线程池/进程池
- 预先创建一组线程或进程,任务到来时从池中分配资源处理,避免频繁创建和销毁。
- 优点:减少资源开销,提高响应速度。
- 缺点:池大小需合理配置,否则可能成为瓶颈。
5. 负载均衡
- 将请求分发到多个服务器或服务实例,避免单点过载。
- 常见方式:轮询、加权轮询、最小连接数等。
- 优点:提高系统整体吞吐量和可用性。
- 缺点:需要额外的基础设施支持(如反向代理)。
6. 连接池
- 复用数据库或外部服务的连接,避免频繁建立和断开连接。
- 优点:减少连接建立的开销,提高性能。
- 缺点:需要管理连接的生命周期和状态。
7. 缓存
- 使用内存缓存(如 Redis)存储频繁访问的数据,减少后端压力。
- 优点:显著降低响应时间。
- 缺点:需要处理缓存一致性问题。
8. 无锁编程
- 通过原子操作或无锁数据结构(如无锁队列)减少线程竞争。
- 优点:提高并发性能。
- 缺点:实现复杂,适用场景有限。
9. 限流与熔断
- 限流:限制单位时间内的请求数量(如令牌桶算法)。
- 熔断:在系统过载时暂时拒绝请求,避免雪崩。
- 优点:保护系统稳定性。
- 缺点:可能影响用户体验。
这些策略可以单独或组合使用,具体选择需根据应用场景和性能需求权衡。
网络I/O性能调优
网络I/O性能调优是指通过优化网络输入输出操作来提高程序的网络通信效率。以下是一些关键的调优方法:
1. 缓冲区大小调整
- 适当调整发送和接收缓冲区的大小可以减少系统调用的次数。
- 在Linux中,可以通过
setsockopt函数设置SO_RCVBUF和SO_SNDBUF选项来调整缓冲区大小。
2. 非阻塞I/O
- 使用非阻塞I/O可以避免线程在等待数据时被阻塞,提高并发性能。
- 在C++中,可以通过
fcntl函数设置套接字为非阻塞模式。
3. I/O多路复用
- 使用
select、poll或epoll等机制可以同时监控多个套接字的状态,减少线程数量。 epoll在Linux上性能较高,适合高并发场景。
4. 零拷贝技术
- 通过
sendfile或splice等系统调用避免数据在用户空间和内核空间之间的多次拷贝。 - 适用于文件传输等场景。
5. 批量操作
- 使用
writev和readv等函数进行批量读写,减少系统调用次数。
6. 延迟确认(TCP_NODELAY)
- 禁用Nagle算法(设置
TCP_NODELAY选项)可以减少小数据包的延迟。 - 适用于实时性要求高的应用。
7. 连接池
- 复用TCP连接可以减少连接建立和断开的开销。
- 适用于频繁短连接的场景。
8. 协议优化
- 使用二进制协议(如Protocol Buffers)代替文本协议(如JSON)可以减少数据传输量。
9. 多线程/多进程
- 在多核系统上,使用多线程或多进程可以充分利用CPU资源。
- 注意避免锁竞争和上下文切换的开销。
10. 硬件加速
- 使用支持TOE(TCP Offload Engine)的网卡可以将部分TCP协议处理卸载到硬件。
这些方法需要根据具体应用场景和性能瓶颈进行选择和组合。
五、实践案例
- 经典网络应用
简单聊天服务器与客户端
服务器端 (Server)
- 创建套接字 (Socket Creation)
- 使用
socket()函数创建一个套接字,指定协议族(如AF_INET)和类型(如SOCK_STREAM表示 TCP)。
- 使用
- 绑定地址 (Binding)
- 使用
bind()函数将套接字绑定到特定的 IP 地址和端口号。通常使用INADDR_ANY表示接受任意网络接口的连接。
- 使用
- 监听连接 (Listening)
- 调用
listen()函数使套接字进入被动监听状态,等待客户端连接。可以指定最大连接队列长度。
- 调用
- 接受连接 (Accepting Connections)
- 使用
accept()函数接受客户端的连接请求。该函数会阻塞直到有客户端连接,并返回一个新的套接字用于与该客户端通信。
- 使用
- 收发数据 (Data Exchange)
- 使用
recv()和send()函数与客户端进行数据交换。服务器可以循环接收和发送消息。
- 使用
- 关闭连接 (Closing)
- 使用
close()或closesocket()关闭套接字,释放资源。
- 使用
客户端 (Client)
- 创建套接字 (Socket Creation)
- 同样使用
socket()函数创建套接字。
- 同样使用
- 连接服务器 (Connecting)
- 使用
connect()函数连接到服务器的 IP 地址和端口号。
- 使用
- 收发数据 (Data Exchange)
- 使用
send()和recv()函数与服务器进行通信。客户端可以发送消息并接收服务器的回复。
- 使用
- 关闭连接 (Closing)
- 使用
close()或closesocket()关闭套接字。
- 使用
示例代码框架
// 服务器端伪代码int server_socket =socket(AF_INET, SOCK_STREAM,0);bind(server_socket,(structsockaddr*)&server_addr,sizeof(server_addr));listen(server_socket,5);int client_socket =accept(server_socket,(structsockaddr*)&client_addr,&addr_len);recv(client_socket, buffer,sizeof(buffer),0);send(client_socket,"Hello Client",strlen("Hello Client"),0);close(client_socket);close(server_socket);// 客户端伪代码int client_socket =socket(AF_INET, SOCK_STREAM,0);connect(client_socket,(structsockaddr*)&server_addr,sizeof(server_addr));send(client_socket,"Hello Server",strlen("Hello Server"),0);recv(client_socket, buffer,sizeof(buffer),0);close(client_socket);注意事项
- 错误处理:每个网络函数调用后都应检查返回值,处理可能的错误。
- 多客户端:简单实现只能处理一个客户端,如需多客户端需使用多线程或
select()/poll()。 - 阻塞模式:默认情况下套接字是阻塞的,
recv()会一直等待数据到达。
文件传输系统实现
文件传输系统是网络编程中常见的应用,用于在不同主机之间高效、可靠地传输文件。以下是实现文件传输系统的关键要素:
1. 基本架构
- 客户端-服务器模型:通常采用客户端请求、服务器响应的模式
- 协议选择:可以使用TCP(可靠传输)或UDP(快速但不可靠)
- 连接管理:建立、维护和终止连接
2. 核心功能实现
- 文件分块:将大文件分割为固定大小的数据块传输
- 校验机制:使用校验和(checksum)或哈希值确保数据完整性
- 断点续传:记录传输进度,支持从中断处继续传输
- 并发控制:支持多文件同时传输
3. 关键实现步骤
- 建立连接:
- 服务器监听特定端口
- 客户端发起连接请求
- 协议设计:
- 定义文件传输的控制命令
- 设计数据包格式(头部信息+数据)
- 文件处理:
- 读取本地文件
- 序列化文件数据
- 分块发送/接收
- 状态管理:
- 跟踪传输进度
- 处理传输错误
4. 性能优化
- 缓冲区管理:合理设置发送/接收缓冲区大小
- 滑动窗口:TCP拥塞控制优化
- 压缩传输:减少网络带宽占用
5. 安全考虑
- 身份验证机制
- 数据传输加密
- 权限控制
6. 错误处理
- 网络中断恢复
- 数据校验失败重传
- 异常情况处理
7. 典型实现方式(C++)
// 简单示例框架classFileTransfer{public:voidsendFile(const std::string& filename);voidreceiveFile(const std::string& savePath);private:boolvalidateFile(const std::string& path);voidsendChunk(constchar* data, size_t size);voidreceiveChunk(char* buffer, size_t size);};实现文件传输系统时,需要特别注意网络字节序、平台兼容性以及异常处理等问题。
- 第三方库应用
Boost.Asio网络库入门
什么是Boost.Asio?
Boost.Asio是Boost库中用于网络和底层I/O编程的跨平台C++库,提供异步I/O模型,支持TCP、UDP、定时器、文件描述符等操作。其核心基于Proactor设计模式,通过事件驱动机制高效处理并发任务。
核心组件
tcp::socket:TCP通信端点。udp::socket:UDP通信端点。
Resolver
将主机名和端口解析为端点(endpoint):
tcp::resolver resolver(io);auto endpoints = resolver.resolve("example.com","80");Socket类
boost::asio::ip::tcp::socket socket(io);io_context
事件调度中心,管理I/O操作和回调。所有异步操作需通过io_context::run()执行。
boost::asio::io_context io; io.run();// 启动事件循环异步操作示例(TCP客户端)
voidasync_connect(boost::asio::ip::tcp::socket& socket,const boost::asio::ip::tcp::resolver::results_type& endpoints){ boost::asio::async_connect( socket, endpoints,[](boost::system::error_code ec,constauto&){if(!ec) std::cout <<"Connected!\n";});}定时器使用
boost::asio::steady_timer timer(io, std::chrono::seconds(1)); timer.async_wait([](auto ec){if(!ec) std::cout <<"Timeout!\n";});错误处理
通过boost::system::error_code捕获异常:
socket.async_read_some(...,[](error_code ec, size_t length){if(ec == boost::asio::error::eof) std::cerr <<"Connection closed\n";});注意事项
- 线程安全:
io_context可多线程调用run(),但单个对象(如socket)需同步访问。 - 资源管理:使用
std::shared_ptr管理异步操作中的对象生命周期。
适用场景
- 高并发服务器(如Web服务、游戏后端)
- 需要非阻塞I/O的低延迟应用
Protobuf序列化实践
什么是Protobuf序列化
Protobuf(Protocol Buffers)是Google开发的一种高效的数据序列化格式。它可以将结构化数据转换为二进制格式,用于网络传输或存储。与XML和JSON等文本格式相比,Protobuf序列化后的数据更小、解析速度更快。
Protobuf序列化的基本步骤
- 定义消息格式:使用
.proto文件定义数据结构
message Person { required string name = 1; optional int32 id = 2; repeated string email = 3; } - 编译.proto文件:使用protoc编译器生成对应语言的类
protoc --cpp_out=. person.proto - 序列化数据:将对象转换为二进制格式
Person person; person.set_name("John Doe"); person.set_id(1234); person.add_email("[email protected]"); std::string serialized_data; person.SerializeToString(&serialized_data);- 反序列化数据:将二进制数据还原为对象
Person new_person; new_person.ParseFromString(serialized_data);Protobuf序列化的特点
- 高效性:二进制格式比文本格式更紧凑
- 跨语言支持:支持多种编程语言
- 向后兼容:字段编号机制支持协议演进
- 快速解析:不需要复杂的文本解析
最佳实践
- 为每个字段分配唯一的编号
- 合理使用required/optional/repeated修饰符
- 考虑数据兼容性,避免删除已使用的字段
- 对于大型项目,将消息定义分散到多个.proto文件中
性能优化建议
- 复用消息对象以减少内存分配
- 对于大型数据,考虑使用分块序列化
- 在性能关键路径上预分配缓冲区
常见问题
- 版本兼容性问题:新旧版本消息格式不一致
- 字段编号冲突:不同消息中使用相同编号
- 内存消耗:处理大型消息时需要注意内存使用