网络 IO 的本质:
等待数据准备好,并将数据从内核缓冲区拷贝到用户缓冲区。
常见 IO 模型:
- 阻塞 IO
- 非阻塞 IO
- IO 多路复用
- 信号驱动 IO
- 异步 IO
阻塞 IO
默认情况下,Socket 是阻塞的。
例如调用 recv() 时:
示例:
1 2
| char buffer[1024]; ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
|
如果没有数据,程序会停在 recv()。
优点:
缺点:
非阻塞 IO
可以将 Socket 设置为非阻塞。
如果没有数据,recv() 不会阻塞,而是立即返回错误。
1 2 3 4 5 6 7
| #include <fcntl.h> #include <unistd.h>
void setNonBlocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); }
|
非阻塞 recv():
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| char buffer[1024]; ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n > 0) { } else if (n == 0) { } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { } else { } }
|
优点:
缺点:
IO 多路复用
IO 多路复用允许一个线程同时监听多个 Socket。
常见 API:
| API |
平台 |
特点 |
| select |
跨平台较好 |
有 fd 数量限制 |
| poll |
Unix/Linux |
无固定 fd 限制 |
| epoll |
Linux |
高性能,适合大量连接 |
| kqueue |
BSD/macOS |
类似 epoll |
select
select 可以监听多个文件描述符。
缺点:
FD_SETSIZE 限制,通常为 1024
- 每次调用都需要重新设置 fd 集合
- 需要遍历所有 fd
简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <sys/select.h> #include <unistd.h> #include <iostream>
fd_set readfds; FD_ZERO(&readfds); FD_SET(sockfd, &readfds);
int ret = select(sockfd + 1, &readfds, nullptr, nullptr, nullptr);
if (ret > 0) { if (FD_ISSET(sockfd, &readfds)) { char buf[1024]; recv(sockfd, buf, sizeof(buf), 0); } }
|
poll
poll 使用数组管理 fd。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <poll.h>
struct pollfd fds[1]; fds[0].fd = sockfd; fds[0].events = POLLIN;
int ret = poll(fds, 1, -1);
if (ret > 0) { if (fds[0].revents & POLLIN) { char buf[1024]; recv(sockfd, buf, sizeof(buf), 0); } }
|
epoll
epoll 是 Linux 下高性能 IO 多路复用机制。
适合:
常用函数:
1 2 3
| epoll_create1() epoll_ctl() epoll_wait()
|
基本流程:
1 2 3 4
| epoll_create1() epoll_ctl(ADD) epoll_wait() 处理事件
|
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <sys/epoll.h> #include <unistd.h> #include <iostream>
int epfd = epoll_create1(0);
struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
struct epoll_event events[1024];
while (true) { int n = epoll_wait(epfd, events, 1024, -1);
for (int i = 0; i < n; ++i) { int fd = events[i].data.fd;
if (events[i].events & EPOLLIN) { char buf[1024]; ssize_t len = recv(fd, buf, sizeof(buf), 0);
if (len > 0) { } else if (len == 0) { close(fd); } } } }
|
LT 与 ET 模式
epoll 有两种触发模式:
| 模式 |
全称 |
特点 |
| LT |
Level Triggered,水平触发 |
只要缓冲区有数据,就一直通知 |
| ET |
Edge Triggered,边缘触发 |
状态变化时通知一次 |
LT 模式
默认模式。
特点:
- 编程简单
- 可以不一次性读完数据
- 只要数据还在,下次还会通知
ET 模式
使用 EPOLLET。
特点:
- 性能更好
- 必须配合非阻塞 IO
- 必须循环读到
EAGAIN
示例:
1
| ev.events = EPOLLIN | EPOLLET;
|
ET 模式读取示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| while (true) { char buf[1024]; ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n > 0) { } else if (n == 0) { close(fd); break; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; } else { close(fd); break; } } }
|