原文做者:aircrafthtml
原文連接:https://www.cnblogs.com/DOMLX/p/9613861.html前端
好了,繼上一篇說到多進程服務端也是有缺點的,每建立一個進程就表明大量的運算與內存空間佔用,相互進程數據交換也很麻煩。python
本章的I/O模型就是能夠解決這個問題的其中一種模型。。。廢話很少說進入主題--linux
I/O複用技術主要就是select函數的使用。ios
一.I/O複用預備知識--select()函數用法與做用c++
select()用來肯定一個或多個套接字的狀態(更爲本質一點來說是文件描述符的狀態)。
編程
使用select()所須要包含的頭文件是:#include<sys/select.h>
windows
函數原型爲:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout);後端
接下來根據函數原型一點點的介紹一下select()函數。數組
(1),struct fd_set 這是一個集合,這個集合中存放的是文件描述符(在unix、linux系統中任何的設備、管道、FIFO等均可經過文件描述符的形式來訪問)。固然一個socket也是一個文件描述符啦。相關的操做有:
FD_ZERO(fd_set *)將某一個集合清空
FD_SET(int, fd_set *)將一個給定的文件描述符加入到集合之中
FD_CLR(int, fd_set *)從集合中刪除指定的文件描述符。
FD_ISSET(int, fd_set *)檢查集合中指定的文件描述符是否準備好(可讀或可寫)
(2),struct timeval這是經常使用的一個結構體,用來表示時間值,有兩個結構體成員:tv_sec表示秒數和tv_usec表示毫秒數。
接下來具體解釋一下select的參數:
nfds:一個整數值,表示的是所要監視的文件描述符的範圍。即你所要監聽的文件描述符的最大值+1(由於select()函數進行遍歷的時候是從0-文件描述符開始遍歷的)。
readfds:是指向fd_set結構的指針,這個集合中加入咱們所須要監視的文件可讀操做的文件描述符。
writefds:指向fd_set結構的指針,這個集合中加入咱們所須要監視的文件可寫操做的文件描述符。
exceptfds:指向fd_set結構的指針,這個集合中加入咱們所須要監視的文件錯誤異常的文件描述符。
timeout:指向timeval結構體的指針,經過傳入的這個timeout參數來決定select()函數的三種執行方式:
1.傳入的timeout爲NULL,則表示將select()函數置爲阻塞狀態,直到咱們所監視的文件描述符集合中某個文件描述符發生變化是,纔會返回結果。
2.傳入的timeout爲0秒0毫秒,則表示將select()函數置爲非阻塞狀態,無論文件描述符是否發生變化均馬上返回繼續執行。
3.傳入的timeout爲一個大於0的值,則表示這個值爲select()函數的超時時間,在timeout時間內一直阻塞,超過期間即返回結果。
而後該說一說select()函數的返回值了:
返回-1:select()函數錯誤,並將全部描述符集合清0,具體的錯誤能夠經過errno輸出來查看(在windows下經過GetLastError獲取相應的錯誤代碼)。
返回0:表示select()函數超時。
返回正數:返回的正數值表示已經準備好的描述符數。
注意在每次select()函數調用之後,都須要將集合清空,由於狀態已經改變,若須要從新監視就須要從新清空後在加入須要監視的文件描述符。
下面經過示例把select函數全部知識點進行整合,但願各位經過以下示例徹底理解以前的內容。
linux下監控鍵盤數據:
#include <sys/time.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <assert.h> int main () { int keyboard; int ret,i; char c; fd_set readfd; struct timeval timeout; keyboard = open("/dev/tty",O_RDONLY | O_NONBLOCK); assert(keyboard>0); while(1) { //設置select函數的超時 timeout.tv_sec=1; timeout.tv_usec=0; //初始化fd_set結構體變量 FD_ZERO(&readfd); FD_SET(keyboard,&readfd); ///監控函數 ret=select(keyboard+1,&readfd,NULL,NULL,&timeout); if(ret == -1) //錯誤狀況 cout<<"error"<<endl ; else if(ret) //返回值大於0 有數據到來 if(FD_ISSET(keyboard,&readfd)) { i=read(keyboard,&c,1); if('\n'==c) continue; printf("hehethe input is %c\n",c); if ('q'==c) break; } else //超時狀況 { cout<<"time out"<<endl; continue; } } }
好了大概對select函數有必定的認知了,下面經過select函數實現I/O複用服務端。
二.基於I/O複用的回聲服務端
什麼是I/O複用?通俗點講,其實就是一個事件監聽,只是這個監聽的事件通常是I/O操做裏的讀(read)與寫(write),只要發生了監聽的事件它就會響應。注意與通常服務器的區別,通常服務器是鏈接請求先進入請求隊列裏,而後,服務端套接字一個個有序去受理。而I/O複用服務器是事件監聽,只要對應監聽事件發生就會響應,是屬於併發服務器的一種。
I/O複用的使用
1,I/O複用的使用其實就是對select函數的使用,說select函數是I/O複用的所有內容也不爲過。但這個函數與通常函數不一樣,它很難使用,咱們先來看看它的調用順序,分爲3步:
步驟一:
步驟二:
步驟三:
下面給出LINUX下基於I/O複用服務端實現代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/time.h> #include <sys/select.h> #define BUF_SIZE 100 void error_handling(char *message); int main(int argc, const char * argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; struct timeval timeout; fd_set reads, cpy_reads; socklen_t adr_sz; int fd_max, str_len, fd_num; char buf[BUF_SIZE]; if (argc != 2) { printf("Usage: %s <port> \n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); if(listen(serv_sock, 5) == -1) error_handling("listen() error"); FD_ZERO(&reads); //向要傳到select函數第二個參數的fd_set變量reads註冊服務器端套接字 FD_SET(serv_sock, &reads); fd_max = serv_sock; while (1) { cpy_reads = reads; timeout.tv_sec = 5; timeout.tv_usec = 5000; //監聽服務端套接字和與客服端鏈接的服務端套接字的read事件 if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) break; if(fd_num == 0) continue; if (FD_ISSET(serv_sock, &cpy_reads))//受理客服端鏈接請求 { adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); FD_SET(clnt_sock, &reads); if(fd_max < clnt_sock) fd_max = clnt_sock; printf("connected client: %d \n", clnt_sock); } else//轉發客服端數據 { str_len = read(clnt_sock, buf, BUF_SIZE); if (str_len == 0)//客服端發送的退出EOF { FD_CLR(clnt_sock, &reads); close(clnt_sock); printf("closed client: %d \n", clnt_sock); } else { //接收數據爲字符串時執行回聲服務 write(clnt_sock, buf, str_len); } } } close(serv_sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
下面給出LINUX下基於I/O複用客戶端實現代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, const char * argv[]) { int sock; char message[BUF_SIZE]; int str_len, recv_len, recv_cnt; struct sockaddr_in serv_adr; if(argc != 3) { printf("Usage: %s <IP> <port> \n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if(sock == -1) error_handling("socket() error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) error_handling("connect() error"); else puts("Connected ..............."); while (1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break; str_len = write(sock, message, strlen(message)); /*這裏須要循環讀取,由於TCP沒有數據邊界,不循環讀取可能出現一個字符串一次發送 但分屢次讀取而致使輸出字符串不完整*/ recv_len = 0; while (recv_len < str_len) { recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1); if(recv_cnt == -1) error_handling("read() error"); recv_len += recv_cnt; } message[recv_len] = 0; printf("Message from server: %s", message); } close(sock); return 0; }
下面給出windows下I/O複用socket服務端代碼:
#include<iostream> #include<WinSock2.h> #pragma comment(lib,"ws2_32.lib") #define bufsize 1024 using namespace std; void main() { WSADATA wsadata; SOCKET serverSocket,clientSocket; int szClientAddr,fdnum,str_len; SOCKADDR_IN serverAddr, clientAddr; fd_set reads, cpyReads; TIMEVAL timeout; char message[bufsize] = "\0"; if(WSAStartup(MAKEWORD(2, 2), &wsadata)!=0) cout<<"WSAStartup() error"<<endl; serverSocket = socket(PF_INET, SOCK_STREAM, 0); if(serverSocket == INVALID_SOCKET) cout<<"socket() error"<<endl; memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(9999); if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) cout << "bind () error" << endl; listen(serverSocket, 5); cout << "服務器啓動成功!" << endl; FD_ZERO(&reads); //全部初始化爲0 FD_SET(serverSocket, &reads); //將服務器套接字存入 while (1) { cpyReads = reads; timeout.tv_sec = 5; //5秒 timeout.tv_usec = 5000; //5000毫秒 //找出監聽中發出請求的套接字 if ((fdnum = select(0, &cpyReads, 0, 0, &timeout)) == SOCKET_ERROR) break; if (fdnum == 0) { cout << "time out!" << endl; continue; } for (unsigned int i = 0; i < reads.fd_count; i++) { if (FD_ISSET(reads.fd_array[i], &cpyReads)) { //判斷是否爲發出請求的套接字 if (reads.fd_array[i] == serverSocket) { //是否爲服務器套接字 szClientAddr = sizeof(clientAddr); clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &szClientAddr); if (clientSocket == INVALID_SOCKET) cout << "accept() error" << endl; FD_SET(clientSocket, &reads); cout << "鏈接的客戶端是:" << clientSocket << endl; } else {//否 就是客戶端 str_len = recv(reads.fd_array[i], message, bufsize - 1, 0); if (str_len == 0) {//根據接受數據的大小 判斷是不是關閉 FD_CLR(reads.fd_array[i], &reads); //清除數組中該套接字 closesocket(cpyReads.fd_array[i]); cout << "關閉的客戶端是:" << cpyReads.fd_array[i] << endl; } else { send(reads.fd_array[i], message, str_len, 0); } } } } } closesocket(clientSocket); closesocket(serverSocket); WSACleanup(); }
下面給出windows下I/O複用socket客戶端代碼:
#include<iostream> #include<WinSock2.h> #pragma comment(lib,"ws2_32.lib") #define bufsize 1024 using namespace std; void main() { WSADATA wsadata; SOCKET clientSocket; SOCKADDR_IN serverAddr; int recvCnt; char message[bufsize] = "\0"; if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) cout << "WSAStartup() error" << endl; if ((clientSocket = socket(PF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) cout << "socket() error" << endl; serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); serverAddr.sin_port = htons(9999); if(connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr))==SOCKET_ERROR) cout<<"connect() error"<<endl; while (1) { cout << "輸入Q或q退出:"; cin >> message; if (!strcmp(message, "Q") || !strcmp(message, "q")) break; send(clientSocket, message, strlen(message), 0); memset(message, 0, sizeof(message)); recv(clientSocket, message, bufsize, 0); cout << "服務器結果:" << message << endl; } closesocket(clientSocket); WSACleanup(); }
最後說一句啦。本網絡編程入門系列博客是連載學習的,有興趣的能夠看我博客其餘篇。。。。c++ 網絡編程課設入門超詳細教程 ---目錄
參考博客:https://blog.csdn.net/zl908760230/article/details/70257229
參考博客:https://blog.csdn.net/hshl1214/article/details/45872243
參考博客:https://blog.csdn.net/u010223072/article/details/48133725
參考書籍:《TCP/IP 網絡編程 --尹聖雨》
如有興趣交流分享技術,可關注本人公衆號,裏面會不按期的分享各類編程教程,和共享源碼,諸如研究分享關於c/c++,python,前端,後端,opencv,halcon,opengl,機器學習深度學習之類有關於基礎編程,圖像處理和機器視覺開發的知識