套接字错误处理函数的封装思想及函数实现
一、套接字錯誤處理函數的封裝思想
在上篇文章中的 CS 模型,存在 bug:
先關 server,再關 client,立即再啟動 server,此時會發現無法啟動。原因是:先關 server,再關 client,執行 shell 命令netstat -apn | grep 端口號,會發現服務器處于
“TIME_WAIT”狀態,即不是真正的退出狀態,也就
意味著,原先 server 端口號并沒有被釋放掉,再次啟動 server,因為端口號是固定的,而且正在被占用中,所以無法啟動,問題出現在 bind 函數調用失敗。
解決方法就是端口復用,代碼為了突出邏輯性,并沒有進行錯誤提示,這就導致使用者不知道錯誤的點,但如果進行錯誤提示,又會使代碼的可讀性變差,因此,采用錯誤處理函數封裝。
錯誤處理函數封裝本質是對庫函數進行從新封裝:
(1)完全按照庫函數的原型重新封裝函數,注意:函數名是原函數名首字母大寫 。
(2)封裝的函數體的內部實現添加錯誤處理,這樣做的好處是使用時候跟原庫函數沒有區別(因為是按庫函數進行封裝的,調用時只需注意函數名即可);相較于使用原庫函數,不需要自己進行錯誤判斷,因為內部已經封裝好了;利用 man 幫助文檔時候,函數名不區分大小寫,例如對于 man 命令,Socket 和 socket 是一樣的,因此在 man_page 中可以直接查看封裝后函數的使用方法。
使用方法:
對 server.c 和 client.c 中使用的函數,重新封裝,一并放入 wrap.c 中(此外會有一個 wrap.h,對這些封裝函數做出聲明),通過聯合編譯實現對原庫函數的功能擴展。
以server.c 為例:
二、套接字錯誤處理函數的實現
wrap.h:
#ifndef __WRAP_H_ #define __WRAP_H_ #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.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); int tcp4bind(short port,const char *IP); #endifwrap.c
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <strings.h> void perr_exit(const char *s) // 錯誤信息處理函數 // 這層封裝的目的是為了減少下方代碼量 {perror(s); // 輸出錯誤信息exit(-1); // 退出進程 }int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) // 阻塞等待客戶端連接函數 {int n; // 控制返回值again:if ((n = accept(fd, sa, salenptr)) < 0){if ((errno == ECONNABORTED) || (errno == EINTR))goto again; // 如果依據錯誤號,表示錯誤是“連接時異常斷 開”或者“阻塞時被信號打斷”else (這2個錯誤都會使accept調用失敗),需要重新調用perr_exit("accept error");}return n; }int Bind(int fd, const struct sockaddr *sa, socklen_t salen) // 綁定端口和IP {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) // > 0,讀取的字節數, = 0,讀到末尾(對端關閉),= -1 讀異常{if (errno == EINTR) // 如果讀阻塞時候被信號打斷,恢復原讀阻塞goto again;elsereturn -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;elsereturn -1; }return n; }int Close(int fd) // 關閉套接字 {int n;if ((n = close(fd)) == -1)perr_exit("close error");return n; }// 參數:文件描述符,存放讀取數據的緩沖區的首地址(傳出參數),要讀多少字節(小于vptr長度) // 返回值:成功 返回已讀取的字節數 ;失敗 :返回-1 ssize_t Readn(int fd, void *vptr, size_t n) // 功能:讀取n個字節 { size_t nleft; // usigned int 剩余未讀取的字節數ssize_t nread; // int 實際讀到的字節數char *ptr; // 指針,用于定位開始讀的位置ptr = vptr; // 從緩存區頭部開始存nleft = n; // 指定要讀取的字節數while (nleft > 0) // 如果還有字節沒有被讀 ? 一直讀取{ if ((nread = read(fd, ptr, nleft)) < 0) // 如果發現讀失敗{ if (errno == EINTR)nread = 0; // 如果是因為信號打斷造成的,認為沒讀到,但不退出(直接跳轉到3)elsereturn -1; // 如果不是信號打斷的,認為讀異常,退出,返回-1} else if (nread == 0) // 如果發現全部讀完(或對方關閉),退出循環,不再繼續讀break; // 從此處退出循環,意味著函數調用成功,返回n// 讀行為正常nleft -= nread; // 更新剩余未讀取的字節數ptr += nread; // 指針后移,定位在下次開始存儲的位置}return n - nleft; // 返回已讀取的字節數 }ssize_t Writen(int fd, const void *vptr, size_t n) // 功能:寫入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; // 本質:忽略 信號打斷造成的情況elsereturn -1;}nleft -= nwritten;ptr += nwritten; }return n; }static ssize_t my_read(int fd, char *ptr) // Readline() 的子函數 {static int read_cnt; // read函數返回值,代表讀到多少個字節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) // 1. 正常讀,但是如果讀失敗{if (errno == EINTR)goto again;return -1; // 讀取1異常 返回-1 }else if (read_cnt == 0) // 2. 正常讀到結尾return 0; // 讀取到末尾(對端關閉) 返回0read_ptr = read_buf; // 3. 既不是異常,也沒讀到結尾,read_ptr 指向 存儲緩存區的首地址}read_cnt--; *ptr = *read_ptr++;return 1; // 讀取成功 返回1 }// 參數:文件描述符,存放讀取數據緩沖區的首地址,緩沖區的大小 // 返回值:成功 0; 失敗 -1 ssize_t Readline(int fd, void *vptr, size_t maxlen) // 功能:讀取一行 {ssize_t n, rc; // my_read 函數返回值char c, *ptr; // 定義my_read讀取的字符 // 定義開始存儲的位置ptr = vptr; // 最開始從緩存區頭開始存儲for (n = 1; n < maxlen; n++){if ( (rc = my_read(fd, &c)) == 1) // 如果讀取1字節成功{ *ptr++ = c; // 將讀取的結果,賦給ptr,同時ptr后移if (c == '\n')break; // 如果到達行末尾,退出if,意味著1行讀取成功,補0,然后return}else if (rc == 0) { *ptr = 0; // 如果正常讀到結尾,直接結尾補0return n - 1;}elsereturn -1; // 剩余情況就是讀異常,返回-1 }*ptr = 0; // 最后位置補0return n; }int tcp4bind(short port,const char *IP) {struct sockaddr_in serv_addr;int lfd = Socket(AF_INET,SOCK_STREAM,0);bzero(&serv_addr,sizeof(serv_addr));if(IP == NULL) {//如果這樣使用 0.0.0.0,任意 ip 將可以連接serv_addr.sin_addr.s_addr = INADDR_ANY; }else{if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){perror(IP); // 轉換失敗exit(1);}}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);Bind(lfd, (struct sockaddr *)&serv_addr,sizeof(serv_addr));return lfd; } 與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的套接字错误处理函数的封装思想及函数实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用套接字实现 CS 模型
- 下一篇: TCP 三次握手 / 四次挥手