不同IO模型服务器的简单实现
1. 阻塞 I/O(Blocking I/O)
在阻塞 I/O 模型下,进程在执行 I/O 操作时会被阻塞,直到数据被成功读取或写入。这个模型是最基础的模型,适用于连接较少的应用场景。每个连接都会阻塞主线程,直到完成相应的 I/O 操作。
代码示例:阻塞 I/O 服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 2048 // 服务器端口
#define BUFFER_SIZE 128 // 缓冲区大小
int main() {
int sockfd, clientfd; // 服务端套接字和客户端套接字
struct sockaddr_in serveraddr, clientaddr; // 服务器和客户端地址结构体
socklen_t len = sizeof(clientaddr); // 用于接收客户端地址长度的变量
char buffer[BUFFER_SIZE]; // 数据缓冲区
// 创建服务端套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 使用 IPv4 地址族,TCP 流套接字
if (sockfd == -1) { // 错误检查
perror("socket"); // 打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
memset(&serveraddr, 0, sizeof(serveraddr)); // 初始化服务器地址
serveraddr.sin_family = AF_INET; // 设置协议族为 IPv4
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网络接口
serveraddr.sin_port = htons(PORT); // 绑定端口
// 将套接字绑定到指定地址和端口
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
perror("bind"); // 打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 开始监听客户端连接
if (listen(sockfd, 10) == -1) {
perror("listen"); // 打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
printf("Server listening on port %d...\n", PORT);
// 主循环,等待客户端连接
while (1) {
// 阻塞接收客户端连接请求
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd == -1) {
perror("accept"); // 错误处理
continue; // 继续监听其他客户端连接
}
// 阻塞接收客户端的数据
int count = recv(clientfd, buffer, BUFFER_SIZE, 0);
if (count == -1) {
perror("recv"); // 错误处理
close(clientfd); // 关闭当前客户端连接
continue; // 继续监听
}
buffer[count] = '\0'; // 确保接收到的数据是以 '\0' 结尾的字符串
// 打印接收到的数据
printf("Received: %s\n", buffer);
// 阻塞发送数据回客户端
send(clientfd, buffer, count, 0);
close(clientfd); // 关闭客户端连接
}
close(sockfd); // 关闭服务器套接字
return 0;
}
解释:
socket()
: 创建一个 TCP 套接字。bind()
: 将套接字绑定到指定的端口和地址上。listen()
: 启动监听,等待客户端连接。accept()
: 阻塞地接受客户端的连接请求。如果没有连接,程序将一直阻塞。recv()
: 阻塞地读取客户端数据。如果客户端没有发送数据,recv()
将一直阻塞。send()
: 阻塞地将数据发送回客户端。
2. 非阻塞 I/O(Non-blocking I/O)
在非阻塞 I/O 模型下,I/O 操作不会阻塞进程。如果数据不可用,调用会立即返回,而不会让进程等待。通常会配合 errno
来检查是否需要重试。
代码示例:非阻塞 I/O 服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#define PORT 2048
#define BUFFER_SIZE 128
int main() {
int sockfd, clientfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t len = sizeof(clientaddr);
char buffer[BUFFER_SIZE];
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置套接字为非阻塞
fcntl(sockfd, F_SETFL, O_NONBLOCK);
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(PORT);
// 绑定
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(sockfd, 10) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd == -1) {
// 如果客户端连接不可用,则跳过并继续监听
if (errno != EWOULDBLOCK) {
perror("accept");
}
continue;
}
// 非阻塞读取数据
int count = recv(clientfd, buffer, BUFFER_SIZE, 0);
if (count > 0) {
buffer[count] = '\0';
printf("Received: %s\n", buffer);
// 非阻塞发送数据回客户端
send(clientfd, buffer, count, 0);
}
close(clientfd); // 关闭客户端连接
}
close(sockfd); // 关闭服务器套接字
return 0;
}
解释:
- 使用
fcntl()
函数将套接字设置为非阻塞模式。这样recv()
和accept()
都不会阻塞进程。如果没有数据或没有连接可用,函数将立即返回并设置errno
为EAGAIN
或EWOULDBLOCK
。 - 适用于需要处理大量连接的场景,但需要增加对
errno
错误的处理和重试机制。
3. 多路复用 I/O(I/O Multiplexing)
多路复用 I/O 允许服务器监控多个文件描述符(如多个客户端连接),并在某个文件描述符准备好进行 I/O 操作时进行处理。常用的方法是 select()
,poll()
或 epoll()
。
代码示例:多路复用 I/O 服务器(使用 select()
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <arpa/inet.h>
#define PORT 2048
#define BUFFER_SIZE 128
#define MAX_CLIENTS 10
int main() {
int sockfd, clientfd, maxfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(clientaddr);
char buffer[BUFFER_SIZE];
fd_set read_fds, temp_fds;
// 创建服务器套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(PORT);
// 绑定
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 开始监听
if (listen(sockfd, MAX_CLIENTS) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
// 初始化读文件描述符集合
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
maxfd = sockfd;
printf("Server listening on port %d...\n", PORT);
while (1) {
// 复制文件描述符集合,用于 select()
temp_fds = read_fds;
// 调用 select 等待多个文件描述符的状态变化
int activity = select(maxfd + 1, &temp_fds, NULL, NULL, NULL);
if (activity == -1) {
perror("select");
exit(EXIT_FAILURE);
}
// 如果有新的连接请求,accept 连接
if (FD_ISSET(sockfd, &temp_fds)) {
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &addrlen);
if (clientfd == -1) {
perror("accept");
continue;
}
FD_SET(clientfd, &read_fds); // 添加新客户端到监视列表
if (clientfd > maxfd) {
maxfd = clientfd; // 更新最大文件描述符
}
printf("New connection from %s\n", inet_ntoa(clientaddr.sin_addr));
}
// 检查每个已连接的客户端
for (int i = 0; i <= maxfd; i++) {
if (FD_ISSET(i, &temp_fds)) {
int count = recv(i, buffer, BUFFER_SIZE, 0);
if (count == 0) {
// 客户端关闭连接
close(i);
FD_CLR(i, &read_fds);
} else if (count > 0) {
buffer[count] = '\0';
printf("Received: %s\n", buffer);
send(i, buffer, count, 0); // 发送回客户端
}
}
}
}
close(sockfd); // 关闭服务器套接字
return 0;
}
解释:
- 使用
select()
监控多个客户端连接。FD_SET()
将套接字添加到文件描述符集合中,FD_ISSET()
检查文件描述符是否就绪。 - 适用于处理中等规模的并发连接,使用
select()
可同时监控多个连接。
4. 信号驱动 I/O(Signal-driven I/O)
信号驱动 I/O 在套接字准备好进行 I/O 操作时,会向进程发送一个信号,通知进程去处理该 I/O 操作。进程会注册一个信号处理函数来响应。
5. 异步 I/O(Asynchronous I/O)
异步 I/O 是最先进的 I/O 模型,进程发出 I/O 请求后,不需要阻塞,操作系统在操作完成时通知进程。这通常通过回调函数来处理。
由于信号驱动 I/O 和异步 I/O 在实现上更复杂且需要额外的支持(例如 aio
库或操作系统提供的特性),因此在这里只提供了阻塞、非阻塞和多路复用 I/O 模型的详细实现和解释。