首先看原先《UNIX網絡編程——併發服務器(TCP)》的代碼,服務器代碼serv.c:編程
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #include<signal.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) void do_service(int); int main(void) { signal(SIGCHLD, SIG_IGN); int listenfd; //被動套接字(文件描述符),即只能夠accept, 監聽套接字 if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */ /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt error"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind error"); if (listen(listenfd, SOMAXCONN) < 0) //listen應在socket和bind以後,而在accept以前 ERR_EXIT("listen error"); struct sockaddr_in peeraddr; //傳出參數 socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數,必須有初始值 int conn; // 已鏈接套接字(變爲主動套接字,便可以主動connect) pid_t pid; while (1) { if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完成的序列 { if( errno == EINTR ) ///////////////////////////////////////////////////////////////////必須處理被中斷的系統調用 continue; else ERR_EXIT("accept error"); } printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid == 0) { // 子進程 close(listenfd); do_service(conn); exit(EXIT_SUCCESS); } else close(conn); //父進程 } return 0; } void do_service(int conn) { char recvbuf[1024]; while (1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret = read(conn, recvbuf, sizeof(recvbuf)); if (ret == 0) //客戶端關閉了 { printf("client close\n"); break; } else if (ret == -1) ERR_EXIT("read error"); fputs(recvbuf, stdout); write(conn, recvbuf, ret); } }
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect error"); struct sockaddr_in localaddr; char cli_ip[20]; socklen_t local_len = sizeof(localaddr); memset(&localaddr, 0, sizeof(localaddr)); if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 ) ERR_EXIT("getsockname error"); inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip)); printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port)); char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { write(sock, sendbuf, strlen(sendbuf)); read(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); } close(sock); return 0; }先運行服務器端,再運行客戶端:
huangcheng@ubuntu:~$ ./serv huangcheng@ubuntu:~$ ./cli
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2750/serv tcp 0 0 127.0.0.1:49484 127.0.0.1:5188 ESTABLISHED 2751/cli tcp 0 0 127.0.0.1:5188 127.0.0.1:49484 ESTABLISHED 2752/serv
能夠看出創建了鏈接,服務器端有兩個進程,一個父進程處於監聽狀態,另外一子進程正在對客戶端進行服務。ubuntu
服務器端的子進程的pid爲2752,並kill掉它:數組
huangcheng@ubuntu:~$ kill -9 2752
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2750/serv tcp 1 0 127.0.0.1:49484 127.0.0.1:5188 CLOSE_WAIT 2751/cli tcp 0 0 127.0.0.1:5188 127.0.0.1:49484 FIN_WAIT2 -
來分析一下,咱們將server子進程 kill掉,則其終止時,socket描述符會自動關閉併發FIN段給client,client收到FIN後處於CLOSE_WAIT狀態,可是client並無終止,也沒有關閉socket描述符,所以不會發FIN給 server子進程,所以server 子進程的TCP鏈接處於FIN_WAIT2狀態。服務器
爲何會出現這種狀況呢,來看client的部分程序:網絡
char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { write(sock, sendbuf, strlen(sendbuf)); read(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); }客戶端程序阻塞在了fgets 那裏,即從標準輸入讀取數據,因此不能執行到下面的read,也即不能返回0,不會退出循環,不會調用close關閉sock,因此出現上述的狀況,即狀態停滯,不能向前推動。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數1:讀寫異常集合中的文件描述符的最大值加1;併發
參數2:讀集合,關心可讀事件;socket
套接口緩衝區有數據可讀
鏈接的讀一半關閉,即接收到FIN段,讀操做將返回0
若是是監聽套接口,已完成鏈接隊列不爲空時。
套接口上發生了一個錯誤待處理,錯誤能夠經過getsockopt指定SO_ERROR選項來獲取。
參數3:寫集合,關心可寫事件;tcp
套接口發送緩衝區有空間容納數據。函數
鏈接的寫一半關閉。即收到RST段以後,再次調用write操做。學習
套接口上發生了一個錯誤待處理,錯誤能夠經過getsockopt指定SO_ERROR選項來獲取。
參數4:異常集合,關心異常事件;
套接口存在帶外數據(TCP頭部 URG標誌,16位緊急指針字段)
參數5:超時時間結構體
對於參數2,3,4來講,若是不關心對應事件則設置爲NULL便可。注意5個參數都是輸入輸出參數,即select返回時可能對其進行了修改,好比集合被修改以便標記哪些套接口發生了事件,時間結構體的傳出參數是剩餘的時間,若是設置爲NULL表示永不超時。用select管理多個I/O,select阻塞等待,一旦其中的一個或多個I/O檢測到咱們所感興趣的事件,select函數返回,返回值爲檢測到的事件個數,而且返回哪些I/O發送了事件,遍歷這些事件,進而處理事件。注意當select阻塞返回後,此時調用read/write 是不會阻塞的,由於正是有可讀可寫事件發生才致使select 返回,也能夠認爲是select 提早阻塞了。
下面是4個能夠對集合進行操做的宏:
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); // 將集合清零
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect error"); struct sockaddr_in localaddr; char cli_ip[20]; socklen_t local_len = sizeof(localaddr); memset(&localaddr, 0, sizeof(localaddr)); if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 ) ERR_EXIT("getsockname error"); inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip)); printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port)); fd_set rset; FD_ZERO(&rset); int nready; int maxfd; int fd_stdin = fileno(stdin); // if (fd_stdin > sock) maxfd = fd_stdin; else maxfd = sock; char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (1) { FD_SET(fd_stdin, &rset); FD_SET(sock, &rset); nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //select返回表示檢測到可讀事件 if (nready == -1) ERR_EXIT("select error"); if (nready == 0) continue; if (FD_ISSET(sock, &rset)) { int ret = read(sock, recvbuf, sizeof(recvbuf)); if (ret == -1) ERR_EXIT("read error"); else if (ret == 0) //服務器關閉 { printf("server close\n"); break; } fputs(recvbuf, stdout); memset(recvbuf, 0, sizeof(recvbuf)); } if (FD_ISSET(fd_stdin, &rset)) { if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) break; write(sock, sendbuf, strlen(sendbuf)); memset(sendbuf, 0, sizeof(sendbuf)); } } close(sock); return 0; }即將兩個事件都添加進可讀事件集合,在while循環中,若是select返回說明有事件發生,依次判斷是哪些事件發生,若是是標準輸入有數據可讀,則讀取後再次回到循環開頭select阻塞等待事件發生,若是是套接口有數據可讀,且返回爲0則說明對方已經關閉鏈接,退出循環並調用close關閉sock。
重複前面的操做:
(1)先運行服務器,再運行客戶端
huangcheng@ubuntu:~$ ./serv huangcheng@ubuntu:~$ ./cli(2)查看網絡狀態:
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2960/serv tcp 0 0 127.0.0.1:49485 127.0.0.1:5188 ESTABLISHED 2963/cli tcp 0 0 127.0.0.1:5188 127.0.0.1:49485 ESTABLISHED 2964/serv(3)kill掉服務器的子進程,再查看網絡狀態:
huangcheng@ubuntu:~$ kill -9 2964 huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2960/serv tcp 0 0 127.0.0.1:5188 127.0.0.1:49485 TIME_WAIT -
即 client 關閉socket描述符,server 子進程的TCP鏈接收到client發的FIN段後處於TIME_WAIT狀態,此時會再發生一個ACK段給client,client接收到以後就處於CLOSED狀態,這個狀態存在時間很短,因此看不到客戶端的輸出條目,TCP協議規定,主動關閉鏈接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximumsegment lifetime)的時間後才能回到CLOSED狀態,須要有MSL 時間的主要緣由是在這段時間內若是最後一個ack段沒有發送給對方,則能夠從新發送。
過一小會再次查看網絡狀態:
huangcheng@ubuntu:~$ netstat -anp | grep 5188 (並不是全部進程都能被檢測到,全部非本用戶的進程信息將不會顯示,若是想看到全部信息,則必須切換到 root 用戶) tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 2960/serv能夠發現只剩下服務器端父進程的監聽狀態了,由TIME_WAIT狀態轉入CLOSED狀態,也很快會消失。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
前面咱們實現的可以併發服務的服務器端程序是使用fork出多個子進程來實現的,如今學習了select函數,能夠用它來改進服務器端程序,實現單進程併發服務。先看以下程序,再來解釋:
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #include<signal.h> #include<sys/wait.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) int main(void) { signal(SIGPIPE, SIG_IGN); int listenfd; //被動套接字(文件描述符),即只能夠accept, 監聽套接字 if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) // listenfd = socket(AF_INET, SOCK_STREAM, 0) ERR_EXIT("socket error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */ /* inet_aton("127.0.0.1", &servaddr.sin_addr); */ int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt error"); if (bind(listenfd, (struct sockaddr*)&servaddr,sizeof(servaddr)) < 0) ERR_EXIT("bind error"); if (listen(listenfd, SOMAXCONN) < 0) //listen應在socket和bind以後,而在accept以前 ERR_EXIT("listen error"); struct sockaddr_in peeraddr; //傳出參數 socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數,必須有初始值 int conn; // 已鏈接套接字(變爲主動套接字,便可以主動connect) int i; int client[FD_SETSIZE]; int maxi = 0; // client數組中最大不空閒位置的下標 for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; int nready; int maxfd = listenfd; fd_set rset; fd_set allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(listenfd, &allset); while (1) { rset = allset; nready = select(maxfd + 1, &rset, NULL, NULL, NULL); if (nready == -1) { if (errno == EINTR) continue; ERR_EXIT("select error"); } if (nready == 0) continue; if (FD_ISSET(listenfd, &rset)) { conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen); //accept再也不阻塞 if (conn == -1) ERR_EXIT("accept error"); for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) { client[i] = conn; if (i > maxi) maxi = i; break; } } if (i == FD_SETSIZE) { fprintf(stderr, "too many clients\n"); exit(EXIT_FAILURE); } printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); FD_SET(conn, &allset); if (conn > maxfd) maxfd = conn; if (--nready <= 0) continue; } for (i = 0; i <= maxi; i++) { conn = client[i]; if (conn == -1) continue; if (FD_ISSET(conn, &rset)) { char recvbuf[1024] = {0}; int ret = read(conn, recvbuf, 1024); if (ret == -1) ERR_EXIT("readline error"); else if (ret == 0) { //客戶端關閉 printf("client close \n"); FD_CLR(conn, &allset); client[i] = -1; close(conn); } fputs(recvbuf, stdout); write(conn, recvbuf, strlen(recvbuf)); if (--nready <= 0) break; } } } return 0; } /* select所能承受的最大併發數受 * 1.一個進程所能打開的最大文件描述符數,能夠經過ulimit -n來調整 * 但一個系統所能打開的最大數也是有限的,跟內存有關,能夠經過cat /proc/sys/fs/file-max 查看 * 2.FD_SETSIZE(fd_set)的限制,這個須要從新編譯內核 */
程序第一次進入while 循環,只把監聽套接字加入關心的事件,select返回說明監聽套接字有可讀事件,即已完成鏈接隊列不爲空,這時調用accept不會阻塞,返回一個已鏈接套接字,將這個套接字加入allset,由於第一次運行則nready = 1,直接continue跳回到while 循環開頭,再次調用select,此次會關心監聽套接字和一個已鏈接套接字的可讀事件,若是繼續有客戶端鏈接上來則繼續將其加入allset,此次nready = 2,繼續執行下面的for 循環,而後對客戶端進行服務。服務完畢再次回到while 開頭調用select 阻塞時,就關心一個監聽套接字和2個已鏈接套接字的可讀事件了,一直循環下去。
程序大概邏輯就這樣,一些細節就你們本身想一想了,好比client數組是用來保存已鏈接套接字的,爲了不每次都得遍歷到FD_SETSIZE-1,保存一個最大不空閒下標maxi,每次遍歷到maxi就能夠了。每次獲得一個conn,要判斷一下conn與maxfd的大小。
當得知某個客戶端關閉,則須要將conn在allset中清除掉。之因此要有allset 和 rset 兩個變量是由於rset是傳入傳出參數,在select返回時rset可能被改變,故須要每次在回到while 循環開頭時須要將allset 從新賦予rset 。