开篇:本文解决什么问题?
- 对TCP/UDP协议的核心特性与区别理解不透彻
- 不熟悉socket套接字的基础操作及I/O模型的适用场景
- 缺乏高并发网络程序的设计思路,无法应对海量连接的性能瓶颈
一、网络协议基础
1. TCP与UDP核心特性对比
TCP和UDP是传输层核心协议,适用于不同的网络通信场景,核心差异体现在可靠性、传输方式等方面:
|------|--------------------|---------------------|
| 特性 | TCP | UDP |
| 连接特性 | 面向连接(三次握手建立连接) | 无连接(无需建立连接直接发送) |
| 可靠性 | 可靠传输(重传、确认、排序) | 不可靠传输(无确认、重传机制) |
| 传输方式 | 字节流传输 | 数据报传输 |
| 拥塞控制 | 支持拥塞控制、流量控制 | 无拥塞控制 |
| 适用场景 | 文件传输、HTTP/HTTPS等 | 视频直播、游戏、DNS等 |
2. TCP关键机制
三次握手:建立TCP连接,确保双方收发能力正常
四次挥手:关闭TCP连接,保证数据全部传输完成
滑动窗口:实现流量控制,避免发送方速率过快导致接收方缓冲区溢出
拥塞控制:通过慢启动、拥塞避免等算法,适应网络拥塞状态
二、Socket编程基础
1. Socket核心操作函数
(1)套接字创建:socket()
cpp
int socket(int domain, int type, int protocol);
参数:
- domain :协议域(如 AF_INET 表示IPv4, AF_UNIX 表示本地域)
- type :套接字类型( SOCK_STREAM 为TCP流套接字, SOCK_DGRAM 为UDP数据报套接字)
- protocol :协议类型(通常设为0,自动匹配type对应的协议)
返回值:成功返回套接字文件描述符,失败返回-1。
(2)地址绑定:bind()
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
将套接字与本地IP地址 和端口绑定, addr 为协议地址结构(如 struct sockaddr_in ), addrlen 为地址长度。
(3)监听连接:listen()
cpp
int listen(int sockfd, int backlog);
仅用于TCP套接字,将套接字转为监听状态, backlog 指定半连接队列的最大长度。
(4)接受连接:accept()
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
阻塞等待并接受TCP客户端连接,返回新的套接字描述符用于与客户端通信, addr 用于获取客户端地址。
(5)发起连接:connect()
cpp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
TCP客户端用于向服务器发起连接, addr 为服务器的协议地址。
(6)数据收发
TCP收发: read() / write() 、 recv() / send() ,基于字节流传输
UDP收发: recvfrom() / sendto() ,需指定对方地址,基于数据报传输
2. TCP服务端/客户端通信流程
服务端: socket() → bind() → listen() → accept() → 收发数据 → 关闭套接字
客户端: socket() → connect() → 收发数据 → 关闭套接字
三、I/O模型与多路复用
1. 常见I/O模型
|---------|------------------------------------------|----------------|
| I/O模型 | 特点 | 适用场景 |
| 阻塞I/O | 调用I/O函数后阻塞,直到数据就绪 | 简单场景、低并发连接 |
| 非阻塞I/O | 轮询检查数据就绪,未就绪时返回错误 | 需实时响应的简单场景 |
| I/O多路复用 | 通过 select / poll / epoll 监听多个fd,数据就绪后再处理 | 高并发连接(如百万级连接) |
| 信号驱动I/O | 通过信号通知数据就绪,异步触发 | 特定场景(如UDP数据接收) |
| 异步I/O | 内核完成数据读写后通知进程,全程无阻塞 | 高性能后台服务 |
2. I/O多路复用核心实现
(1)select/poll
select :监听有限数量的文件描述符(默认1024),需轮询检查就绪fd,效率较低
poll :突破fd数量限制,仍需轮询,性能提升有限
(2)epoll(高性能I/O多路复用)
epoll是Linux特有的多路复用机制,支持海量文件描述符,核心函数:
cpp
int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改/删除fd
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待fd就绪
触发模式:水平触发(LT)和边沿触发(ET),ET模式需配合非阻塞I/O使用,效率更高。
优势:基于事件驱动,无需轮询,仅处理就绪fd,适合高并发场景。
四、高并发网络程序设计
1. 多进程/多线程模型
多进程:通过 fork() 创建子进程处理客户端连接,进程间独立,稳定性高,但创建/切换开销大。
多线程:通过pthread创建线程处理连接,开销小于进程,但需解决线程同步问题。
2. 线程池/进程池模型
预先创建一定数量的进程/线程,复用资源处理客户端请求,避免频繁创建/销毁的开销,组成:
任务队列:存储客户端连接任务
工作线程/进程:循环获取任务并处理
同步机制:互斥锁+条件变量保证任务队列线程安全
3. 反应堆模型(Reactor)
基于epoll的事件驱动模型,将I/O事件、信号事件等抽象为事件,由反应堆框架统一处理:
主反应堆:监听客户端连接事件,接受连接后将fd注册到子反应堆
子反应堆:处理已连接fd的读写事件,实现高并发处理
4. 协程模型
基于用户态的轻量级线程,切换开销远小于线程,通过协程调度器实现高并发,如libco、ucontext等库,适用于I/O密集型场景。
五、网络编程关键问题
1. 粘包问题(TCP)
TCP是字节流传输,易出现粘包,解决方法:固定消息长度;消息头部添加长度字段;使用特殊分隔符分割消息。
2. 半关闭与优雅退出
通过 shutdown() 实现套接字的半关闭,保证数据全部收发完成后再关闭连接,避免数据丢失。
3. 端口复用
通过 setsockopt() 设置 SO_REUSEADDR 选项,允许端口快速复用,避免服务重启时出现"地址已被使用"的错误。
4. 心跳机制
在TCP连接中添加心跳包,检测对方是否在线,避免无效连接占用资源,通常通过定时发送小数据包实现。