linux网络编程-----几种服务器模型及io多路复用函数
libevent實現了對io多路復用函數的封裝,復習一下linux下的io復用函數,select,poll,epoll
在c/s模型中,要處理多個客戶端的請求以達到并發處理的效果,有以下幾種方法
- 主線程accept,多線程處理,為每一個客戶端開一個線程
- 主進程accept,多進程處理,為每一個客戶端開一個進程
- 線程池/進程池,將程序執行過程中線程/進程的創建銷毀開銷放在程序一開始執行時進行,進一步可以動態改變池中線程/進程個數
- io多路復用函數,單線程模式
多線程模式的服務器模型大體如下
//server.cpp #include <iostream>#include <cerrno> #include <cstdlib> #include <cstdio> #include <cstring> #include <unistd.h>#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>#include <pthread.h>void* process_client(void* arg);int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return -1;}struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(8080);servaddr.sin_addr.s_addr = INADDR_ANY;if(bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){close(sockfd);std::cerr << "bind error" << std::endl;return -1;}if(listen(sockfd, 10) < 0){close(sockfd);std::cerr << "listen error" << std::endl;return -1;}struct sockaddr_in addr;bzero(&addr, sizeof(addr));socklen_t len = sizeof(addr);while(true){/* 主線程接受客戶端請求,每一個子線程處理一個服務器與客戶端的交互 */int fd = accept(sockfd, (struct sockaddr*)&addr, &len);pthread_t tid;pthread_create(&tid, NULL, process_client, (void*)&fd);pthread_detach(tid);}close(sockfd);return 0; }void *process_client(void *arg) {int fd = *static_cast<int *>(arg);char reply[] = "server has receive your message";char msg[4096];/* 每個線程一個循環 */while(true){bzero(msg, sizeof(msg));int ret = recv(fd, msg, sizeof(msg), 0);if(ret < 0){continue;}else if(ret == 0){ std::cout << "close connection with client " << fd << std::endl;close(fd);pthread_exit(NULL);break;}else{msg[ret] = '\0';std::cout << "receive from client " << fd << " : " << msg << std::endl;ret = send(fd, reply, strlen(reply), MSG_NOSIGNAL);if(ret < 0){continue;}else if(ret == 0){std::cout << "close connection with client " << fd << std::endl;close(fd);pthread_exit(NULL);break;}}}} //client.cpp #include <iostream> #include <string>#include <cerrno> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h>#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(8080);inet_aton("127.0.0.1", &servaddr.sin_addr);connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));std::string msg;char res[4096];while(true){getline(std::cin, msg);if(msg == "exit"){close(sockfd);break;}if(send(sockfd, msg.c_str(), msg.size(), MSG_NOSIGNAL) <= 0){std::cout << "close connection from server" << std::endl;close(sockfd);break;}int len = recv(sockfd, res, sizeof(res), 0);if(len <= 0){std::cout << "close connection from server" << std::endl;close(sockfd);break;}res[len] = '\0';std::cout << "receive from server" << res << std::endl;}return 0; }多進程模式的服務器模型如下
//server.cpp #include <iostream> #include <string>#include <cstring> #include <cstdlib>#include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>void process_client(int fd);int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(8080);servaddr.sin_addr.s_addr = INADDR_ANY;bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));listen(sockfd, 10);struct sockaddr_in addr;socklen_t len = sizeof(addr);signal(SIGCHLD, SIG_IGN);while(true){int fd = accept(sockfd, (struct sockaddr*)&addr, &len);std::cout << "receive a new client " << fd << std::endl;if(fork() == 0){close(sockfd);process_client(fd);exit(EXIT_SUCCESS);}else{close(fd);}}close(sockfd);return 0; }void process_client(int fd) {char reply[] = "server has receive your message";char msg[4096];while(true){int len = recv(fd, msg, sizeof(msg), 0);if(len <= 0){std::cerr << "close connection with client" << fd << std::endl;close(fd);break;}msg[len] = '\0';std::cout << "receive from client" << fd << " : " << msg << std::endl;if(send(fd, reply, strlen(reply), MSG_NOSIGNAL) <= 0){std::cout << "close connection with client" << fd << std::endl;close(fd);break;}} } //client.cpp //同多線程client.cpp創建進程時關閉套接字的原因:
- fork創建進程,是將主進程的內存空間copy一份作為子進程的內存空間,這就導致了監聽套接字也被copy了,從而監聽套接字的引用計數變為2,這樣如果子進程不close掉,而僅僅主進程close,那么監聽套接字就不會被關閉。當然進程結束后會自動關閉相應的套接字,但還是手動關閉的好,以免在子進程中使用exec,就沒辦法關閉了
- 主進程同理,客戶端套接字引用計數為2,也需要關閉一個
線程池模式下的服務器模型
需要注意的幾點
- 在程序開始時就創建一定數量的線程池,初始化互斥鎖和條件變量
- 維護一個任務隊列,采用互斥鎖保護隊列中的任務只被一個線程執行
- 采用條件變量保證在沒有任務時的cpu使用情況,即不需要不斷輪循查看是否有任務沒有被執行
- 擴展可以動態改變池中線程個數,保證既沒有過多線程空閑,也沒有過多任務沒有被執行
- 釋放線程空間,釋放互斥鎖和條件變量
代碼(沒有實現動態改變線程個數)
進程池模式下的服務器模型
io多路復用函數
上述這些方法在某種程度上都有缺陷,當客戶端請求過多時效率都會降低,內存消耗都比較明顯。對于處理高并發的客戶端請求,可以采用io多路復用的的方法,linux下提供了select,poll,epoll三個函數,使用這些函數時可以把監控的套接字設置成非阻塞(可以使用fcntl函數實現)。
select
select是早期的io多路復用函數,函數原型如下
傳給select的參數告訴內核
- 用戶所關心的描述符
- 對于每個描述符,用戶所關心的事件,可讀,可寫,出現異常
- 愿意等待的時長,可以永遠等待,等待一個固定時長或者根本不等待
從slect返回時內核通知進程
- 已準備好的描述符總數量
- 每一個描述符是否可讀/可寫/出現異常(需要用戶手動判斷每一個,不能直接定位到準備好的描述符)
select最后一個參數tvptr指定愿意等待的時間長度,有三種情況
- NULL:永遠等待直到有某(些)個描述符準備好才返回
- tvptr->tv_sec==0 && tvptr->tv_usec==0:不等待,直接返回
- tvptr->tv_sec!=0 || tvptr->tv_usec!=0:等待指定的時間,當某個描述符準備好,或者時間到,select就會返回。注意如果某個描述符準備好,而時間沒有到,也會立即返回
readfds,writefds,exceptfds是指向描述符集的指針,都是fd_set*類型,fd_set有不同的實現方式,可以是每一位作為一個描述符,也可以是一個很大的數組。
在使用select之前,用戶需要手動初始化需要的描述符集,不關心的可以傳入NULL。使用方法如下
select使用FD_SET添加描述符,使用FD_ISSET判斷是否某個描述符準備好。可以把fd_set想象成一個位數組,每一位代表一個描述符,使用FD_ZERO時將每一位置為0,使用FD_SET時將對應位置為1,在select中返回后,將準備好的描述符對應的那一為的1保留,沒有準備好的描述符對應的那一位置為0。FD_ISSET就判斷對應位是否為1即可知道描述符是否準備好。
但是select的缺陷還是很多的
- 每次循環都需要調用FD_ZERO清空描述符集,這就導致需要用戶手動再添加關心的描述符,很麻煩,libevent是通過上述方法解決這一問題的,即添加到readfds_in,select時改變readfds_out,這樣每次循環只需要將readfds_in復制給readfds_out即可
- 不能直接定位到準備好的描述符上,用戶需要手動遍歷所有的描述符判斷哪些準備好
- 每次調用select,都需要把fd集合從用戶態拷貝到內核態
- 每次調用select都需要在內核遍歷所有的fd
- select支持的描述符數量太小,默認為1024
poll
poll函數類似于select,相比于select使用起來更簡單,但是仍然沒有解決輪循判斷描述符是否準備好的問題,換句話說,當描述符數量很大時,效率仍然很低
poll函數的原型如下
與select不同,poll不是為每一個事件構造一個描述符集,而是構造一個pollfd結構的數組,每個數組元素指定一個描述符以及用戶關心的事件
struct pollfd {int fd; /* 描述符 */short events; /* 用戶關心的事件,調用poll時由用戶設置 */short revents; /* 是什么事件將描述符激活,在poll返回時由內核設置*/ };events可以由以下幾種通過或運算結合在一起
- POLLIN:可讀
- POLLOUT:可寫
- POLLERR:出錯
- POLLHUP:掛斷
- POLLNVAL:描述符沒有綁定到一個文件/TCP連接
- …
poll的等待時長是int類型
- timeout == -1:永遠等待或者某個描述符準備好后返回
- timeout == 0:不等待,直接返回
- timeout > 0:等待指定微秒或者某個描述符準備好后返回
使用方法同select,構造一個很大的pollfd數組,調用時傳入一個當前最大描述符大一的值,返回后判斷每個描述符是否準備好,這一點和select一樣,poll不能直接定位到準備好的哪些描述符,需要判斷每一個的revents
epoll
與select和poll不同,epoll克服了二者的缺陷,在阻塞等待過程中不是循環遍歷每個描述符判斷是否準備好以便返回,而是通過內核中的相應fd回調函數的調用直接得知某個文件描述符已經被激活(準備好),這就解決多當描述符數量過多時效率下降的問題。另外,epoll在返回后將所有已經準備好的描述符都存放在了一個數組中,用戶不再需要手動遍歷每一個描述符以判斷是否是準備好的,大大提高了效率。通常在處理高并發的客戶端請求時都會使用epoll代替上面兩個。
epoll使用struct epoll_event結構體來存儲描述符和事件
events可以是以下幾種的或運算結果
- EPOLLIN:描述符可讀
- EPOLLOUT:描述符可寫
- EPOLLERR:描述符出錯
- EPOLLHUP:描述符被掛斷
- EPOLLET:設置為邊緣觸發,相對于水平觸發而言
- …
epoll提供了幾個接口
/* 創建一個epoll監聽描述符,參數為最大需要監聽描述符的個數 */ int epoll_create(int size);/* *epoll的事件注冊函數*epfd: 通過epoll_create創建的監聽描述符*op: 想要執行的操作,添加描述符的監聽EPOLL_CTL_ADD, 修改描述符的監聽EPOLL_CTL_MOD,刪除描述符的監聽EPOLL_CTL_DEL*event: 對fd想要監聽的事件,或者更新的事件*/ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);/* *epoll阻塞等待函數*events是一個數組,用于存放所有準備好的描述符epoll_event*maxevents是最大可以接受多少個激活的epoll_event,不能超過epoll_create傳入的參數*timeout超時時長*/ int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);使用方法如下
//server.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h>#include <sys/epoll.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0)return 0;struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(8080);servaddr.sin_addr.s_addr = INADDR_ANY;if(bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){close(sockfd);return 0;}if(listen(sockfd, 10) < 0){close(sockfd);return 0;}int epollfd = epoll_create(1024);struct epoll_event events[1024];int fd_numbers = 1;struct epoll_event event;event.data.fd = sockfd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event); while(1){int n = epoll_wait(epollfd, events, fd_numbers, -1);if(n < 1)break;/* 直接遍歷events數組, 不需要遍歷所有fd判斷哪個fd被激活 */for(int i = 0; i < n; ++i){int fd = events[i].data.fd;short fd_event = events[i].events;/* 出現異常 */if((fd_event & EPOLLHUP) || (fd_event & EPOLLERR) || !(fd_event & EPOLLIN)){perror("epoll error");close(fd);epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);--fd_numbers;}/* 有客戶端請求連接服務器 */else if(fd == sockfd){struct sockaddr_in clientaddr;bzero(&clientaddr, sizeof(clientaddr));socklen_t len = sizeof(clientaddr);int client_fd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);struct epoll_event client_event;client_event.data.fd = client_fd;client_event.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, client_fd, &client_event);++fd_numbers;}else{char msg[4096];bzero(msg, sizeof(msg));int recv_ret = recv(fd, msg, sizeof(msg), 0);/* 如果客戶端close,那么客戶端fd會變為可讀,讀取時返回0表示連接關閉*/if(recv_ret == 0){printf("close connection with %d\n", fd);close(fd);epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);--fd_numbers;}else if(recv_ret > 0){printf("recv from %d: %s\n", fd, msg);char reply[] = "server has received client message";if(send(fd, reply, strlen(reply), MSG_NOSIGNAL) == 0){printf("close connection with %d\n", fd);close(fd);epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);--fd_numbers;}}}}}close(epollfd);close(sockfd);return 0; } //client.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h>#include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in sockaddr;bzero(&sockaddr, sizeof(sockaddr));sockaddr.sin_family = AF_INET;sockaddr.sin_port = htons(8080);sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//inet_aton(&sockaddr.sin_addr, "127.0.0.1");if(connect(sockfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0){close(sockfd);return 0;}char msg[4096];while(1){bzero(msg, sizeof(msg));scanf("%s", msg);if(strcmp(msg, "exit") == 0){break;}send(sockfd, msg, strlen(msg), MSG_NOSIGNAL);int len = recv(sockfd, msg, sizeof(msg), 0);msg[len] = '\0';printf("receive from server %s\n", msg);}close(sockfd);return 0; }epoll相比于select與poll,克服了二者的缺點
- epoll在每次epoll_ctl添加fd時就將這個fd拷貝到內核,保證了整個epoll過程中同一個fd只拷貝一次,而select每次都需要重新將所有fd拷貝到內核
- epoll在epoll_ctl時為每一個fd設置一個回調函數,當這個fd就緒,就會調用相應回調函數,函數中將對應的fd添加到一個就緒鏈表,epoll_wait實際上就在就緒鏈表中查看所有就緒的fd。而select和poll則需要遍歷整個fd以判斷是否有哪個fd就緒。epoll采用回調機制提升性能,效果明顯
- epoll可以支持的描述符數量很大
總結
以上是生活随笔為你收集整理的linux网络编程-----几种服务器模型及io多路复用函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: libevent源码学习-----统一事
- 下一篇: linux网络编程-----TCP连接及