(read/write、select、getsockopt、signal)实时判断socket连接状态/是否断开
為什么socket服務器斷開之后客戶端還能發送一次數據呢?
文章目錄
- 為什么socket服務器斷開之后客戶端還能發送一次數據呢?
- 一、了解背后的原因
- 1、客戶端是如何將數據發送給服務器端的?(服務器端發客戶端同理)
- 2、服務器端程序退出后,客戶端繼續發送數據會發生什么?
- 3、問題描述:
- 4、問題原因:
- 5、問題復現:
- 二、解決辦法
- 1、如果只處理客戶端程序退出而不對發送失敗的數據作要求
- 2、如果既處理客戶端程序退出也要準確判斷發送的數據是否失敗
- (1)getsockopt()函數:獲取socket狀態
- (2) read()/write()和send()/recv()判斷
- (3)select、poll、epoll判斷
- 三、總結
最近遇到一個大坑:發現服務器端斷開連接時,客戶端還能write成功一次,然后客戶端程序退出了,不過服務器端是沒有收到的,而且我的服務器數據庫里面也沒有保存。最后一次能write成功估計是將數據寫入到緩沖區里面了,但是客戶端還沒確定服務器是否已經斷開了連接,所以能write成功。因為項目對數據比較敏感,沒有成功發送給服務器端的數據需要保存在客戶端本地數據庫,等客戶端重新連接上服務器后再將該數據發送出去,后來我用了下面函數解決了問題,在write之前調用該函數即可:
//頭文件 #include <sys/types.h> #include <sys/socket.h> #include <libgen.h> #include <linux/tcp.h> #include <netinet/in.h> #include <netinet/ip.h>//函數 int SocketConnected(int sockfd) {struct tcp_info info;if (sockfd <= 0)return 0;int len = sizeof(info);getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *) & len);if ((info.tcpi_state == 1)) {printf("socket connected\n");return 1;} else {printf("socket disconnected\n");return 0;} }一、了解背后的原因
1、客戶端是如何將數據發送給服務器端的?(服務器端發客戶端同理)
當調用send()或者write()發送數據時,并不是直接將數據發送到網絡中,而是先將待發送的數據放到socket發送緩沖區中,然后由TCP協議執行數據的網絡發送。send()/write()函數不保證數據能夠通過網絡成功發送出去,只要能夠將數據寫入到socket發送緩沖區,send()/write()就可以成功返回。
注意:1.一般來說,讀字符終端、網絡的socket描述字,管道文件等,這些文件的缺省read都是阻塞的方式。 2.如果是讀磁盤上的文件,一般不會是阻塞方式的。如果客戶端發送一個空的數據給服務器,服務器的read()將一直阻塞到客戶端發的數據不為空為止。同理如果服務器端發送一個空的數據給客戶端,客戶端的read()也將阻塞到服務器端發送的數據不為空為止。
某個客戶端和服務器端的正常連接狀態如下:
2、服務器端程序退出后,客戶端繼續發送數據會發生什么?
當服務器socket突然關閉時,客戶端并不知情,此時客戶端調用send()/write()依然可以將數據放置到socket發送緩沖區中,所以send()/write()成功返回。
直到TCP協議執行發送動作的時候才發現不對勁,這時客戶端才拿到服務器socket已關閉的信息,由于TCP協議無法繼續網絡數據的傳輸,所以系統自動關閉客戶端socket發送緩沖區的讀端。這時,再次調動send()/write()函數,就會導致程序終止,原因是收到了SIGPIPE信號。
SIGPIPE信號產生的規則:當一個進程向某個已收到RST的套接字執行寫操作時,內核向該進程發送SIGPIPE信號。
了解一下除了SIGPIPE信號之外還有哪些信號:
3、問題描述:
當服務器close一個連接時,若client端進程接著發數據,根據TCP協議的規定,會收到一個RST響應,client再往這個服務器發送數據時,數據還是能存到緩沖區,之后系統會發出一個SIGPIPE信號給該進程,告訴進程這個連接已經斷開了,不要再寫了,如果client再發送數據到緩沖區,是不能成功的。又或者當一個進程向某個已經收到RST的socket執行寫操作時,內核向該進程發送一個SIGPIPE信號。該信號的缺省處理是終止進程,因此進程必須捕獲它以免不情愿的被終止。
我遇到的情況是服務器socket句柄已關閉,然后客戶端向一個已關閉的服務端連接句柄中執行寫操作,從而產生了SIGPIPE信號。
4、問題原因:
根據信號的默認處理規則SIGPIPE信號的默認執行動作是terminate(終止、退出),所以進程會退出。
系統里邊定義了三種處理方法:
1)SIG_DFL //默認處理信號
2)SIG_IGN //忽略信號
3)SIG_ERR //出錯
根據信號的默認處理規則SIGPIPE信號的默認執行動作是terminate(終止、退出),所以進程會退出。若不想客戶端退出,需要把 SIGPIPE默認執行動作屏蔽。
5、問題復現:
到了這里,大概可以了解這其中的原因了,下面用一個客戶端和服務器端的代碼進行測試,復現我遇到的問題,并進行分析。
//客戶端程序: #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <stdio.h> #include <arpa/inet.h> #include <string.h>int main(int argc, char *argv[]) {int sockfd;int connect_rv ;char write_buf[128] = {0};char read_buf[128] = {0};int read_rv;sockfd = socket(AF_INET, SOCK_STREAM,0);if(sockfd == -1){printf("create socket fail\n");return -1;}struct sockaddr_in server;memset(&server, 0, sizeof(struct sockaddr_in));server.sin_family = AF_INET;server.sin_port = htons(6666);//無論是端口還是地址,網絡字節序都是大端字節序,所以要進行相應的轉換。server.sin_addr.s_addr = inet_addr("127.0.0.1");connect_rv = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if(connect_rv == -1){perror("connect fail");return -2;}//實時判斷標準輸入是否有數據輸入,不過需要注意的是fgets()會將回車符\n也讀到write_buf中while(fgets(write_buf,sizeof(write_buf), stdin) != NULL){write(sockfd, write_buf, sizeof(write_buf));//將標準輸入的數據發送給服務器端//printf("write_rv:%d errno:%s\n", write_rv, strerror(errno));memset(read_buf, 0, sizeof(read_buf));read_rv = read(sockfd, read_buf, read_rv);//接收服務器發送過來的數據//printf("read_rv:%d errno:%s\n", read_rv, strerror(errno));if(read_rv<0){printf("read fail\n");}puts(read_buf);//將讀到的內容存到數組里面}close(sockfd);return 0; } //服務器端 #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h>int main() { int sockfd = -1; int bind_rv = -1; int listen_rv = -1; int connect_fd = -1; int read_rv = -1; char buf[128];sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd == -1){perror("create socket fail");return -1;}struct sockaddr_in server;//創建結構體,記得使用memset清空內存memset(&server, 0, sizeof(struct sockaddr_in));server.sin_family = AF_INET;server.sin_port = htons(6666);server.sin_addr.s_addr = htonl(INADDR_ANY);bind_rv= bind(sockfd, (const struct sockaddr *)&server, sizeof(server));if(sockfd == -1){perror("bind the IP and port fail");return -2;}listen_rv = listen(sockfd, SOMAXCONN);struct sockaddr_in client;socklen_t len = sizeof(client);//accept()第三個參數是存放長度變量的地址connect_fd = accept(sockfd, (struct sockaddr *)&client, &len);if(connect_fd == -1){perror("accept fail");return -3;}while(1){memset(buf,0,sizeof(buf));//使用memset,確保每次讀取數據之前,buf是空的read_rv=read(connect_fd, buf, sizeof(buf));//接收客戶端發送過來的數據,并存到數組buf中printf("read_rv:%d\n", read_rv);if(read_rv < 0){printf("read fail\n");}fputs(buf, stdout);//將buf的數據打印到標準輸出上write(connect_fd, buf, read_rv);//將buf發給客戶端printf("write_rv:%d\n\n\n", write_rv);}close(sockfd);return 0; }運行服務器端和客戶端程序:
如果沒有在命令行上寫入任何字符,就回車發送,客戶端會將“\n”發送給服務器端。
二、解決辦法
1、如果只處理客戶端程序退出而不對發送失敗的數據作要求
可用signa()和sigaction()函數進行信號注冊,對SIGPIPE進行響應處理。將SIGPIPE的默認處理方法屏蔽,可用signal(SIGCHLD,SIG_IGN)或者重載其處理方法。兩者區別在于signal設置的信號句柄只能起一次作用,信號被捕獲一次后,信號句柄就會被還原成默認值了;sigaction設置的信號句柄,可以一直有效,值到你再次改變它的設置。具體代碼如下:
struct sigaction action;action.sa_handler = handle_pipe;sigemptyset(&action.sa_mask);action.sa_flags = 0;sigaction(SIGPIPE, &action, NULL);void handle_pipe(int sig){//不做任何處理即可}在源文件中要添加signal.h頭文件:#include <signal.h>。 //頭文件 #include <signal.h> signal(SIGPIPE, handle_pipe);void handle_pipe(int sig) {if(sig == SIGPIPE){}//不做任何處理即可 }很奇怪的就是,我在使用這個方法的時候,我給客戶端安裝SIGPIPE信號,并對該信號進行忽略處理,就read不到服務端發送過來的數據了,read讀到的數據為0,但是服務器端顯示是正常發送的。
2、如果既處理客戶端程序退出也要準確判斷發送的數據是否失敗
(1)getsockopt()函數:獲取socket狀態
頭文件: #include <sys/types.h> #include <sys/socket.h>定義函數:int getsockopt(int s, int level, int optname, void* optval, socklen_t* optlen);函數說明:getsockopt()會將參數s 所指定的socket 狀態返回. 參數optname 代表欲取得何種選項狀態, 而參數optval 則指向欲保存結果的內存地址, 參數optlen 則為該空間的大小. 參數level、optname 請參考setsockopt().
返回值:成功則返回0, 若有錯誤則返回-1, 錯誤原因存于errno
錯誤代碼:
1、EBADF 參數s 并非合法的socket 處理代碼
2、ENOTSOCK 參數s 為一文件描述詞, 非socket
3、ENOPROTOOPT 參數optname 指定的選項不正確
4、EFAULT 參數optval 指針指向無法存取的內存空間
范例:
//頭文件 #include <sys/types.h> #include <sys/socket.h> #include <libgen.h> #include <linux/tcp.h> #include <netinet/in.h> #include <netinet/ip.h>//這定義了一個函數socketconnected(),里面調用了getsocket()函數進行判斷客戶端和服務器端的連接狀態, //如果正常連接則返回1,否則返回0;因此就可以解決服務器斷開,而客戶端還能write()/send()一次的問題。 int socketconnected(int sockfd) {struct tcp_info info;//其實我們就是使用到tcp_info結構體的tcpi_state成員變量來存取socket的連接狀態,int len = sizeof(info);//如果此返回值為1,則說明socket連接正常,如果返回值為0,則說明連接異常。//所以我們也可以直接用一個整形變量來存這個值,然后進行判斷即可。if (sockfd <= 0)return 0;getsockopt(sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *) & len);if((info.tcpi_state == 1)) {printf("socket connected\n");return 1;} else {printf("socket disconnected\n");return 0;} }(2) read()/write()和send()/recv()判斷
用 read()/write()和send()/recv()取判斷socket連接狀態時,會面臨一個問題就是,這幾個函數是阻塞的。
當read()/recv()返回值小于等于0時,socket連接斷開。但是還需要判斷 errno是否等于 EINTR,如果errno == EINTR 則說明read()/recv()函數是由于程序接收到信號后返回的,socket連接還是正常的,不應close掉socket連接。
ssize_t write(int fd, const void*buf,size_t nbytes)write函數將buf中的nbytes字節內容寫入文件描述符fd,成功時返回寫的字節數,失敗時返回-1,并設置errno變量,在網絡程序中,當我們向套接字文件描述符寫時有兩可能.
1)write的返回值大于0,表示寫了部分或者是全部的數據. 這樣我們用一個while循環來不停的寫入,但是循環過程中的buf參數和nbyte參數得由我們來更新。也就是說,網絡寫函數是不負責將全部數據寫完之后在返回的。
2)返回的值小于0,此時出現了錯誤.我們要根據錯誤類型來處理,如果錯誤為EINTR表示在寫的時候出現了中斷錯誤,如果為EPIPE表示網絡連接出現了問題(對方已經關閉了連接),為了處理以上的情況,我們自己編寫一個寫函數來處理這幾種情況.
int my_write(int fd,void *buffer,int length) { int bytes_left; int written_bytes; char *ptr;ptr=buffer; bytes_left=length; while(bytes_left>0) {written_bytes=write(fd,ptr,bytes_left);if(written_bytes<=0){ if(errno==EINTR)written_bytes=0;else return(-1);}bytes_left-=written_bytes;ptr+=written_bytes; } return(0); }讀函數read
ssize_t read(int fd,void *buf,size_t nbyte)read函數是負責從fd中讀取內容.當讀成功 時,read返回實際所讀的字節數,如果返回的值是0 表示已經讀到文件的結束了,小于0表示出現了錯誤.如果錯誤為EINTR說明讀是由中斷引起 的, 如果是ECONNREST表示網絡連接出了問題. 和上面一樣,我們也寫一個自己的讀函數.
int my_read(int fd,void *buffer,int length) { int bytes_left; int bytes_read; char *ptr; ptr = buffer;bytes_left=length; while(bytes_left>0) {bytes_read=read(fd,ptr,bytes_left);if(bytes_read<0){if(errno==EINTR)bytes_read=0;elsereturn(-1);}else if(bytes_read==0)break;bytes_left-=bytes_read;ptr+=bytes_read; } return(length-bytes_left); }(3)select、poll、epoll判斷
這種方法用在服務端應該是比較合理的,因為服務器需要連接多個客戶端的時候,使用多路復用就可以連接多個客戶端,但是將這個判斷方法用在客戶端比較麻煩。
這里有關于select判斷socket是否斷開的博客,可參考:
https://blog.csdn.net/zzhongcy/article/details/21992123
三、總結
總的來說使用getsockopt()函數進行判斷是最方便的,直接在write或read之前調用getsockopt()判斷就可以了。
此博客為本人學習筆記,如侵,刪。
參考鏈接:
http://c.biancheng.net/cpp/html/358.html
https://www.cnblogs.com/embedded-linux/p/7468442.html
https://blog.csdn.net/petershina/article/details/7946615
總結
以上是生活随笔為你收集整理的(read/write、select、getsockopt、signal)实时判断socket连接状态/是否断开的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: abaqus python_ABAQUS
- 下一篇: 大气湍流退化图像复原技术研究及DSP实现