从前面的知识学习了如何通过socket ,多进程,多线程创建一个高并发服务器,但是在实际工作中,我们并不会用到前面的方法 去弄一个高并发服务器,有更加好用的方法,就是多路I/O转接器
零 多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种 select , poll , epoll
其中select 可以跨平台
poll 和 epoll不能跨平台,只能在linux上使用。
重点是epoll
次重点是 select
poll知道就行了,再学习后我们会知道原因。
本章主要学习 select。
一 select
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
- 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。
select 函数借助内核监听两件事,:客户端连接,数据通信事件。
核心函数: select函数 ,FD_CLR,FD_ISSET,FD_SET,FD_ZERO
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd位清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd位清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
返回值:所有监听的集合中,满足事件发生的总数
server 端代码实现1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <sys/select.h>
#include "wrap.h"
//select 实现高并发代码示例
int main() {
int listenfd;
int ret = 0;
//第一步:创建socket。 打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//第二步 build IP+PORT到网络地址 到socket 创建出来的listenfd
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);
Bind(listenfd, (struct sockaddr *) &servaddr,sizeof(servaddr));
//第三步,设置可以同时监听的最大的数量为1024
Listen(listenfd, 1024);
//第四步,这时候
//void FD_CLR(int fd, fd_set *set);
//int FD_ISSET(int fd, fd_set *set);
//void FD_SET(int fd, fd_set *set);
//void FD_ZERO(fd_set *set);
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(listenfd, &readfds);
fd_set allfds = readfds;
int maxfd = listenfd;
//第五步,这时候就要弄一个循环去监听了。
//第一次的时候,肯定是只监听了listenfd一个,都是后面,如果有cfd连接上的话,那就监听的多了
//因此要使用一个maxfd 作为备份记录
//select 函数的意义是:
// int select(int nfds, fd_set *readfds, fd_set *writefds,
// fd_set *exceptfds, struct timeval *timeout);
//
// nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
// readfds: 监控有读数据到达文件描述符集合,传入传出参数
// writefds: 监控写数据到达文件描述符集合,传入传出参数
// exceptfds: 监控异常发生达文件描述符集合, 如带外数据到达异常,传入传出参数
// timeout: 定时阻塞监控时间,3种情况
// 1.NULL,永远等下去
// 2.设置timeval,等待固定时间
// 3.设置timeval里时间均为0,检查描述字后立即返回,轮询
// struct timeval {
// long tv_sec; /* seconds */
// long tv_usec; /* microseconds */
//};返回值:所有监听的集合中,满足事件发生的总数
int nready = 0;
struct sockaddr_in clie_addr;
int clie_addrlen = sizeof(clie_addr);
int connectfd = 0;
char buf[BUFSIZ];
char str[INET_ADDRSTRLEN];
while (1) {
readfds = allfds;
nready = select(maxfd + 1, &readfds,NULL,NULL,NULL);
//nready为所有监听的集合中,满足事件发生的总数
//第三个参数timeval我们设置的是NULL,表示阻塞等待
//因此如果有事件发生,那么就会走到后面的代码,且readfds集合中会改动成为真正
//有监听事件发生后,第一步做错误判断处理
if (nready < 0 ) {
//说明有error发生
perr_exit("select error");
}
//第六步,判断nready中是否有 新的连接事件发生,也就是说listenfd是否
if (FD_ISSET(listenfd, &readfds)) {
//走到这里,说明有新的链接过来了,那么我们要做如下的几件事
//6.1 server赶紧连接,注意,这时候并不会阻塞,因为listenfd已经有了读取事件才会走到这里
connectfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addrlen);
//这里加一行log,目的是将连接的客户端的信息打印出来
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
//这里添加一行判断,如果connectedfd已经超过1024,则不支持,select对于每个进程或者线程只能支持最多1024个
if (connectfd == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
//6.2 将这个新的链接添加到 监听的readfds中
FD_SET(connectfd,&allfds); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
//6.3 更新maxfd
if (connectfd > maxfd) {
maxfd = connectfd; /* 有可能最大的connectfd 需要变化,这是因为当有新的connectfd之前,如果没有任何的connectfd没有断开,则会变成最大,但是如果中间有connect断开,则这个新的connectfd的值,有可能不是最大。select第一个参数需要 */
//假设之前来了,4,5,6,这时候客户端4已经断开了,这时候 connectfd就等于4,如果4,5,6都没有断开过,则这时候新来的会是7
}
//6.4 判断select 的返回值 是否只有一个,且是listenfd事件,如果走到这里说明这一个已经被处理了,因此就没有必要往下再执行了
if (--nready == 0 ) {
continue;
}
}
//第七步,走到这里说明nready中有一个或者多个 客户端发送数据的事件过来。
//我们当前的写法是通过循环1024次,挨个往出拿,/* 检测哪个clients 有数据就绪 */
char buf[BUFSIZ] = {0};
int realreadnum = 0;
for (int i = listenfd+1; i < 1024;++i) {
bzero(buf, BUFSIZ);
if (FD_ISSET(i, &readfds)) {
REREADPOINT:
realreadnum = Read(i, buf, BUFSIZ); //真正的读取到了数据
if (realreadnum == 0 ) {//在网络环境下,read函数返回0,说明是对端关闭了,也就是说,客户端关闭了
//那么就应该关闭当前的connect端,并将该监听从 allfds中 移除
Close(i);
FD_CLR(i, &allfds);
}
else if (realreadnum == -1) {
if (errno == EINTR) {
//说明是被信号打断的,一般要重新read
printf("信号打断\n");
goto REREADPOINT;
}
else if(errno == EAGAIN || errno == EWOULDBLOCK)
{
printf(" WOULDBLOCK \n");
//说明在打开文件的时候是使用的O_NONBLOCK方式打开的,但是没有读取到数据
//当前代码是不会走到这里的,因为前面代码select的最后一个参数用的NULL,是阻塞的
//一般在这里 也要重新读,但是这里有个问题,如果一直都读取不到,会不会死循环?
goto REREADPOINT;
}
else if (errno == ECONNRESET) {
//ECONNRESET 说明连接被重置了,因此要将该cfd关闭,并重新移除监听队列
Close(i);
FD_CLR(i, &allfds);
}
else {
//这就是真正的有问题了,注意这里不要exit程序,应该只是让打印log
//不退出程序是因为,这时候还有其他的链接连上的
perror("read num <0");
}
}
else if (realreadnum > 0 ) {
//真正的读取到了客户端发送过来的数据
for (int j = 0; j < realreadnum;++j) {
buf[j] = toupper(buf[j]);
}
Write(i, buf,realreadnum);
Write(STDOUT_FILENO,buf,realreadnum);
}
if (--nready == 0) { //有可能50个已经连接上的链接中,这时候只有3个有数据发送过来了,还是要从50个中遍历,但是不同的是,假设是 5,19,30,那么写这一块代码就能节省时间了,当遍历到30后,--nready 就会等于0,直接跳出
break; /* 跳出for, 但还在while中 */
}
}
}
}
return ret;
}
server 端代码优化
可以看到,select函数在有客户端和服务器端沟通的过程中,需要依次遍历对比知道具体是哪一个connectfd有数据发送过来,这很影响工作效率,因此有了如下的优化。
优化的整体思路是:当有了connectfd链接后,就将这些connectfd记录到另一个数组中,并记录这个数组的最大下标。当客户端的链接断开的时候,则将当前的下标重置
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <sys/select.h>
#include "wrap.h"
//从select1.c我们看到,代码还有可以完善的地方,这里就完善一下
//1.最后每次都要循环1020次,遍历查看哪些cfd有数据发送过来,这是不合理的
//因此我们需要搞一个数组,用这个数组记录真正的有数据访问的cfd
//并且弄一个int 值,记录这个数组的最大下标 maxi
//select 实现高并发代码示例
int main() {
int listenfd;
//额外添加代码 start
int client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxi;
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
for (int i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
//额外添加代码 end
int ret = 0;
//第一步:创建socket。 打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//端口复用的代码添加上,端口复用的代码需要写在bind之前,socket之后
int opt = 1; //1表示可以让端口复用,0表示不让端口复用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//第二步 build IP+PORT到网络地址 到socket 创建出来的listenfd
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
//第三步,设置可以同时监听的最大的数量为1024
Listen(listenfd, 1024);
//第四步,这时候
//void FD_CLR(int fd, fd_set *set);
//int FD_ISSET(int fd, fd_set *set);
//void FD_SET(int fd, fd_set *set);
//void FD_ZERO(fd_set *set);
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(listenfd, &readfds);
fd_set allfds = readfds;
int maxfd = listenfd;
//第五步,这时候就要弄一个循环去监听了。
//第一次的时候,肯定是只监听了listenfd一个,都是后面,如果有cfd连接上的话,那就监听的多了
//因此要使用一个maxfd 作为备份记录
//select 函数的意义是:
// int select(int nfds, fd_set *readfds, fd_set *writefds,
// fd_set *exceptfds, struct timeval *timeout);
//
// nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
// readfds: 监控有读数据到达文件描述符集合,传入传出参数
// writefds: 监控写数据到达文件描述符集合,传入传出参数
// exceptfds: 监控异常发生达文件描述符集合, 如带外数据到达异常,传入传出参数
// timeout: 定时阻塞监控时间,3种情况
// 1.NULL,永远等下去
// 2.设置timeval,等待固定时间
// 3.设置timeval里时间均为0,检查描述字后立即返回,轮询
// struct timeval {
// long tv_sec; /* seconds */
// long tv_usec; /* microseconds */
//};返回值:所有监听的集合中,满足事件发生的总数
int nready = 0;
struct sockaddr_in clie_addr;
int clie_addrlen = sizeof(clie_addr);
int connectfd = 0;
char buf[BUFSIZ];
char str[INET_ADDRSTRLEN];
while (1) {
readfds = allfds;
nready = select(maxfd + 1, &readfds, NULL, NULL, NULL);
//nready为所有监听的集合中,满足事件发生的总数
//第三个参数timeval我们设置的是NULL,表示阻塞等待
//因此如果有事件发生,那么就会走到后面的代码,且readfds集合中会改动成为真正
//有监听事件发生后,第一步做错误判断处理
if (nready < 0) {
//说明有error发生
perr_exit("select error");
}
//第六步,判断nready中是否有 新的连接事件发生,也就是说listenfd是否
if (FD_ISSET(listenfd, &readfds)) {
//走到这里,说明有新的链接过来了,那么我们要做如下的几件事
//6.1 server赶紧连接,注意,这时候并不会阻塞,因为listenfd已经有了读取事件才会走到这里
connectfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addrlen);
//这里加一行log,目的是将连接的客户端的信息打印出来
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
//额外添加代码 start
int i = 0;
for (; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
client[i] = connectfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
//额外添加代码 end
//这里添加一行判断,如果connectedfd已经超过1024,则不支持,select对于每个进程或者线程只能支持最多1024个
if (connectfd == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
//6.2 将这个新的链接添加到 监听的readfds中
FD_SET(connectfd, &allfds); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
//6.3 更新maxfd
if (connectfd > maxfd) {
maxfd = connectfd; /* 有可能最大的connectfd 需要变化,这是因为当有新的connectfd之前,如果没有任何的connectfd没有断开,则会变成最大,但是如果中间有connect断开,则这个新的connectfd的值,有可能不是最大。select第一个参数需要 */
//假设之前来了,4,5,6,这时候客户端4已经断开了,这时候 connectfd就等于4,如果4,5,6都没有断开过,则这时候新来的会是7
}
//额外添加代码 start
if (i > maxi) {
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
}
//额外添加代码 end
//6.4 判断select 的返回值 是否只有一个,且是listenfd事件,如果走到这里说明这一个已经被处理了,因此就没有必要往下再执行了
if (--nready == 0) {
continue;
}
}
//第七步,走到这里说明nready中有一个或者多个 客户端发送数据的事件过来。
//我们当前的写法是通过循环1024次,挨个往出拿,/* 检测哪个clients 有数据就绪 */
char buf[BUFSIZ] = { 0 };
int realreadnum = 0;
//for (int i = listenfd + 1; i < 1024; ++i) {
// bzero(buf, BUFSIZ);
// if (FD_ISSET(i, &readfds)) {
// REREADPOINT:
// realreadnum = Read(i, buf, BUFSIZ); //真正的读取到了数据
// if (realreadnum == 0) {//在网络环境下,read函数返回0,说明是真的读取到文件末尾了
// //既然已经读取到了文件末尾,那么就应该关闭当前的connect端,并将该监听从 allfds中 移除
// Close(i);
// FD_CLR(i, &allfds);
// }
// else if (realreadnum == -1) {
// if (errno == EINTR) {
// //说明是被信号打断的,一般要重新read
// printf("信号打断\n");
// goto REREADPOINT;
// }
// else if (errno == EAGAIN || errno == EWOULDBLOCK)
// {
// printf(" WOULDBLOCK \n");
// //说明在打开文件的时候是使用的O_NONBLOCK方式打开的,但是没有读取到数据
// //当前代码是不会走到这里的,因为前面代码select的最后一个参数用的NULL,是阻塞的
// //一般在这里 也要重新读,但是这里有个问题,如果一直都读取不到,会不会死循环?
// goto REREADPOINT;
// }
// else if (errno == ECONNRESET) {
// //ECONNRESET 说明连接被重置了,因此要将该cfd关闭,并重新移除监听队列
// Close(i);
// FD_CLR(i, &allfds);
// }
// else {
// //这就是真正的有问题了,注意这里不要exit程序,应该只是让打印log
// //不退出程序是因为,这时候还有其他的链接连上的
// perror("read num <0");
// }
// }
// else if (realreadnum > 0) {
// //真正的读取到了客户端发送过来的数据
// for (int j = 0; j < realreadnum; ++j) {
// buf[j] = toupper(buf[j]);
// }
// Write(i, buf, realreadnum);
// Write(STDOUT_FILENO, buf, realreadnum);
// }
// }
//}
//额外添加代码 start
int sockfd = 0;
for (int i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ((sockfd = client[i]) < 0)
continue;
bzero(buf, BUFSIZ);
if (FD_ISSET(sockfd, &readfds)) {
REREADPOINT:
realreadnum = Read(sockfd, buf, BUFSIZ); //真正的读取到了数据
if (realreadnum == 0) {//在网络环境下,read函数返回0,说明是对端关闭了,也就是说,客户端关闭了
//那么就应该关闭当前的connect端,并将该监听从 allfds中 移除
Close(sockfd);
FD_CLR(sockfd, &allfds);
printf("read done\n");
//额外添加的代码 start
client[i] = -1;
//额外添加的代码 end
}
else if (realreadnum == -1) {
if (errno == EINTR) {
//说明是被信号打断的,一般要重新read
printf("信号打断\n");
goto REREADPOINT;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf(" WOULDBLOCK \n");
//说明在打开文件的时候是使用的O_NONBLOCK方式打开的,但是没有读取到数据
//当前代码是不会走到这里的,因为前面代码select的最后一个参数用的NULL,是阻塞的
//一般在这里 也要重新读,但是这里有个问题,如果一直都读取不到,会不会死循环?
goto REREADPOINT;
}
else if (errno == ECONNRESET) {
//ECONNRESET 说明连接被重置了,因此要将该cfd关闭,并重新移除监听队列
Close(sockfd);
FD_CLR(sockfd, &allfds);
//额外添加的代码 start
client[i] = -1;
//额外添加的代码 end
}
else {
//这就是真正的有问题了,注意这里不要exit程序,应该只是让打印log
//不退出程序是因为,这时候还有其他的链接连上的
perror("read num <0");
}
}
else if (realreadnum > 0) {
//真正的读取到了客户端发送过来的数据
for (int j = 0; j < realreadnum; ++j) {
buf[j] = toupper(buf[j]);
}
Write(sockfd, buf, realreadnum);
Write(STDOUT_FILENO, buf, realreadnum);
}
if (--nready == 0) { //有可能50个已经连接上的链接中,这时候只有3个有数据发送过来了,还是要从50个中遍历,但是不同的是,假设是 5,19,30,那么写这一块代码就能节省时间了,当遍历到30后,--nready 就会等于0,直接跳出
break; /* 跳出for, 但还在while中 */
}
}
}
//额外添加代码 end
}
return ret;
}