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 8080 int main(void) { //创建socket int listenfd; listenfd = Socket(AF_INET,SOCK_STREAM,0); //bind 绑定端口和IP sockfd struct 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) { //创建socket int socketfd; socketfd = Socket(AF_INET,SOCK_STREAM,0); //连接connect struct 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); //阻塞,阻塞状态不会消耗CPU if(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); //监听任何合理的IP Bind(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 8080 int main(void) { //创建socket int 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 8080 int main(void) { //创建socket int 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)。如果让快递站代签,我们就可以做其他事情,当有快递到了,快递站的工作人员就会通知你,你有快递到了,这时候你可以选择让快递站的工作人员送到你家(异步)你仍然可以继续扫地,也可以直接去快递站点拿(同步),这时候你去拿快递的时间你不能扫地。