从前面的知识学习了如何通过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位清0int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1void 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()调用出错则返回-1listenfd = Socket(AF_INET, SOCK_STREAM, 0);//第二步 build IP+PORT到网络地址 到socket 创建出来的listenfdstruct 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));//第三步,设置可以同时监听的最大的数量为1024Listen(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 更新maxfdif (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) {//说明是被信号打断的,一般要重新readprintf("信号打断\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;//额外添加代码 startint 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[] *///额外添加代码 endint ret = 0;//第一步:创建socket。 打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1listenfd = 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 创建出来的listenfdstruct 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));//第三步,设置可以同时监听的最大的数量为1024Listen(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));//额外添加代码 startint 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 更新maxfdif (connectfd > maxfd) {maxfd = connectfd; /* 有可能最大的connectfd 需要变化,这是因为当有新的connectfd之前,如果没有任何的connectfd没有断开,则会变成最大,但是如果中间有connect断开,则这个新的connectfd的值,有可能不是最大。select第一个参数需要 *///假设之前来了,4,5,6,这时候客户端4已经断开了,这时候 connectfd就等于4,如果4,5,6都没有断开过,则这时候新来的会是7}//额外添加代码 startif (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);// }// }//}//额外添加代码 startint 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");//额外添加的代码 startclient[i] = -1;//额外添加的代码 end}else if (realreadnum == -1) {if (errno == EINTR) {//说明是被信号打断的,一般要重新readprintf("信号打断\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);//额外添加的代码 startclient[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;}