网络 IO 的本质:

等待数据准备好,并将数据从内核缓冲区拷贝到用户缓冲区。

常见 IO 模型:

  1. 阻塞 IO
  2. 非阻塞 IO
  3. IO 多路复用
  4. 信号驱动 IO
  5. 异步 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 {
// 其他错误
}
}

优点:

  • 不会因为一个连接阻塞整个线程

缺点:

  • 需要不断轮询,浪费 CPU

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;
}
}
}