C++项目实战-高并发服务器详析
本节中的socket一套函数用的是之前封装过了的,进行异常处理(可自取)
链接: C++项目实战-socket编程_干饭小白的博客-CSDN博客
目录
BIO模型
NIO模型
多进程并发服务器
多线程并发服务器
NIO模型
I/O多路复用(I/O多路转接)
select
poll
最重要的epoll(单独拿出来)
epoll进阶
BIO模型
阻塞等待:不占用CPU宝贵的时间片,但是每次只能处理一个操作
BIO模型: 通过多线程/多进程解决每次只能处理一个操作的缺陷。但是线程/进程本身需要消耗系统资源,并且线程和进程的调度占用CPU.
BIO模型:
1.线程或进程会消耗资源2.线程或进程的调度会消耗CPU
NIO模型
非阻塞、忙轮询:不断的去催,或者说每隔一端时间就去查看有没有操作
提高了程序的运行效率、但占用大量CPU资源和系统资源
NIO模型:
多进程并发服务器
使用多进程并发服务器时要考虑以下几点:
1.父进程最大文件描述符个数(父进程中需要close关闭accept返回的新文件描述符)
2.系统创建进程个数(与内存大小相关)
3.进程创建过多是否会降低整体服务器性能(进程调度)
父进程:用来专门负责监听,并把任务分给子进程(子进程与客户端进行数据交流)
子进程:与客户端进行数据交流
回收子进程:当每一个子线程结束时,父进程可能还在accept(慢系统调用)
子进程需要父进程去回收,子进程结束会发送SIGCHLD信号,默认处理动作是忽略,但是我们需要捕捉,并通过这个信号进程子进程的回收
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include "wrap.h"#define MAXLINE 80 //最大的连接数 #define SERV_PORT 8080int main(void) {//创建socketint listenfd;listenfd = Socket(AF_INET,SOCK_STREAM,0);//bind 绑定端口和IP sockfdstruct sockaddr_in serveraddr;bzero(&serveraddr,sizeof(serveraddr));serveraddr.sin_port = htons(SERV_PORT);serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);serveraddr.sin_family = AF_INET; Bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));//listen 设置监听的最大数量 listen(listenfd,20); //accept 阻塞等待,连接int pid,n,i;struct sockaddr_in clientaddr;socklen_t clientlen;int connfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN]; // INET --> IPV4 ADDR-->sockaddr str len while(1){clientlen = sizeof(clientaddr);//这个一定要放在while里面,因为多进程,可能连接的客户端不同,connfd是不同的connfd = Accept(listenfd,(struct sockaddr *)&clientaddr,&clientlen);pid = fork();if(pid == 0) {//子进程读取数据和处理数据,不用做监听工作。监听工作交给父进程Close(listenfd);//读写数据,阻塞的(网络IO的数据准备就绪)while(1){n = read(connfd,buf,MAXLINE);if(n == 0) //说明有客户端关闭了,socket的对端关闭{printf("the other side has been closed\n");break;}//打印连接的客户端信息printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&clientaddr.sin_addr,str,sizeof(str)),ntohs(clientaddr.sin_port));//业务处理for(i = 0;i<n;++i){buf[i] = toupper(buf[i]); //小写转大写} write(connfd,buf,n);}//客户端关闭,关闭 connfd文件描述符Close(connfd);return 0;}else if(pid > 0) //父线程不需要读写数据,fork之后,父子线程的文件描述符表是相同的{Close(connfd);}else //出错了{perr_exit("fork");} }Close(listenfd);return 0;return 0; }
#include <stdio.h> #include <netinet/in.h> #include "wrap.h" #include <string.h> #include <unistd.h>#define MAXLINE 80 #define SERV_PORT 8080 #define SERV_IP "127.0.0.1"int main(void) {//创建socketint socketfd; socketfd = Socket(AF_INET,SOCK_STREAM,0);//连接connectstruct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;serveraddr.sin_port = htons(SERV_PORT);inet_pton(AF_INET,SERV_IP,&serveraddr.sin_addr);Connect(socketfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));//读写数据char buf[MAXLINE];int n;while(fgets(buf,MAXLINE,stdin) != NULL) //fgets从键盘键入数据{Write(socketfd,buf,sizeof(buf));n = Read(socketfd,buf,MAXLINE); //一个socketfd操作读写两个缓冲区if(n == 0) //对端已经关闭{printf("the other side has been closed..\n");break;}else{Write(STDOUT_FILENO,buf,n); //向标准终端中写入数据}}Close(socketfd);return 0;return 0; }
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <error.h>void perr_exit(const char *s) {perror(s);exit(1); } int Accept(int fd,struct sockaddr *sa,socklen_t *salenptr) {int n; //accept:阻塞,是慢系统调用。可能会被信息中断again:if((n = accept(fd,sa,salenptr)) < 0){if((errno == ECONNABORTED) || (errno == EINTR)){goto again; //重启}else{perr_exit("accept error");}}return n; } int Bind(int fd,const struct sockaddr *sa,socklen_t salen) {int n;if((n = bind(fd,sa,salen)) < 0){perr_exit("bind error");}return n; } int Connect(int fd,const struct sockaddr *sa,socklen_t salen) {int n;if((n = connect(fd,sa,salen)) < 0){perr_exit("connect error");}return n; } int Listen(int fd,int backlog) {int n;if((n = listen(fd,backlog)) < 0){perr_exit("listen error");}return n; } int Socket(int family,int type,int protocol) {int n;if((n = socket(family,type,protocol)) < 0){perr_exit("socket error");}return n; } ssize_t Read(int fd,void *ptr,size_t nbytes) {ssize_t n;again:if((n = read(fd,ptr,nbytes)) == -1){if(errno == EINTR)//被中断{goto again;}else{return -1;}}return n; } ssize_t Write(int fd,const void *ptr,size_t nbytes) {ssize_t n;again:if((n = write(fd,ptr,nbytes)) == -1){if(errno == EINTR){goto again;}else{return -1;}}return n; } int Close(int fd) {int n;if((n = close(fd)) == -1){perr_exit("close error");}return n; } ssize_t Readn(int fd,void *vptr,size_t n) {size_t nleft;ssize_t nread;char *ptr;ptr = vptr;nleft = n;while(nleft > 0){if((nleft = read(fd,ptr,nleft)) < 0){if(errno == EINTR){nread = 0;}else{return -1;}}else if(nread == 0){break;}nleft -= nread;ptr += nread;}return n-nleft;} ssize_t Writen(int fd,const void *vptr,size_t n) {size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while(nleft > 0){if((nwritten = write(fd,ptr,nleft)) <= 0){if(nwritten < 0 && errno == EINTR){nwritten = 0;}else{return -1;}}nleft -= nwritten;ptr += nwritten;}return n; } static ssize_t my_read(int fd,char *ptr) {static int read_cnt;static char *read_ptr;static char read_buf[100];if(read_cnt <= 0){again:if((read_cnt = read(fd,read_buf,sizeof(read_buf))) < 0){if(errno == EINTR){goto again;}return -1;}else if(read_cnt == 0){return 0;}read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;return 1; } ssize_t Readline(int fd,void *vptr,size_t maxlen) {ssize_t n,rc;char c,*ptr;ptr = vptr;for(n=1;n<maxlen;n++){if((rc = my_read(fd,&c)) == 1){*ptr++ = c;if(c == '\n'){break;}}else if(rc == 0){*ptr = 0;return n-1;}else{return -1;}}*ptr = 0;return n; }
#ifndef _WRAP_H_ #define _WRAP_H_// #include <arpa/inet.h> // #include <stdlib.h> // #include <string.h> // #include <unistd.h> // #include <stdio.h>void perr_exit(const char *s); int Accept(int fd,struct sockaddr *sa,socklen_t *salenptr); int Bind(int fd,const struct sockaddr *sa,socklen_t salen); int Connect(int fd,const struct sockaddr *sa,socklen_t salen); int Listen(int fd,int backlog); int Socket(int family,int type,int protocol); ssize_t Read(int fd,void *ptr,size_t nbytes); ssize_t Write(int fd,const void *ptr,size_t nbytes); int Close(int fd); ssize_t Readn(int fd,void *vptr,size_t n); ssize_t Writen(int fd,const void *vptr,size_t n); ssize_t my_read(int fd,char *ptr); ssize_t Readline(int fd,void *vptr,size_t maxlen); #endif
多线程并发服务器
在使用线程模型开发服务器时需要考虑以下问题:
1.调整进程内最大文件描述符上限
2.线程如有共享数据,需要考虑线程同步
3.服务于客户端线程退出时,退出处理。(退出值、分离态)
4.系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <pthread.h> #include <arpa/inet.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8080//一个连接对应一个客户端的信息 struct s_info {struct sockaddr_in cliaddr;int connfd; };//子线程处理的逻辑 void *do_work(void *arg) {int n,i;//要进行业务处理的必要的信息 文件描述符 发送方的IP和动态端口struct s_info *ts = (struct s_info *)arg;//缓存char buf[MAXLINE];char str[INET_ADDRSTRLEN]; //存储点分十进制的 IP//设置线程分离pthread_detach(pthread_self());//业务处理while(1){n = Read(ts->connfd,buf,MAXLINE); //阻塞,阻塞状态不会消耗CPUif(n == 0){printf("the other side has been closed.\n");break;}printf("recevied from %s at PORT %d\n",inet_ntop(AF_INET,&(*ts).cliaddr,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);return NULL; }int main(void) {int i = 0;//创建套接字(监听)int listenfd;listenfd = Socket(AF_INET,SOCK_STREAM,0);//绑定struct sockaddr_in servaddr; //服务器端套接字servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //监听任何合理的IPBind(listenfd,(struct servaddr *)&servaddr,sizeof(servaddr));//设置监听Listen(listenfd,20);//连接struct s_info ts[256]; //最大的连接数 256 int connfd;struct sockaddr_in cliaddr;socklen_t cliaddr_len;pthread_t tid;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++;//为了安全起见if(i == 255){break;}}return 0; }
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <pthread.h> #include <arpa/inet.h> #include <unistd.h> #include "wrap.h"#define MAXLINE 80 #define SERV_IP "127.0.0.1" #define SERV_PORT 8080int main(void) {//创建socketint sockfd;sockfd = Socket(AF_INET,SOCK_STREAM,0);//连接struct sockaddr_in servaddr;servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr);Connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//通信int n;char buf[MAXLINE];while(fgets(buf,MAXLINE,stdin) != NULL){Write(sockfd,buf,sizeof(buf));n = Read(sockfd,buf,MAXLINE);if(n == 0){printf("the other side has been closed\n");}else{Write(STDOUT_FILENO,buf,n);}}Close(sockfd);return 0; }
NIO模型
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <pthread.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8080int main(void) {//创建socketint listenfd;listenfd = Socket(AF_INET,SOCK_STREAM,0);//将listenfd设置为非阻塞fcntl(listenfd,F_SETFD,fcntl(listenfd,F_GETFD,0) | O_NONBLOCK);//绑定struct sockaddr_in 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);//连接struct sockaddr_in cliaddr;socklen_t cliaddr_len = sizeof(cliaddr);int connfd;char buf[MAXLINE];int n,i=0;while(1){connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);n = Read(connfd,buf,MAXLINE);fcntl(connfd,F_SETFD,fcntl(connfd,F_GETFD,0) | O_NONBLOCK);if(n == -1){if(errno == EAGAIN || errno == EWOULDBLOCK){continue; //再次启动read}//出错 Read会处理}else if(n == 0){break;}else{for(i = 0;i<n;++i){buf[i] = toupper(buf[i]);}Write(connfd,buf,n);}}Close(connfd);return 0; }
客户端跟之前几个一样的,就不写了
小结:
上面无论是多线程还是多进程都是以BIO模型实现的,也就是阻塞的方式,很容易看出,主线程(主进程)负责监听,子线程负责读写数据并进行逻辑处理,即:Reactor模式
创建线程或进程需要消耗系统资源
进程(线程)之间的调度(切换)需要消耗系统资源
线程(线程)的撤销也需要消耗系统资源
与 NIO 模型相比,NIO通过轮询的方式实现的,需要占用大量的CPU
无论是NIO模型还是BIO模型,每次只能处理一个连接请求,有没有一种方式能够实现同时监听多个文件描述符,我们再看无论是监听工作还是数据读写或者业务逻辑处理都是由用户进程来完成的,这样一来的话用户进程的大部分时间都用来处理监听工作了,是很不划算的,我们是否可以将监听交给内核来完成呢?这等等的问题,可以通过多路复用技术来解决,多路复用包括三种方式:select、poll、epoll 我们一起来探究他们吧
I/O多路复用(I/O多路转接)
多路I/O转接服务器也叫做多任务IO服务器。该服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件文件描述符。
I/O多路复用使得程序能够同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用有:select、pool和epoll
举个生活中的例子,帮助理解。我们不妨把server想象成自己,把内核想象成快递站,我们收快递有两种方式,第一是自己去接收快递,第二是让快递站代签。如果我们自己拿快递的话,要么一直等着快递员(这个时候阻塞 BIO),要么每个10分钟去催一次快递员(在每次催完的10分钟内我们可以去扫地,做饭等 NIO)。如果让快递站代签,我们就可以做其他事情,当有快递到了,快递站的工作人员就会通知你,你有快递到了,这时候你可以选择让快递站的工作人员送到你家(异步)你仍然可以继续扫地,也可以直接去快递站点拿(同步),这时候你去拿快递的时间你不能扫地。
select
主旨思想:
1.首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
2.调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或多个进行I/O操作时,该函数才返回。
a.这个函数是阻塞的
b.函数对文件描述符的检测的操作是由内核完成的
3.在返回时,它会告诉进程有多少描述符要进行I/O操作
分析:
1.select能够监听的文件描述符个数受限于 FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。 【默认进程中能够打开的文件描述符个数为1024,历史遗留问题:重新编译linux内核可解决】2.解决1024以下客户端时使用select是很合适的,但如果连接的客户端过多,select采用轮询模型(NIO),会大幅度降低服务器响应效率【因为select不会告诉应用进程到底是哪个文件描述符有数据到达,所以每次都需要去循环一遍,比如有1000个客户端连接,每次循环的需要有1000次系统调用,是消耗大量的资源】
3.工作过程中存在大量的拷贝工作
4.select了解以下,不需要花费大量时间学习,不划算
图解原理:
相关API
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timerval *timeval *timeout);
nfds:监控的文件描述符集里最大文件描述符加1,告诉内核检测多少个文件描述符
readfds:监控有读数据到达文件描述符集合,传入传出参数 ==> 可读事件
writefds:监控写数据到达文件描述符集合,传入传出参数 ==> 可写事件
exceptfds:监控异常发生达到文件描述符集合,传入传出参数 ==> 异常事件
timeout:定时阻塞监控时间
1.NULL 永远等待,直到检测到了文件描述符有变化
2.设置 timeval 等待固定时间 【- tv_sec > 0 tv_usec > 0, 阻塞对应的时间】
3.设置 timeval 时间均为 0,检测描述符后立即返回,轮询
【 - tv_sec = 0 tv_usec = 0, 不阻塞】
struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */ };
void FD_CLR(int fd,fd_set *set); //把文件描述符集合里fd清0
void 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
一些说明:
select返回的是所监听的集合中满足条件的总数,通过上面的四个函数可以判断恐惧特发生的事件和具体哪一个满足条件。fd_set是位图机制
select缺点:
1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3.select支持的文件描述符数量太小了,默认是1024
4.fds集合不能重用,每次都需要重置
代码案例:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8080int main(void) {int i,n;//创建socket套接字int listenfd;listenfd = Socket(AF_INET,SOCK_STREAM,0);//绑定struct sockaddr_in servaddr;bzero(&servaddr,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);servaddr.sin_addr.s_addr = htonl(INADDR_ANY);Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//设置Listen(listenfd,20);//连接前 ==> 先交给selectint connfd,sockfd;fd_set allset,rset; //allset保留原来的位,int nready; //select返回值,有事件发生的总数位图int maxfd = listenfd; //最大的文件描述符位置,告诉内核监听范围FD_ZERO(&allset); //全部置为0FD_SET(listenfd,&allset); //把监听描述符的位置 置1struct sockaddr_in cliaddr;socklen_t cliaddr_len;char str[INET_ADDRSTRLEN]; //保存客户端的 IPint client[FD_SETSIZE]; //保存监听的位置 client[i] = 4 表示第4个文件描述符有事件发生int maxi = -1; //client 的最大下标char buf[MAXLINE];for(i = 0;i<FD_SETSIZE;++i){client[i] = -1; }while(1){//监听读事件,阻塞。select最好不要设置为轮询的方式rset = allset;nready = select(maxfd+1,&rset,NULL,NULL,NULL); if(nready < 0) //失败,退出{// void perr_exit(const char *s)// {// perror(s);// exit(1);// }perr_exit("select error");}if(FD_ISSET(listenfd,&rset)) //如果为真,有新的连接到达 监听事件不放入client{//有新连接到达,我们需要把连接的客户端的信息拿到,后面要给它返回信息cliaddr_len = sizeof(cliaddr);//连接 accept,此时Accept不会发生阻塞等待,因为listenfd已经有事件发生connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);//打印连接的客户端信息printf("连接来自 %s 在 %d 端口\n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));//把客户端连接的文件描述符加入到监听队列中,监听该客户端是否有数据传来for(i = 0; i<FD_SETSIZE; ++i) //通过 for循环总是能得到最小的空闲位置{if(client[i] < 0){client[i] = connfd;break;}}//select能监听的文件个数达到上限 1024if(i == FD_SETSIZE){fputs("select能监听的文件个数达到上限\n",stdout); //向标准设备打印提示信息exit(1);}if(i > maxi){maxi = i;}FD_SET(connfd,&allset); //添加到监听信号集if(connfd > maxfd){maxfd = connfd; //maxfd做个迭代}if(--nready == 0) //如果没有更多的就绪文件描述符,继续回到select阻塞监听{continue;}}for(i = 0;i <= maxi;++i) //检测哪一个 clients有数据就绪{sockfd = client[i];if(client[i] < 0) {continue;}if(FD_ISSET(sockfd,&rset)){if((n = Read(sockfd,buf,MAXLINE)) == 0) //与客户端关闭连接{Close(sockfd);FD_CLR(sockfd,&allset); //解除select监听此文件描述符client[i] = -1;}int j;for(j=0;j<n;++j){buf[j] = toupper(buf[j]);}Write(sockfd,buf,n);if(--nready == 0){break;}} }}Close(listenfd);return 0; }
客户端代码前面案例相同
poll
poll只针对Linux有效,poll模型是基于select最大文件描述符限制提出的,跟select一样,只是将select使用的三个基于位的文件描述符(readfds/writefds/exceptfds)封装成了一个结构体,然后通过数组的是形式来突破最大文件描述符的限制。
#include <poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
参数:
fds: 数组的首地址
nfds_t: 监控数组中有多少文件描述符需要被监控
timeout:
-1 阻塞等待
0 立即返回,不阻塞
>0 等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
struct pollfd
{
int fd; //文件描述符
short events; //监控的事件
short revents; //监控事件中满足条件返回的事件
};
- 返回值: -1 : 失败 >0(n)
成功,n表示检测到集合中有n个文件描述符发生变化
如果不再监控某一个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0
代码案例:
#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"#define MAXLINE 80 #define SERV_PORT 8080 #define OPEN_MAX 1024 //监控的最多的数量int main(void) {int i,n,j;//创建socketint listenfd = Socket(AF_INET,SOCK_STREAM,0);//绑定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(SERV_PORT);Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//设置监听Listen(listenfd,20);//pollstruct pollfd fds[OPEN_MAX]; //记录监听的文件描述符nfds_t maxi = -1; //最大的那个监听文件描述符int nready;//将设置为监听描述符fds[0].fd = listenfd;fds[0].events = POLLRDNORM; //监听为普通事件maxi = 0;//client[i] = -1 表示文件描述符 i 不处于监听状态for(i=1;i<OPEN_MAX;++i){fds[i].fd = -1;}//客户端信息struct sockaddr_in cliaddr;socklen_t cliaddr_len;int connfd,sockfd;char str[INET_ADDRSTRLEN];char buf[MAXLINE];while(1){nready = poll(fds,maxi+1,-1); //-1表示阻塞等待if(fds[0].revents & POLLRDNORM) // &位操作 有客户端连接请求{cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);//打印客户端信息printf("连接来自 %s 在端口 %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)), ntohs(cliaddr.sin_port));//将 connfd 加入到监听数组for(i=1;i<OPEN_MAX;++i){if(fds[i].fd < 0){fds[i].fd = connfd;break;}}//判断监听事件是否超过最大限制if(i == OPEN_MAX){perr_exit("too many clients\n");}fds[i].events = POLLRDNORM;if(i > maxi){maxi = i;}if(--nready <= 0){continue;}} for(i = 1;i<= maxi;++i){sockfd = fds[i].fd;if(fds[i].fd < 0){continue;;}if(fds[i].revents & (POLLRDNORM|POLLERR)){if((n = Read(sockfd,buf,MAXLINE)) < 0){if(errno == ECONNRESET) //sockfd 不监听了{printf("fds[%d] aborted connection\n",i);Close(sockfd);fds[i].fd = -1;}else{ perr_exit("read error");}}else if(n == 0){printf("fds[%d] closed connection\n",i);Close(sockfd);fds[i].fd = -1;}else{for(j=0;j<n;++j){buf[j] = toupper(buf[j]);}Writen(sockfd,buf,n);}if(--nready <= 0){break;}}}}return 0; }
最重要的epoll(单独拿出来)
epoll是Linux下多路复用IO接口select/poll的增强版本,它能够显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被监听的文件描述符集合,另外一点就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒儿加入Ready队列的描述符集合就行了。
目前epoll是Linux大规模并发网络程序中的热门首先模型
epoll除了提供select/poll那种IO事件的水平触发(LT)外,还提供了边缘触发(ET)。这就使得用户空间程序有可能缓冲IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率
可以使用cat命令查看一个进程可以打开的socket描述符上限
cat /proc/sys/fs/file-max
也可以修改配置文件的方式修改该上限
sudo vi /etc/security/limits.conf
在文件尾部写入一下配置,soft软限制,hard硬限制
soft nofile 65536
hard nofile 100000
基础API
1.创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关
#include <sys/epoll.h>
int epoll_create(int size);
- 参数: size : 目前没有意义了。随便写一个数,必须大于0
- 返回值: -1 : 失败 > 0 : 文件描述符,操作epoll实例的
创建一个新epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)
2.控制某个epoll监控的文件描述符上的事件,注册、修改、删除
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
epfd: 为epoll_create的句柄
op:表示动作,用3个宏来表示
EPOLL_CTL_ADD:注册新的fd到epfd
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd删除一个fd
event:告诉内核需要监听的事件
struct epoll_event{
_uint32_t events; // Epoll events
epoll_data data; //user data variable
};
typedef union epoll_data {
void *ptr; //回调函数
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
3.等待所监听文件描述符上有事件产生,类似于select()调用
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数:
- epfd : epoll实例对应的文件描述符
- events : 传出参数,保存了发送了变化的文件描述符的信息
- maxevents : 第二个参数结构体数组的大小
- timeout : 阻塞时间
0 : 不阻塞 -
1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
> 0 : 阻塞的时长(毫秒)
- 返回值:
- 成功,返回发送变化的文件描述符的个数 > 0
- 失败 -1
代码案例:
#include <stdio.h> #include <stdlib.h> #include <netinet/in.h> #include <string.h> #include <sys/epoll.h> #include <arpa/inet.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8080 #define OPEN_MAX 1024int main(void) {int i,n,j,ret;//创建套接字int listenfd = Socket(AF_INET,SOCK_STREAM,0);//绑定struct sockaddr_in servaddr;bzero(&servaddr,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);servaddr.sin_addr.s_addr = htonl(INADDR_ANY);Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//设置监听Listen(listenfd,20);//epollint client[OPEN_MAX];int maxi = -1;for(i=0;i<OPEN_MAX;++i){client[i] = -1;}maxi = -1;//创建一个 epoll 句柄int efd = epoll_create(OPEN_MAX);if(efd == -1){perr_exit("epoll_create");}//设置连接int nready;struct epoll_event tep,ep[OPEN_MAX];tep.events = EPOLLIN;tep.data.fd = listenfd;ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);if(ret == -1){perr_exit("epoll_ctl");}//客户端信息struct sockaddr_in cliaddr;socklen_t cliaddr_len;char str[INET_ADDRSTRLEN];int connfd,sockfd;char buf[MAXLINE];while(1){nready = epoll_wait(efd,ep,OPEN_MAX,-1); //-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) //有新客户端连接{cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);printf("连接来自 %s 在端口 %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));for(j=0;j<OPEN_MAX;++j){if(client[j] < 0){client[j] = connfd;break;}}if(j == OPEN_MAX){perr_exit("too many clients");}if(j > maxi){maxi = j;}tep.events = EPOLLIN;tep.data.fd = connfd;ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);if(ret == -1){perr_exit("epoll_ctl");}// if(--nready <= 0)// {// continue;// }}else{sockfd = ep[i].data.fd;n = Read(sockfd,buf,MAXLINE);if(n == 0){for(j =0;j <= maxi;++j){if(client[j] == sockfd){client[j] = -1;break;}}ret = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL);if(ret == -1){perr_exit("epoll_ctl");}Close(sockfd);printf("cliend[%d] closed connect\n",j);}else{for(j =0 ;j<n;++j){buf[j] = toupper(buf[j]);}Writen(sockfd,buf,n);}}}}Close(listenfd);Close(efd);return 0; }
epoll进阶
事件模式:
EPOLL事件有两种模型 ==>
Edge Triggred(ET) 边缘触发:只有数据到来才触发,不管缓存区中是否还有数据
Level Triggered(LT)水平触发:水平触发只要有数据都会触发
案例:
1.假定我们已经把一个用来从管道中读取数据的文件描述符(RFD)添加到epoll描述符
2.管道的另一端写入了 2KB 的数据
3.调用epoll_wait,并且它会返回RFD,说明它已经准备好读取操作
4.读取1KB的数据
5.调用 epoll_wait...
在这个过程中,有两种工作模式
client ----> 1000B
epoll_wait(cfd);
read(500B) 已读 500B
水平触发:触发epoll,直到读完
边缘触发:不告诉了,不触发epoll,除非有新的数据到达
ET模式
ET模式即Edge Triggered 工作模式
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候ET工作模式才会汇报事件。因此在第5步的时候调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接字,以避免由于一个文件句柄的阻塞读、阻塞写操作把处理多个文件描述符的任务饿死。
1).基于非阻塞文件句柄
2).只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才挂起、等待。但这并不意味着说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定缓冲区已经没有数据了,也就可以认为此事件已处理完成
LT模式
与LT模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
LT:LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以堆这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET:ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变成就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意:如果一直不对这个fd作IO操作(从而导致它不再变成未就绪),内核不会发送更多的通知.
代码案例:
基于网络C/S非阻塞模型的epoll ET触发模式
#include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #include <sys/socket.h>#define MAXLINE 10 #define SERV_PORT 8080int main(void) {int res;//创建socket套接字int listenfd = socket(AF_INET,SOCK_STREAM,0);//绑定struct sockaddr_in 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);//epoll + ET设置struct epoll_event event;struct epoll_event resevent[10];int efd = epoll_create(10);event.events = EPOLLIN | EPOLLET; //ET边缘触发 默认是水平触发//保存客户端信息int connfd,len;char str[INET_ADDRSTRLEN];char buf[MAXLINE];struct sockaddr_in cliaddr;socklen_t cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);printf("连接的来自 %s 在端口 %d\n",inet_ntop(AF_INET,&servaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));//设置connfd 非阻塞int flag = fcntl(connfd,F_GETFL);flag |= O_NONBLOCK;fcntl(connfd,F_SETFL,flag);event.data.fd = connfd;epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&event);while(1){printf("epoll_wait begin\n");res = epoll_wait(efd,resevent,10,-1);printf("epoll_wait end res %d\n",res);if(resevent[0].data.fd == connfd){while((len = read(connfd,buf,MAXLINE/2)) > 0){write(STDOUT_FILENO,buf,len);}}}return 0; }
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 10 #define SERV_PORT 8080 #define SERV_IP "127.0.0.1"int main(void) {int i;char ch = 'a';char buf[MAXLINE];//创建套接字int sockfd;sockfd = socket(AF_INET,SOCK_STREAM,0);//连接struct sockaddr_in servaddr;bzero(&servaddr,sizeof(servaddr));servaddr.sin_family = AF_INET;inet_pton(AF_INET,SERV_IP,&servaddr.sin_addr);servaddr.sin_port = htons(SERV_PORT); connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));while(1){for(i=0;i<MAXLINE/2;++i){buf[i] = ch;}buf[i-1] = '\n';ch++;for(;i<MAXLINE;++i){buf[i] = ch;}buf[i-1] = '\n';ch++;write(sockfd,buf,sizeof(buf));sleep(10);}close(sockfd);return 0; }
C++项目实战-高并发服务器详析相关推荐
- 【Linux】C++项目实战-高并发服务器详析
目录 多进程实现并发服务器 多线程实现并发服务器 BIO模型 NIO模型 I/O多路复用(I/O多路转接) select 主旨思想 图解原理 函数解析 代码举例 select的缺点 poll 函数解析 ...
- 网络编程实战之高级篇, 彻底解决面试C10k问题, 高并发服务器, IO多路复用, 同时监视多个IO事件
目录 一.前言 二.IO多路复用的理解 三.IO多路复用的发展 select poll epoll 四.C10K服务端代码 五. 总结 一.前言 网络入门篇,从操作系统的层次推开网络大门 网络入门基 ...
- Linux 高并发服务器实战 - 1 Linux系统编程入门
Linux 高并发服务器实战-1Linux系统编程入门 在本机和服务器端设置公共密钥(配置免密登录) 在本机cmd里输入 ssh-keygen -t rsa,生成本机的公密钥 在服务器端里也配置 ss ...
- Linux 高并发服务器实战 - 2 Linux多进程开发
Linux 高并发服务器实战 - 2 Linux多进程开发 进程概述 概念1: 概念2: 微观而言,单CPU任意时刻只能运行一个程序 并发:两个队列交替使用一台咖啡机 并行:两个队列同时使用两台咖啡机 ...
- redis 缓存数据_Redis 缓存数据方案对比:常规 VS 高并发服务器
1 Redis 是什么 Redis 是一种开源的非关系型数据库.起源于负载较大时,当前关系型数据库无法承载的情况. 到目前为止,Redis 可以用作数据库.缓存.消息处理.Redis 可以存储键和5种 ...
- Linux高并发服务器开发---笔记2(多进程)
0630 第4章 项目制作与技能提升 4.0 视频课链接 4.1 项目介绍与环境搭建 4.2 Linux系统编程1.4.3 Linux系统编程2 4.4 多进程 1-9 10.进程间通信☆☆☆ 进程间 ...
- Linux高并发服务器开发---笔记1(环境搭建、系统编程、多进程)
0613 第4章 项目制作与技能提升 4.0 视频课链接 4.1 项目介绍与环境搭建 4.1.1 项目介绍 4.1.2 开发环境搭建 ①安装Linux系统.XSHELL.XFTP.Visual Stu ...
- Linux高并发服务器开发---笔记4(网络编程)
0705 第4章 项目制作与技能提升 4.0 视频课链接 4.1 项目介绍与环境搭建 4.2 Linux系统编程1.4.3 Linux系统编程2 4.4 多进程 1-9 10.进程间通信☆☆☆ 4.5 ...
- [Linux 高并发服务器]网络基础
[Linux 高并发服务器]网络基础 文章概述 该博客为牛客网C++项目课:Linux高并发服务器 个人笔记 作者信息 NEFU 2020级 zsl ID:fishingrod/鱼竿钓鱼干 Email ...
最新文章
- mysql中文乱码问题的解决方案
- 在FreeBSD上安装Bugzilla
- 推荐算法-聚类-层次聚类法
- 点击按钮改变div的颜色
- javaweb关于用户是否登录全局判断,没有登录跳转到登录界面
- php协程实现mysql异步_swoole与php协程实现异步非阻塞IO开发
- 技术无“学历” 从大专学校走出来的少年AI狂人
- threejs向量夹角和夹角方向
- c语言指针变量的定义数组的长度,【C语言更新】指向数组的指针
- 批量修改文本文件编码GB18030为UTF-8
- PS打开PSD文档服务器未响应,ps打不开psd文件的解决方法
- java使用ffmpeg实现视频切割
- CDP科普:客户数据中台(CDP)是什么?
- Arm NEON intrinsics指令格式
- 从前慢-谷粒商城篇章4
- 《国富论》笔记——货币
- 什么是华为云服务器?它有什么优点?
- Windows系统DOS命令之多线程技术
- 机器学习驱动的语言测试
- 麻烦你先搞懂这几个问题,简历再写熟悉数据库!!!