12、close與shutdown區別
阿新 • • 發佈:2018-12-12
close終止了資料傳送的兩個方向。 shutdown可以有選擇的終止某個方向的資料傳送或者終止資料傳送的兩個方向。 shutdown how=1就可以保證對等方接收到一個EOF字元,而不管其他程序是否已經打開了套接字。而close不能保證,直到套接字引用計數減為0時才傳送。也就是說直到所有的程序都關閉了套接字。
思考1 客戶端向伺服器傳送:FIN(close) E D C B A, 問:伺服器還能收到資料嗎?伺服器還可以向客戶端回報文嗎? 客戶端想在關閉之後,仍然能接收到回射伺服器應答(shutdown)。 思考2 父程序中close(conn);會不會向客戶端傳送FIN報文段那? 檔案的引用計數-1,當減少為0,才會傳送引用計數。 思考3: 客戶端//shutdown(sock, SHUT_WR);只關閉了寫; 言外之意我可以接收資料
測試close與shutdown的區別
第一種測試場景
執行結果如下
現在呼叫shutdown 實驗結果
原始碼:
9client_readline.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <netinet/ip.h> /* superset of previous */ #include <arpa/inet.h> #include <errno.h> #include <unistd.h> #include <signal.h> /* 包尾加上\n程式設計實踐 */ #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ }while(0) /* 使用說明: //1一次全部讀走 //2次讀完資料 //出錯分析 //對方已關閉 思想: tcpip是流協議,不能保證1次讀操作,能全部把報文讀走,所以要迴圈 讀指定長度的資料。 按照count大小讀資料,若讀取的長度ssize_t<count 說明讀到了一個結束符, 對方已關閉 函式功能: 從一個檔案描述符中讀取count個字元到buf中 引數: @buf:接受資料記憶體首地址 @count:接受資料長度 返回值: @ssize_t:返回讀的長度 若ssize_t<count 讀失敗失敗 */ ssize_t readn(int fd, void *buf, size_t count) { size_t nleft = count; //剩下需要讀取的資料個數 ssize_t nread; //成功讀取的位元組數 char * bufp = (char*)buf;//將引數接過來 while (nleft > 0) { //如果errno被設定為EINTR為被訊號中斷,如果是被訊號中斷繼續, //不是訊號中斷則退出。 if ((nread = read(fd, bufp, nleft)) < 0) { //異常情況處理 if (errno == EINTR) //讀資料過程中被訊號中斷了 continue; //再次啟動read //nread = 0; //等價於continue return -1; }else if (nread == 0) //到達檔案末尾EOF,資料讀完(讀檔案、讀管道、socket末尾、對端關閉) break; bufp += nread; //將字串指標向後移動已經成功讀取個數的大小。 nleft -=nread; //需要讀取的個數=需要讀取的個數-已經成功讀取的個數 } return (count - nleft);//返回已經讀取的資料個數 } /* 思想:tcpip是流協議,不能1次把指定長度資料,全部寫完 按照count大小寫資料 若讀取的長度ssize_t<count 說明讀到了一個結束符,對方已關閉。 函式功能: 向檔案描述符中寫入count個字元 函式引數: @buf:待寫資料首地址 @count:待寫長度 返回值: @ssize_t:返回寫的長度 -1失敗 */ ssize_t writen(int fd, void *buf, size_t count) { size_t nleft = count; //需要寫入的個數 ssize_t nwritten; //成功寫入的位元組數 char * bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) <= 0) { //異常情況處理 訊號打斷,則繼續 if ((nwritten < 0) && (errno == EINTR)) //讀資料過程中被訊號中斷了 continue; //再次啟動write //nwritten = 0; //等價continue else return -1; } bufp += nwritten; //移動緩衝區指標 nleft -=nwritten; //記錄剩下未讀取的資料 } return count;//返回已經讀取的資料個數 } //讀資料,但不把資料緩衝區清空 //@ssize_t返回值:返回緩衝區資料的長度 -1失敗 ssize_t recv_peek(int sockfd, void *buf, size_t len) { while (1) { //MSG_PEEK 讀取佇列中指定大小的資料,但不取出 int ret = recv(sockfd, buf, len, MSG_PEEK); //如果被訊號中斷,則繼續 if (ret == -1 && errno == EINTR) continue; return ret; } } /* maxline 一行最大數 先提前peek一下緩衝區,如果有資料從緩衝區讀資料, 1、緩衝區資料中帶\n 2 、快取區中不帶\n 讀取資料包直到\n 功能:按行讀取檔案,只要遇到\n就,讀走資料,返回, @buf 接收資料記憶體首地址 @maxline 接收資料記憶體最大值 */ ssize_t readline(int sockfd, void *buf, size_t maxline) { int ret; int nread; //成功預讀取的資料個數 char *bufp = buf; //讀取資料存放的陣列,在外分配記憶體 int nleft = maxline;//封包最大值 while (1) { //看一看緩衝區有沒有資料,並不移除核心緩衝區資料 //讀資料,但不把資料緩衝區清空,成功:ret是報文的長度 ret = recv_peek(sockfd, bufp, nleft); if (ret < 0) //失敗 return ret; else if (ret == 0)//對方已關閉 return ret; nread = ret; int i; //讀資料,但不把資料緩衝區清空,避免了一個位元組一個位元組的讀資料 //先利用recv的MSG_PEEK功能,預讀資料,然後查詢\n //根據\n的位置,根據指定長度,再真正的讀資料 for (i = 0; i < nread; i++) { if (bufp[i] == '\n') //若緩衝區有\n { ret = readn(sockfd, bufp, i+1);//將資料從快取區讀走 if (ret != i + 1) exit(EXIT_FAILURE); return ret;//有\n就返回,並返回讀走的資料 } } //若資料長度 nread > 緩衝區最大長度maxline 退出 if (nread > nleft) exit(EXIT_FAILURE); //若沒有\n,說明訊息還沒有結束,不是完整的一條訊息,就把這些資料也讀到buf緩衝區中。 //依此迴圈,直到遇到\n,把整個一行資料,全部讀完,放入buf中 //bufp記錄了每次需追加的位置 nleft -= nread; ret = readn(sockfd, bufp, nread); if (ret != nread) exit(EXIT_FAILURE); bufp += nread; //bufp每次跳到追加的末尾 } return -1; } void echo_cli(int sockfd) { char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { //寫資料(本身帶有\n), 所以不需要再單獨加\n,因為從stdin輸入完需要按下enter鍵 writen(sockfd, sendbuf, strlen(sendbuf)); #if 0 sleep(3); //測試對端關閉的情況下再次寫資料,造成客戶端產生SIGPIPE訊號,該訊號的預設動作是終止 writen(sockfd, "再次寫資料aaa....\n", 100); #endif //按照行讀資料 int ret = readline(sockfd, recvbuf, sizeof(recvbuf)); if (ret == -1) ERR_EXIT("readline()"); else if (ret == 0) { printf("server close\n"); break; } fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); } close(sockfd); } void test() { int sockfd = 0; const char *serverip = "192.168.66.128"; //若程式收到SIGPIPE,則忽略 signal(SIGPIPE, SIG_IGN); //建立socket if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) ERR_EXIT("socket()"); //定義socket結構體 man 7 ip struct sockaddr_in srvsddr; srvsddr.sin_family = AF_INET; srvsddr.sin_port = htons(8001);//轉化為網路位元組序 //第一種 #if 0 srvsddr.sin_addr.s_addr = inet_addr(serverip); #endif //第二種 #if 0 //srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在網路位元組序 //srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //繫結本機的任意一個地址 #endif //第三種 //建議使用這種 #if 1 int ret; ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr); if (ret == 0) { ERR_EXIT("inet_pton()"); } #endif //程序-》核心 if (connect(sockfd, (struct sockaddr*)&srvsddr, sizeof(srvsddr)) < 0) ERR_EXIT("connect"); struct sockaddr_in localaddr; socklen_t addrlen = sizeof(localaddr); //核心-》程序 //獲取本地的地址 注意是已連線以後的套接字 if ((getsockname(sockfd, (struct sockaddr *)&localaddr, &addrlen)) < 0) ERR_EXIT("getsockname()"); printf("本機的ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); echo_cli(sockfd); return ; } int main() { test(); return 0; }
10server_readline.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <netinet/ip.h> /* superset of previous */ #include <arpa/inet.h> #include <errno.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> /* 包尾加上\n程式設計實踐 */ #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ }while(0) /* 使用說明: //1一次全部讀走 //2次讀完資料 //出錯分析 //對方已關閉 思想: tcpip是流協議,不能保證1次讀操作,能全部把報文讀走,所以要迴圈 讀指定長度的資料。 按照count大小讀資料,若讀取的長度ssize_t<count 說明讀到了一個結束符, 對方已關閉 函式功能: 從一個檔案描述符中讀取count個字元到buf中 引數: @buf:接受資料記憶體首地址 @count:接受資料長度 返回值: @ssize_t:返回讀的長度 若ssize_t<count 讀失敗失敗 */ ssize_t readn(int fd, void *buf, size_t count) { size_t nleft = count; //剩下需要讀取的資料個數 ssize_t nread; //成功讀取的位元組數 char * bufp = (char*)buf;//將引數接過來 while (nleft > 0) { //如果errno被設定為EINTR為被訊號中斷,如果是被訊號中斷繼續, //不是訊號中斷則退出。 if ((nread = read(fd, bufp, nleft)) < 0) { //異常情況處理 if (errno == EINTR) //讀資料過程中被訊號中斷了 continue; //再次啟動read //nread = 0;//等價於continue return -1; }else if (nread == 0) //到達檔案末尾EOF,資料讀完(讀檔案、讀管道、socket末尾、對端關閉) break; bufp += nread; //將字串指標向後移動已經成功讀取個數的大小。 nleft -=nread; //需要讀取的個數=需要讀取的個數-已經成功讀取的個數 } return (count - nleft);//返回已經讀取的資料個數 } /* 思想:tcpip是流協議,不能1次把指定長度資料,全部寫完 按照count大小寫資料 若讀取的長度ssize_t<count 說明讀到了一個結束符,對方已關閉。 函式功能: 向檔案描述符中寫入count個字元 函式引數: @buf:待寫資料首地址 @count:待寫長度 返回值: @ssize_t:返回寫的長度 -1失敗 */ ssize_t writen(int fd, void *buf, size_t count) { size_t nleft = count; //需要寫入的個數 ssize_t nwritten; //成功寫入的位元組數 char * bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) <= 0) { //異常情況處理 訊號打斷,則繼續 if ((nwritten < 0) && (errno == EINTR)) //讀資料過程中被訊號中斷了 continue; //再次啟動write //nwritten = 0; //等價continue else return -1; } bufp += nwritten; //移動緩衝區指標 nleft -=nwritten; //記錄剩下未讀取的資料 } return count;//返回已經讀取的資料個數 } //從指定的socket中讀取指定大小的資料但不取出,封裝後不被訊號中斷 ssize_t recv_peek(int sockfd, void *buf, size_t len) { while (1) { //MSG_PEEK 讀取佇列中指定大小的資料,但不取出 int ret = recv(sockfd, buf, len, MSG_PEEK); //如果被訊號中斷,則繼續 if (ret == -1 && errno == EINTR) continue; return ret; } } /* maxline 一行最大數 先提前peek一下緩衝區,如果有資料從緩衝區讀資料, 1、緩衝區資料中帶\n 2 、快取區中不帶\n 讀取資料包直到\n */ ssize_t readline(int sockfd, void *buf, size_t maxline) { int ret; int nread; //成功預讀取的資料個數 char *bufp = buf; //讀取資料存放的陣列,在外分配記憶體 int nleft = maxline;//封包最大值 while (1) { //看一看緩衝區有沒有資料,並不移除核心緩衝區資料 ret = recv_peek(sockfd, bufp, nleft); if (ret < 0) //失敗 return ret; else if (ret == 0)//對方已關閉 return ret; nread = ret; int i; //逐字元讀取 for (i = 0; i < nread; i++) { if (bufp[i] == '\n') //若緩衝區有\n { ret = readn(sockfd, bufp, i+1);//讀走資料 if (ret != i + 1) exit(EXIT_FAILURE); return ret;//有\n就返回,並返回讀走的資料 } } //如果讀到的數大於 一行最大數 異常處理 if (nread > nleft) exit(EXIT_FAILURE); nleft -= nread;//若緩衝區沒有\n, 把剩餘的資料讀走 ret = readn(sockfd, bufp, nread); if (ret != nread) exit(EXIT_FAILURE); bufp += nread;//bufp指標後移後,再接著偷看緩衝區資料recv_peek,直到遇到\n } return -1; } void do_service(int conn) { char recvbuf[1024]; while (1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret = readline(conn, recvbuf, sizeof(recvbuf)); if (ret == -1) ERR_EXIT("readline()"); if (ret == 0) { printf("client close\n"); break; } //將資料列印輸出 fputs(recvbuf, stdout); writen(conn, recvbuf, strlen(recvbuf)); if (recvbuf[0] == '2') //注意2 一共2處。。。。 { //如果父程序沒有關閉,則客戶端應該關閉2次,才能往對端傳送FIN分節 //close(conn); //11111111111 //close(conn); //11111111111 shutdown(conn, SHUT_WR); } } } //正確的使用方法 void handle_sigchld2(int signo) { int mypid; while (( mypid = waitpid(-1, NULL, WNOHANG)) > 0) { printf("孩子退出,父程序要收屍:%d\n", mypid); } } void test() { int sockfd = 0; int conn = 0; const char *serverip = "192.168.66.128"; //安裝訊號處理函式 使用waitpid #if 1 signal(SIGCHLD, handle_sigchld2); #endif //建立socket if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) ERR_EXIT("socket()"); //定義socket結構體 man 7 ip struct sockaddr_in srvsddr; srvsddr.sin_family = AF_INET; srvsddr.sin_port = htons(8001);//轉化為網路位元組序 //第一種 #if 0 srvsddr.sin_addr.s_addr = inet_addr(serverip); #endif //第二種 #if 0 //srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在網路位元組序 //srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //繫結本機的任意一個地址 #endif //第三種 //建議使用這種 #if 1 int ret; ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr); if (ret == 0) { ERR_EXIT("inet_pton()"); } #endif //設定埠複用 //使用SO_REUSEADDR選項可以使得不必等待TIME_WAIT狀態消失就可以重啟伺服器 int optval = 1; if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) ERR_EXIT("setsockopt()"); if(bind(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) <0 ) ERR_EXIT("bind()"); if(listen(sockfd, SOMAXCONN) < 0) ERR_EXIT("listen()"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr);//值-結果引數 pid_t pid; while (1) { if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) ERR_EXIT("accept()"); printf("客戶端1 ip:%s port:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); #if 0 //方法2 只要能拿到連線socket就行 struct sockaddr_in clientaddr; socklen_t clientlen = sizeof(clientaddr); //注意是已連線以後的套接字 if (getpeername(conn, (struct sockaddr*)&clientaddr, &clientlen) < 0) ERR_EXIT("getpeername"); printf("客戶端2 ip=%s port=%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); #endif pid = fork(); if (pid == -1) { ERR_EXIT("fork()"); } if (pid == 0) { //子程序不需要監聽socket close(sockfd); do_service(conn); exit(EXIT_SUCCESS); }else { //必須要關閉掉,因為close()函式使用了引用計數技術 //close(conn);//父程序不需要連線socket 注意1.。。 } } return ; } int main() { test(); return 0; }