目录
引言:
多进程服务器
例程分享:
多线程服务器
例程分享:
I/O多路复用服务器
select
例程分享:
poll
例程分享:
epoll
例程分享:
总结建议
引言:
随着互联网的迅猛发展,服务器面临着越来越多的并发请求。如何设计一个能够高效处理大量并发请求的服务器成为了一个关键问题。本文将介绍几种常见的高并发服务器设计方案,包括多进程服务器、多线程服务器、I/O多路复用服务器和epoll服务器,并分析它们的优缺点,以便读者能够选择适合自己需求的设计方案。
多进程服务器
利用fork创建子进程处理每个连接请求。
优点:充分利用多核CPU的计算能力,隔离不同连接之间的资源。
缺点:父进程需要设置较大的文件描述符限制,进程创建和切换开销较大。
相关API函数:fork、waitpid、socket、bind、listen、accept、read、write、close
实现要点:
- 父进程close子进程socket,避免泄漏。
- 信号处理回收子进程。
- 每个子进程处理一个连接请求。
例程分享:
/*
服务器监听端口,接收客户端连接。
对每个连接fork子进程处理请求。
子进程循环接收客户端数据,转换大小写后返回。
父进程关闭连接socket,信号函数回收子进程。
客户端连接后循环发送接收数据。
使用多进程处理连接请求,充分利用多核CPU。
fork创建进程,waitpid和信号处理回收子进程。
父子进程同步处理,避免混乱。
*//* server.c */#include <stdio.h> // 标准IO头文件#include <string.h> // 字符串处理头文件#include <netinet/in.h> // socket编程头文件#include <arpa/inet.h> // IP地址转换头文件#include <signal.h> // 信号处理头文件#include <sys/wait.h> // 等待子进程头文件#include <sys/types.h> // 数据类型头文件#include "wrap.h" // socket函数封装头文件#define MAXLINE 80 // 最大读写字节数#define SERV_PORT 800 // 服务器端口号// 信号处理函数,回收子进程
void do_sigchild(int num) {while (waitpid(0, NULL, WNOHANG) > 0);
}int main(void) {// socket地址结构struct sockaddr_in servaddr, cliaddr;// 客户端地址长度socklen_t cliaddr_len;// 监听和连接socketint listenfd, connfd;// 数据缓冲区char buf[MAXLINE];// 客户端IP字符串char str[INET_ADDRSTRLEN];int i, n; // 循环变量和读字节数pid_t pid; // 进程ID// 安装信号处理函数struct sigaction newact;newact.sa_handler = do_sigchild;sigemptyset(&newact.sa_mask);newact.sa_flags = 0;sigaction(SIGCHLD, &newact, NULL);// 创建监听socketlistenfd = Socket(AF_INET, SOCK_STREAM, 0);// 初始化服务器地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);// 绑定地址和端口Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));// 设置监听队列长度Listen(listenfd, 20);// 循环接收客户端连接请求while (1) {Copy codecliaddr_len = sizeof(cliaddr);// 接收一个客户端连接connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);// Fork一个子进程处理连接pid = fork(); if (pid == 0) {// 子进程关闭监听socketClose(listenfd);// 处理客户端请求while (1) {// 接收客户端数据 n = Read(connfd, buf, MAXLINE);// 判断客户端是否关闭if (n == 0) {printf("the other side has been closed.\n");break;}// 打印客户端信息printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));// 转换为大写for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);// 发送转换后数据Write(connfd, buf, n);}// 关闭连接 Close(connfd);return 0;} else if (pid > 0) {// 父进程关闭连接socketClose(connfd); } elseperr_exit("fork");
}// 关闭监听socket
Close(listenfd);return 0;
}/* client.c */#include <stdio.h>#include <string.h>#include <unistd.h>#include <netinet/in.h>#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666int main(int argc, char *argv[]) {// 服务器地址结构struct sockaddr_in servaddr;// 数据缓冲区char buf[MAXLINE];// socket和读字节数int sockfd, n;// 创建socketsockfd = Socket(AF_INET, SOCK_STREAM, 0);// 初始化服务器地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT);// 连接服务器Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));// 循环发送接收数据while (fgets(buf, MAXLINE, stdin) != NULL) {Copy code// 发送数据到服务器Write(sockfd, buf, strlen(buf)); // 从服务器读取数据n = Read(sockfd, buf, MAXLINE);// 判断服务器是否关闭if (n == 0) {printf("the other side has been closed.\n");break;} else// 输出服务器返回数据Write(STDOUT_FILENO, buf, n);}// 关闭连接Close(sockfd);return 0;
}
多线程服务器
一个进程内创建线程处理每个连接请求。
优点:高效利用多核CPU,创建和销毁线程开销较小。
缺点:需要调整进程的文件描述符限制,需要进行线程同步,线程退出时需要进行资源清理。
相关API函数:pthread_create、pthread_detach、pthread_join、pthread_mutex_init、pthread_mutex_lock、pthread_mutex_unlock、socket、bind、listen、accept、read、write、close
要点:
- 调整进程文件描述符限制
- 共享数据同步
- 线程退出处理,防止资源泄漏
- 过多线程会降低性能
例程分享:
/*
服务器端创建监听套接字,绑定地址并监听。
主循环调用accept接收客户端连接,为每个客户端创建线程do_work处理请求。
do_work通过read接收客户端数据,转换为大写后返回。
客户端创建连接后,循环发送数据和读取服务器返回数据。
服务器使用线程处理每个连接请求,可以处理大量连接。
通过传递参数,线程可以获取客户端信息。
设置线程分离态,自动回收资源,实现高效的多线程服务器。
*//* server.c */#include <stdio.h> // 标准输入输出头文件#include <string.h> // 字符串处理头文件#include <netinet/in.h> // socket编程头文件#include <arpa/inet.h> // inet地址转换头文件#include <pthread.h> // 线程编程头文件#include "wrap.h" // 封装的socket函数头文件#define MAXLINE 80 //最大读取字符数#define SERV_PORT 6666 //服务器端口// 用于传递给线程的客户信息
struct s_info {
struct sockaddr_in cliaddr; // 客户socket地址
int connfd; // 客户端连接套接字
};// 线程处理函数
void *do_work(void *arg) {int n,i;struct s_info *ts = (struct s_info*)arg; // 获取传递的参数char buf[MAXLINE]; // 数据缓冲区char str[INET_ADDRSTRLEN]; // 存储socket地址的字符串// 设置线程为分离态,线程结束时自动释放资源pthread_detach(pthread_self());while (1) {// 获取客户端发送的数据n = Read(ts->connfd, buf, MAXLINE); if (n == 0) { // 对端关闭连接printf("the other side has been closed.\n");break;}// 打印客户端信息printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),ntohs((*ts).cliaddr.sin_port));// 将数据转换为大写for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);// 发送转换后的数据Write(ts->connfd, buf, n);}// 关闭客户端连接Close(ts->connfd);
}int main(void) {struct sockaddr_in servaddr, cliaddr; // 本地和客户端的socket地址socklen_t cliaddr_len; // 客户端socket地址长度int listenfd, connfd; // 监听和连接套接字int i = 0; pthread_t tid; // 线程idstruct s_info ts[256]; // 存储所有客户端信息的数组// 创建监听套接字listenfd = Socket(AF_INET, SOCK_STREAM, 0);// 初始化本地socket地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);// 绑定监听套接字到本地地址Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));// 设置监听队列长度Listen(listenfd, 20);printf("Accepting connections ...\n");// 循环接收客户端连接while (1) { // 接收一个客户端连接cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);// 保存客户信息ts[i].cliaddr = cliaddr; ts[i].connfd = connfd;// 为客户端创建线程处理请求pthread_create(&tid, NULL, do_work, (void*)&ts[i]);i++;}return 0;
}/* client.c */#include <stdio.h>#include <string.h>#include <unistd.h>#include <netinet/in.h>#include "wrap.h"#define MAXLINE 80
#define SERV_PORT 6666int main(int argc, char *argv[])struct sockaddr_in servaddr; // 服务器端地址结构char buf[MAXLINE]; // 数据缓冲区int sockfd, n; // 套接字和读返回值// 创建流式套接字sockfd = Socket(AF_INET, SOCK_STREAM, 0);// 初始化服务器地址结构bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT);// 连接服务器Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));// 循环发送数据和读取服务器返回数据while (fgets(buf, MAXLINE, stdin) != NULL) { // 向服务器发送数据Write(sockfd, buf, strlen(buf)); // 从服务器读取数据n = Read(sockfd, buf, MAXLINE);// 判断服务器是否关闭if (n == 0)printf("the other side has been closed.\n");else// 输出服务器返回数据Write(STDOUT_FILENO, buf, n);}// 关闭socket连接Close(sockfd);return 0;
}
I/O多路复用服务器
select/poll/epoll使单线程可以同时处理多个连接请求。
select
优点: 可移植,使用简单。
缺点: 连接数受限,监听效率低。
要点:
- select监听读写事件
- 每次循环重置监听描述符
- 根据返回就绪数遍历处理事件
- 根据描述符状态处理连接关闭等
例程分享:
/*
服务器端初始化socket地址,创建监听套接字。
使用select()监听套接字可读事件。
调用accept()接收客户端连接请求。
添加新的连接到select监听的文件描述符集。
循环扫描就绪文件描述符,调用read()接收客户端数据。
对数据进行处理后,调用write()返回给客户端。
客户端创建连接后,循环read()和write()实现双向通信。
服务器使用select()处理多个连接,但依然是同步阻塞模型
*//* server.c *//* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号int main(int argc, char *argv[])
{int i, maxi, maxfd, listenfd, connfd, sockfd;int nready, client[FD_SETSIZE]; // FD_SETSIZE通常为1024ssize_t n;fd_set rset, allset; // 用于select()的文件描述符集char buf[MAXLINE]; // 数据缓冲区char str[INET_ADDRSTRLEN]; // 地址字符串缓冲区socklen_t cliaddr_len;struct sockaddr_in cliaddr, servaddr; // 客户端和服务器地址结构listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socketbzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构servaddr.sin_family = AF_INET; // 设置地址族为IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任何接口servaddr.sin_port = htons(SERV_PORT); // 设置端口号Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 将socket绑定到地址Listen(listenfd, 20); // 监听连接,队列长度为20maxfd = listenfd; // 初始化maxfdmaxi = -1; // 初始化maxifor (i = 0; i < FD_SETSIZE; i++)client[i] = -1; // 初始化client[]FD_ZERO(&allset); // 清空allsetFD_SET(listenfd, &allset); // 将listenfd添加到allsetfor ( ; ; ) { // 主服务器循环rset = allset; // 每次循环都重置rsetnready = select(maxfd+1, &rset, NULL, NULL, NULL); // 调用select()if (nready < 0)perr_exit("select error"); // 如果select()返回错误,退出if (FD_ISSET(listenfd, &rset)) { // 如果有新的客户端正在连接cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); // 接受新的连接printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); // 打印客户端的地址和端口for (i = 0; i < FD_SETSIZE; i++) {if (client[i] < 0) {client[i] = connfd; // 将接受的文件描述符保存在client[]中break;}}if (i == FD_SETSIZE) {fputs("too many clients\n", stderr); // 如果连接的客户端过多,打印错误并退出exit(1);}FD_SET(connfd, &allset); // 将新的文件描述符添加到allsetif (connfd > maxfd)maxfd = connfd; // 如果需要,更新maxfdif (i > maxi)maxi = i; // 如果需要,更新maxiif (--nready == 0)continue; // 如果没有更多的就绪文件描述符,继续下一次循环}for (i = 0; i <= maxi; i++) { // 检查哪些客户端有数据准备好读取if ( (sockfd = client[i]) < 0)continue;if (FD_ISSET(sockfd, &rset)) {if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {Close(sockfd); // 如果客户端已经关闭了连接,也关闭服务器端FD_CLR(sockfd, &allset); // 从allset中移除文件描述符client[i] = -1; // 从client[]中移除文件描述符} else {int j;for (j = 0; j < n; j++)buf[j] = toupper(buf[j]); // 将数据转换为大写Write(sockfd, buf, n); // 将数据写回客户端}if (--nready == 0)break; // 如果没有更多的就绪文件描述符,跳出循环}}}close(listenfd); // 关闭监听的socketreturn 0;
}/* client.c *//* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号int main(int argc, char *argv[])
{struct sockaddr_in servaddr; // 服务器地址结构char buf[MAXLINE]; // 数据缓冲区int sockfd, n; // Socket文件描述符和读取的字节数sockfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socketbzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构servaddr.sin_family = AF_INET; // 设置地址族为IPv4inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 设置服务器的IP地址servaddr.sin_port = htons(SERV_PORT); // 设置端口号Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 连接到服务器while (fgets(buf, MAXLINE, stdin) != NULL) { // 主客户端循环Write(sockfd, buf, strlen(buf)); // 将输入写入服务器n = Read(sockfd, buf, MAXLINE); // 读取服务器的响应if (n == 0)printf("the other side has been closed.\n"); // 如果服务器已经关闭了连接,打印一条消息elseWrite(STDOUT_FILENO, buf, n); // 将服务器的响应写入stdout}Close(sockfd); // 关闭socketreturn 0;
}
poll
优点: 没有连接数限制。
缺点: 依然轮询模型,效率低。
要点:
- pollfd结构体监听事件
- 每次循环遍历pollfd处理就绪事件
- 根据返回事件和错误处理连接状态
例程分享:
/*
服务器端:
创建监听socket,绑定地址并监听
使用pollfd数组保存所有连接的文件描述符
调用poll监听socket上的事件,主要是POLLRDNORM读事件
当监听socket有事件时,表示有新连接,调用accept获取新连接
将新连接的文件描述符添加到pollfd数组中,继续监听读事件
当连接socket有读事件时,调用read读取客户端数据
对数据进行转换处理,调用write将数据返回给客户端
根据返回事件和错误情况判断连接是否正常
客户端:
创建socket,连接服务器地址
循环调用read读取用户输入
调用write将用户输入发送给服务器
调用read读取服务器返回数据
将服务器数据输出到标准输出
关闭连接
*//* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号
#define OPEN_MAX 1024 // 最大的打开文件描述符数量int main(int argc, char *argv[])
{int i, j, maxi, listenfd, connfd, sockfd;int nready;ssize_t n;char buf[MAXLINE], str[INET_ADDRSTRLEN];socklen_t clilen;struct pollfd client[OPEN_MAX]; // pollfd结构体数组,用于存储多个文件描述符struct sockaddr_in cliaddr, servaddr; // 客户端和服务器地址结构listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socketbzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构servaddr.sin_family = AF_INET; // 设置地址族为IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任何接口servaddr.sin_port = htons(SERV_PORT); // 设置端口号Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 将socket绑定到地址Listen(listenfd, 20); // 监听连接,队列长度为20client[0].fd = listenfd;client[0].events = POLLRDNORM; // listenfd监听普通读事件for (i = 1; i < OPEN_MAX; i++)client[i].fd = -1; // 用-1初始化client[]里剩下元素maxi = 0; // client[]数组有效元素中最大元素下标for ( ; ; ) { // 主服务器循环nready = poll(client, maxi+1, -1); // 调用poll()函数,阻塞等待文件描述符就绪if (client[0].revents & POLLRDNORM) { // 有客户端链接请求clilen = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); // 接受新的连接printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); // 打印客户端的地址和端口for (i = 1; i < OPEN_MAX; i++) {if (client[i].fd < 0) {client[i].fd = connfd; // 找到client[]中空闲的位置,存放accept返回的connfdbreak;}}if (i == OPEN_MAX)perr_exit("too many clients"); // 如果连接的客户端过多,打印错误并退出client[i].events = POLLRDNORM; // 设置刚刚返回的connfd,监控读事件if (i > maxi)maxi = i; // 更新client[]中最大元素下标if (--nready <= 0)continue; // 没有更多就绪事件时,继续回到poll阻塞}for (i = 1; i <= maxi; i++) { // 检测client[]if ((sockfd = client[i].fd) < 0)continue;if (client[i].revents & (POLLRDNORM | POLLERR)) { // 如果有数据可读或者有错误发生if ((n = Read(sockfd, buf, MAXLINE)) < 0) {if (errno == ECONNRESET) { // 当收到 RST标志时/* connection reset by client */printf("client[%d] aborted connection\n", i);Close(sockfd); // 关闭socketclient[i].fd = -1; // 从client[]中移除文件描述符} else {perr_exit("read error"); // 如果读取错误,退出}} else if (n == 0) {/* connection closed by client */printf("client[%d] closed connection\n", i);Close(sockfd); // 关闭socketclient[i].fd = -1; // 从client[]中移除文件描述符} else {for (j = 0; j < n; j++)buf[j] = toupper(buf[j]); // 将数据转换为大写Writen(sockfd, buf, n); // 将数据写回客户端}if (--nready <= 0)break; // 如果没有更多的就绪文件描述符,跳出循环}}}return 0;
}/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h" // 这是一个自定义的头文件,封装了socket函数#define MAXLINE 80 // 缓冲区的最大长度
#define SERV_PORT 6666 // 服务器端口号int main(int argc, char *argv[])
{struct sockaddr_in servaddr; // 服务器地址结构char buf[MAXLINE]; // 数据缓冲区int sockfd, n; // Socket文件描述符和读取的字节数sockfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socketbzero(&servaddr, sizeof(servaddr)); // 清零服务器地址结构servaddr.sin_family = AF_INET; // 设置地址族为IPv4inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 设置服务器的IP地址servaddr.sin_port = htons(SERV_PORT); // 设置端口号Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 连接到服务器while (fgets(buf, MAXLINE, stdin) != NULL) { // 主客户端循环Write(sockfd, buf, strlen(buf)); // 将输入写入服务器n = Read(sockfd, buf, MAXLINE); // 读取服务器的响应if (n == 0)printf("the other side has been closed.\n"); // 如果服务器已经关闭了连接,打印一条消息elseWrite(STDOUT_FILENO, buf, n); // 将服务器的响应写入stdout}Close(sockfd); // 关闭socketreturn 0;
}
epoll
优点:提高程序在大量并发连接中的系统CPU利用率,能够高效处理大量并发请求。
缺点:需要调整进程的文件描述符限制,需要进行连接管理。
要点:
- epoll_create创建句柄
- epoll_ctl注册和控制事件
- epoll_wait等待就绪事件
- 根据就绪事件处理请求
例程分享:
/*
服务器端:
服务器端的代码主要完成以下任务:创建一个TCP socket并绑定到指定的IP地址和端口。
使用epoll创建一个事件监听列表,并将监听socket添加到这个列表中。
进入一个无限循环,使用epoll_wait()函数等待事件的发生。
当新的客户端连接时,接受连接并将新的socket添加到epoll的监听列表中。
当已连接的客户端发送数据时,读取数据,将数据转换为大写,然后将数据回写到客户端。
当客户端关闭连接时,从epoll的监听列表中移除这个socket,并关闭这个socket。
客户端端:
客户端的代码主要完成以下任务:创建一个TCP socket并连接到服务器。
进入一个无限循环,从stdin读取输入,将输入写入服务器,然后读取服务器的响应并将响应写入stdout。
当服务器关闭连接时,打印一条消息并退出循环。
*//* server.c */
// ...省略部分代码...// 创建一个TCP socket
listenfd = Socket(AF_INET, SOCK_STREAM, 0);// 清零服务器地址结构
bzero(&servaddr, sizeof(servaddr));
// 设置地址族为IPv4
servaddr.sin_family = AF_INET;
// 监听任何接口
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 设置端口号
servaddr.sin_port = htons(SERV_PORT);// 将socket绑定到地址
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));// 监听连接,队列长度为20
Listen(listenfd, 20);// 创建epoll实例
efd = epoll_create(OPEN_MAX);
if (efd == -1)perr_exit("epoll_create");// 设置监听事件为EPOLLIN(可读事件),并将listenfd添加到epoll的监听列表中
tep.events = EPOLLIN; tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)perr_exit("epoll_ctl");// 主服务器循环
while (1) {// 调用epoll_wait()函数,阻塞等待文件描述符就绪nready = epoll_wait(efd, ep, OPEN_MAX, -1);if (nready == -1)perr_exit("epoll_wait");// 遍历就绪的文件描述符for (i = 0; i < nready; i++) {// 如果不是可读事件,跳过if (!(ep[i].events & EPOLLIN))continue;// 如果是新的客户端连接if (ep[i].data.fd == listenfd) {// 接受新的连接clilen = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));// 将新的socket添加到epoll的监听列表中tep.events = EPOLLIN; tep.data.fd = connfd;res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);if (res == -1)perr_exit("epoll_ctl");} else {// 如果是已连接的客户端发送的数据sockfd = ep[i].data.fd;n = Read(sockfd, buf, MAXLINE);if (n == 0) {// 如果客户端关闭了连接,从epoll的监听列表中移除这个socket,并关闭这个socketres = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);if (res == -1)perr_exit("epoll_ctl");Close(sockfd);printf("client[%d] closed connection\n", j);} else {// 如果接收到数据,将数据转换为大写,然后将数据回写到客户端for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Writen(sockfd, buf, n);}}}
}
// 关闭监听socket和epoll实例
close(listenfd);
close(efd);
return 0;/* client.c */
// ...省略部分代码...// 创建一个TCP socket
sockfd = Socket(AF_INET, SOCK_STREAM, 0);// 清零服务器地址结构
bzero(&servaddr, sizeof(servaddr));
// 设置地址族为IPv4
servaddr.sin_family = AF_INET;
// 设置服务器的IP地址
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
// 设置端口号
servaddr.sin_port = htons(SERV_PORT);// 连接到服务器
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));// 主客户端循环
while (fgets(buf, MAXLINE, stdin) != NULL) {// 将输入写入服务器Write(sockfd, buf, strlen(buf));// 读取服务器的响应n = Read(sockfd, buf, MAXLINE);if (n == 0)// 如果服务器已经关闭了连接,打印一条消息printf("the other side has been closed.\n");else// 将服务器的响应写入stdoutWrite(STDOUT_FILENO, buf, n);
}// 关闭socket
Close(sockfd);
return 0;
总结建议
epoll服务器根据不同的需求和场景,我们可以选择不同的高并发服务器设计方案。多进程服务器、多线程服务器、I/O多路复用服务器和epoll服务器都有各自的优缺点和适用场景。通过分享的例程和相关API函数的介绍,读者可以更好地理解和选择适合自己需求的设计方案,从而高效处理大量并发请求,满足互联网快速发展的需求。