C++ 网络编程
一、TCP Socket通信实现
记忆口诀:服创绑监听,接收发关尽;客创连收发,关闭要记心。
服务器端流程
- 创建socket:调用
socket()
创建流式套接字(TCP) - 绑定地址:通过
bind()
将socket与IP地址和端口绑定 - 监听连接:使用
listen()
开启监听,设置最大连接队列 - 接受连接:调用
accept()
阻塞等待客户端连接,返回新socket - 数据收发:使用
send()
和recv()
进行数据传输 - 关闭socket:通信结束后关闭连接
客户端流程
- 创建socket:同服务器端
- 连接服务器:通过
connect()
向服务器发起连接请求 - 数据收发:同服务器端
- 关闭socket:通信结束后关闭连接
服务器端代码示例
1 |
|
客户端代码示例
1 |
|
关键函数解析
socket()
: 创建套接字,参数为协议族、套接字类型和协议bind()
: 绑定地址和端口listen()
: 监听连接请求,设置最大等待队列长度accept()
: 接受客户端连接,返回新的通信socketconnect()
: 客户端连接服务器send()/recv()
: 数据传输函数shutdown()
: 优雅关闭连接,可指定关闭方向close()
: 关闭套接字,释放资源
跨平台实现
- Windows平台: 使用WinSock API,如
WSASocket()
、WSAStartup()
- 跨平台库: 使用Boost.Asio、Poco等库封装底层差异
二、Socket阻塞与非阻塞模式
记忆口诀:阻塞等操作,线程被挂起;非阻立即返,错误EAGAIN;多路复用配,效率更优异。
阻塞模式
- 默认行为: Socket I/O操作会阻塞调用线程,直到操作完成或发生错误
- 例子:
read()
会一直等待直到接收到数据;write()
会等待直到数据被写入缓冲区 - 优点: 编程简单,易于理解
- 缺点: 线程阻塞影响效率,可能导致资源浪费
非阻塞模式
- 修改行为: Socket I/O操作不阻塞调用线程,无法立即完成时返回错误码
- 错误码: 通常为
EAGAIN
或EWOULDBLOCK
- 应用场景: 通常与I/O多路复用结合使用(select、poll、epoll)
模式切换方法
1. 使用fcntl函数:
1 |
|
2. 使用ioctl函数:
1 |
|
三、多客户端连接处理方案
记忆口诀:多线程易实现,资源消耗大;多路复用优,并发能力强;异步效率高,实现最复杂。
1. 多进程/多线程模型
- 多进程: 每个客户端连接fork一个子进程(Unix/Linux)
- 多线程: 每个客户端连接创建一个新线程(跨平台)
- 优点: 编程简单,隔离性好
- 缺点: 资源消耗大,线程/进程上下文切换开销高
多线程服务器示例:
1 |
|
2. I/O多路复用模型
- select/poll: 单线程轮询多个socket(select有FD数量限制)
- epoll(Linux): 事件驱动,高效处理大量连接(LT/ET模式)
- 优点: 资源利用率高,适合高并发
- 缺点: 编程复杂度高
select多路复用示例:
1 |
|
epoll多路复用示例(Linux):
1 |
|
3. 异步I/O模型
- Windows IOCP / Linux aio: 内核直接通知I/O完成
- 优点: 线程利用率最大化
- 缺点: 平台依赖,调试困难
4. 方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
多线程/进程 | 编程简单,隔离性好 | 资源消耗大,扩展性差 | 连接数少,计算密集型 |
select/poll | 跨平台支持 | FD数量受限(select),轮询效率低 | 中小规模并发 |
epoll(Linux) | 事件驱动,无FD限制 | Linux专属,编程复杂度高 | 大规模高并发(如Web服务器) |
异步I/O(IOCP) | 线程利用率最大化 | 平台依赖,调试困难 | 特定平台高性能需求 |
5. epoll关键特性
- 水平触发(LT): 默认模式,只要socket有数据可读就触发事件
- 边缘触发(ET): 仅在数据到来时触发一次,要求一次性读完所有数据
- 高效机制: 使用红黑树管理FD,事件链表通知就绪FD,O(1)时间复杂度
6. 多线程+epoll混合模型(主从Reactor)
- 主Reactor线程: 接受连接并分发给Worker线程
- Worker线程池: 每个线程维护一个epoll实例处理I/O
四、粘包和拆包问题
记忆口诀:定长最直接,分隔符易识别,消息头含长度,自定义最灵活。
粘包拆包问题的解决方法
1. 固定长度法
- 发送端将每个包都封装成固定长度(如100字节)
- 不足部分通过补0或空字符填充到指定长度
- 接收端按固定长度读取数据
2. 分隔符法
- 发送端在每个包的末尾使用固定分隔符(如
\r\n
) - 接收端通过查找分隔符来确定包的边界
- 示例: FTP协议采用此方式
3. 消息头+消息体法
- 将消息分为头部和消息体两部分
- 头部中保存整个消息的长度信息
- 接收端先读取头部,再根据长度读取完整消息体
- 优点: 高效可靠,广泛应用于自定义协议
4. 自定义协议法
- 根据业务需求设计完整的协议格式
- 通常包含魔数、版本号、消息类型、长度、校验等字段
- 优点: 灵活适应特定场景,安全性更高
五、网络编程优化技巧
记忆口诀:地址复用快重启,超时设置防阻塞;线程池减开销,零拷贝提性能;事件驱动模式优,协程简化异步程。
1. 错误处理与优化
- 地址复用: 设置
SO_REUSEADDR
标志允许快速重启服务器 - 超时设置: 通过
setsockopt()
设置SO_RCVTIMEO
和SO_SNDTIMEO
避免永久阻塞 - 优雅关闭: 使用
shutdown()
而非直接close()
,避免数据丢失
2. 关键优化策略
- 使用线程池减少线程创建开销
- 采用边缘触发模式提高epoll效率
- 分离I/O操作与业务逻辑(Reactor模式)
- 使用零拷贝技术(如
splice()
)减少数据拷贝
3. 现代实践
- 使用Boost.Asio、libevent等成熟库封装底层差异
- 结合协程(如C++20的coroutine)简化异步编程模型
- 考虑io_uring(Linux 5.1+)进一步提升I/O性能
六、常见面试问题
1. TCP和UDP的主要区别
- TCP: 面向连接、可靠传输、有序、重量级
- UDP: 无连接、不可靠、无序、轻量级
- 适用场景: TCP用于文件传输、网页浏览等;UDP用于视频通话、游戏等
2. 为什么服务器需要两个socket
- 监听socket: 用于接受连接请求,保持监听状态
- 通信socket: 用于与客户端实际通信,可创建多个
3. select的1024个FD限制问题
- 历史原因: 由
fd_set
的实现(位图)决定 - 解决方法: 可通过修改内核参数调整,但效率仍低于epoll
4. epoll的ET模式为什么要求非阻塞socket
- 原因: ET模式下若数据未读完,不会再次触发事件
- 风险: 使用阻塞socket可能导致线程永久阻塞
5. 多线程服务器中的竞态条件处理
- 互斥锁: 使用
std::mutex
保护共享资源 - 无锁结构: 采用原子操作实现无锁数据结构
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.