(read/write、select、getsockopt、signal)实时判断socket连接状态/是否断开
為什么socket服務(wù)器斷開之后客戶端還能發(fā)送一次數(shù)據(jù)呢?
文章目錄
- 為什么socket服務(wù)器斷開之后客戶端還能發(fā)送一次數(shù)據(jù)呢?
 
- 一、了解背后的原因
- 1、客戶端是如何將數(shù)據(jù)發(fā)送給服務(wù)器端的?(服務(wù)器端發(fā)客戶端同理)
- 2、服務(wù)器端程序退出后,客戶端繼續(xù)發(fā)送數(shù)據(jù)會發(fā)生什么?
- 3、問題描述:
- 4、問題原因:
- 5、問題復(fù)現(xiàn):
 
- 二、解決辦法
- 1、如果只處理客戶端程序退出而不對發(fā)送失敗的數(shù)據(jù)作要求
- 2、如果既處理客戶端程序退出也要準(zhǔn)確判斷發(fā)送的數(shù)據(jù)是否失敗
- (1)getsockopt()函數(shù):獲取socket狀態(tài)
- (2) read()/write()和send()/recv()判斷
- (3)select、poll、epoll判斷
 
 
- 三、總結(jié)
最近遇到一個大坑:發(fā)現(xiàn)服務(wù)器端斷開連接時,客戶端還能write成功一次,然后客戶端程序退出了,不過服務(wù)器端是沒有收到的,而且我的服務(wù)器數(shù)據(jù)庫里面也沒有保存。最后一次能write成功估計是將數(shù)據(jù)寫入到緩沖區(qū)里面了,但是客戶端還沒確定服務(wù)器是否已經(jīng)斷開了連接,所以能write成功。因為項目對數(shù)據(jù)比較敏感,沒有成功發(fā)送給服務(wù)器端的數(shù)據(jù)需要保存在客戶端本地數(shù)據(jù)庫,等客戶端重新連接上服務(wù)器后再將該數(shù)據(jù)發(fā)送出去,后來我用了下面函數(shù)解決了問題,在write之前調(diào)用該函數(shù)即可:
//頭文件 #include <sys/types.h> #include <sys/socket.h> #include <libgen.h> #include <linux/tcp.h> #include <netinet/in.h> #include <netinet/ip.h>//函數(shù) 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、客戶端是如何將數(shù)據(jù)發(fā)送給服務(wù)器端的?(服務(wù)器端發(fā)客戶端同理)
當(dāng)調(diào)用send()或者write()發(fā)送數(shù)據(jù)時,并不是直接將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)中,而是先將待發(fā)送的數(shù)據(jù)放到socket發(fā)送緩沖區(qū)中,然后由TCP協(xié)議執(zhí)行數(shù)據(jù)的網(wǎng)絡(luò)發(fā)送。send()/write()函數(shù)不保證數(shù)據(jù)能夠通過網(wǎng)絡(luò)成功發(fā)送出去,只要能夠?qū)?shù)據(jù)寫入到socket發(fā)送緩沖區(qū),send()/write()就可以成功返回。
注意:1.一般來說,讀字符終端、網(wǎng)絡(luò)的socket描述字,管道文件等,這些文件的缺省read都是阻塞的方式。 2.如果是讀磁盤上的文件,一般不會是阻塞方式的。如果客戶端發(fā)送一個空的數(shù)據(jù)給服務(wù)器,服務(wù)器的read()將一直阻塞到客戶端發(fā)的數(shù)據(jù)不為空為止。同理如果服務(wù)器端發(fā)送一個空的數(shù)據(jù)給客戶端,客戶端的read()也將阻塞到服務(wù)器端發(fā)送的數(shù)據(jù)不為空為止。
某個客戶端和服務(wù)器端的正常連接狀態(tài)如下:
 
2、服務(wù)器端程序退出后,客戶端繼續(xù)發(fā)送數(shù)據(jù)會發(fā)生什么?
當(dāng)服務(wù)器socket突然關(guān)閉時,客戶端并不知情,此時客戶端調(diào)用send()/write()依然可以將數(shù)據(jù)放置到socket發(fā)送緩沖區(qū)中,所以send()/write()成功返回。
 
直到TCP協(xié)議執(zhí)行發(fā)送動作的時候才發(fā)現(xiàn)不對勁,這時客戶端才拿到服務(wù)器socket已關(guān)閉的信息,由于TCP協(xié)議無法繼續(xù)網(wǎng)絡(luò)數(shù)據(jù)的傳輸,所以系統(tǒng)自動關(guān)閉客戶端socket發(fā)送緩沖區(qū)的讀端。這時,再次調(diào)動send()/write()函數(shù),就會導(dǎo)致程序終止,原因是收到了SIGPIPE信號。
 
SIGPIPE信號產(chǎn)生的規(guī)則:當(dāng)一個進(jìn)程向某個已收到RST的套接字執(zhí)行寫操作時,內(nèi)核向該進(jìn)程發(fā)送SIGPIPE信號。
了解一下除了SIGPIPE信號之外還有哪些信號:
 
3、問題描述:
當(dāng)服務(wù)器close一個連接時,若client端進(jìn)程接著發(fā)數(shù)據(jù),根據(jù)TCP協(xié)議的規(guī)定,會收到一個RST響應(yīng),client再往這個服務(wù)器發(fā)送數(shù)據(jù)時,數(shù)據(jù)還是能存到緩沖區(qū),之后系統(tǒng)會發(fā)出一個SIGPIPE信號給該進(jìn)程,告訴進(jìn)程這個連接已經(jīng)斷開了,不要再寫了,如果client再發(fā)送數(shù)據(jù)到緩沖區(qū),是不能成功的。又或者當(dāng)一個進(jìn)程向某個已經(jīng)收到RST的socket執(zhí)行寫操作時,內(nèi)核向該進(jìn)程發(fā)送一個SIGPIPE信號。該信號的缺省處理是終止進(jìn)程,因此進(jìn)程必須捕獲它以免不情愿的被終止。
我遇到的情況是服務(wù)器socket句柄已關(guān)閉,然后客戶端向一個已關(guān)閉的服務(wù)端連接句柄中執(zhí)行寫操作,從而產(chǎn)生了SIGPIPE信號。
4、問題原因:
根據(jù)信號的默認(rèn)處理規(guī)則SIGPIPE信號的默認(rèn)執(zhí)行動作是terminate(終止、退出),所以進(jìn)程會退出。
系統(tǒng)里邊定義了三種處理方法:
 1)SIG_DFL //默認(rèn)處理信號
 2)SIG_IGN //忽略信號
 3)SIG_ERR //出錯
根據(jù)信號的默認(rèn)處理規(guī)則SIGPIPE信號的默認(rèn)執(zhí)行動作是terminate(終止、退出),所以進(jìn)程會退出。若不想客戶端退出,需要把 SIGPIPE默認(rèn)執(zhí)行動作屏蔽。
5、問題復(fù)現(xiàn):
到了這里,大概可以了解這其中的原因了,下面用一個客戶端和服務(wù)器端的代碼進(jìn)行測試,復(fù)現(xiàn)我遇到的問題,并進(jìn)行分析。
//客戶端程序: #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);//無論是端口還是地址,網(wǎng)絡(luò)字節(jié)序都是大端字節(jié)序,所以要進(jìn)行相應(yīng)的轉(zhuǎn)換。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;}//實時判斷標(biāo)準(zhǔn)輸入是否有數(shù)據(jù)輸入,不過需要注意的是fgets()會將回車符\n也讀到write_buf中while(fgets(write_buf,sizeof(write_buf), stdin) != NULL){write(sockfd, write_buf, sizeof(write_buf));//將標(biāo)準(zhǔn)輸入的數(shù)據(jù)發(fā)送給服務(wù)器端//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);//接收服務(wù)器發(fā)送過來的數(shù)據(jù)//printf("read_rv:%d errno:%s\n", read_rv, strerror(errno));if(read_rv<0){printf("read fail\n");}puts(read_buf);//將讀到的內(nèi)容存到數(shù)組里面}close(sockfd);return 0; } //服務(wù)器端 #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;//創(chuàng)建結(jié)構(gòu)體,記得使用memset清空內(nèi)存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()第三個參數(shù)是存放長度變量的地址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,確保每次讀取數(shù)據(jù)之前,buf是空的read_rv=read(connect_fd, buf, sizeof(buf));//接收客戶端發(fā)送過來的數(shù)據(jù),并存到數(shù)組buf中printf("read_rv:%d\n", read_rv);if(read_rv < 0){printf("read fail\n");}fputs(buf, stdout);//將buf的數(shù)據(jù)打印到標(biāo)準(zhǔn)輸出上write(connect_fd, buf, read_rv);//將buf發(fā)給客戶端printf("write_rv:%d\n\n\n", write_rv);}close(sockfd);return 0; }運行服務(wù)器端和客戶端程序:
 
 
 如果沒有在命令行上寫入任何字符,就回車發(fā)送,客戶端會將“\n”發(fā)送給服務(wù)器端。
二、解決辦法
1、如果只處理客戶端程序退出而不對發(fā)送失敗的數(shù)據(jù)作要求
可用signa()和sigaction()函數(shù)進(jìn)行信號注冊,對SIGPIPE進(jìn)行響應(yīng)處理。將SIGPIPE的默認(rèn)處理方法屏蔽,可用signal(SIGCHLD,SIG_IGN)或者重載其處理方法。兩者區(qū)別在于signal設(shè)置的信號句柄只能起一次作用,信號被捕獲一次后,信號句柄就會被還原成默認(rèn)值了;sigaction設(shè)置的信號句柄,可以一直有效,值到你再次改變它的設(shè)置。具體代碼如下:
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信號,并對該信號進(jìn)行忽略處理,就read不到服務(wù)端發(fā)送過來的數(shù)據(jù)了,read讀到的數(shù)據(jù)為0,但是服務(wù)器端顯示是正常發(fā)送的。
2、如果既處理客戶端程序退出也要準(zhǔn)確判斷發(fā)送的數(shù)據(jù)是否失敗
(1)getsockopt()函數(shù):獲取socket狀態(tài)
頭文件: #include <sys/types.h> #include <sys/socket.h>定義函數(shù):int getsockopt(int s, int level, int optname, void* optval, socklen_t* optlen);函數(shù)說明:getsockopt()會將參數(shù)s 所指定的socket 狀態(tài)返回. 參數(shù)optname 代表欲取得何種選項狀態(tài), 而參數(shù)optval 則指向欲保存結(jié)果的內(nèi)存地址, 參數(shù)optlen 則為該空間的大小. 參數(shù)level、optname 請參考setsockopt().
返回值:成功則返回0, 若有錯誤則返回-1, 錯誤原因存于errno
錯誤代碼:
 1、EBADF 參數(shù)s 并非合法的socket 處理代碼
 2、ENOTSOCK 參數(shù)s 為一文件描述詞, 非socket
 3、ENOPROTOOPT 參數(shù)optname 指定的選項不正確
 4、EFAULT 參數(shù)optval 指針指向無法存取的內(nèi)存空間
范例:
//頭文件 #include <sys/types.h> #include <sys/socket.h> #include <libgen.h> #include <linux/tcp.h> #include <netinet/in.h> #include <netinet/ip.h>//這定義了一個函數(shù)socketconnected(),里面調(diào)用了getsocket()函數(shù)進(jìn)行判斷客戶端和服務(wù)器端的連接狀態(tài), //如果正常連接則返回1,否則返回0;因此就可以解決服務(wù)器斷開,而客戶端還能write()/send()一次的問題。 int socketconnected(int sockfd) {struct tcp_info info;//其實我們就是使用到tcp_info結(jié)構(gòu)體的tcpi_state成員變量來存取socket的連接狀態(tài),int len = sizeof(info);//如果此返回值為1,則說明socket連接正常,如果返回值為0,則說明連接異常。//所以我們也可以直接用一個整形變量來存這個值,然后進(jìn)行判斷即可。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連接狀態(tài)時,會面臨一個問題就是,這幾個函數(shù)是阻塞的。
當(dāng)read()/recv()返回值小于等于0時,socket連接斷開。但是還需要判斷 errno是否等于 EINTR,如果errno == EINTR 則說明read()/recv()函數(shù)是由于程序接收到信號后返回的,socket連接還是正常的,不應(yīng)close掉socket連接。
ssize_t write(int fd, const void*buf,size_t nbytes)write函數(shù)將buf中的nbytes字節(jié)內(nèi)容寫入文件描述符fd,成功時返回寫的字節(jié)數(shù),失敗時返回-1,并設(shè)置errno變量,在網(wǎng)絡(luò)程序中,當(dāng)我們向套接字文件描述符寫時有兩可能.
1)write的返回值大于0,表示寫了部分或者是全部的數(shù)據(jù). 這樣我們用一個while循環(huán)來不停的寫入,但是循環(huán)過程中的buf參數(shù)和nbyte參數(shù)得由我們來更新。也就是說,網(wǎng)絡(luò)寫函數(shù)是不負(fù)責(zé)將全部數(shù)據(jù)寫完之后在返回的。
2)返回的值小于0,此時出現(xiàn)了錯誤.我們要根據(jù)錯誤類型來處理,如果錯誤為EINTR表示在寫的時候出現(xiàn)了中斷錯誤,如果為EPIPE表示網(wǎng)絡(luò)連接出現(xiàn)了問題(對方已經(jīng)關(guān)閉了連接),為了處理以上的情況,我們自己編寫一個寫函數(shù)來處理這幾種情況.
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); }讀函數(shù)read
ssize_t read(int fd,void *buf,size_t nbyte)read函數(shù)是負(fù)責(zé)從fd中讀取內(nèi)容.當(dāng)讀成功 時,read返回實際所讀的字節(jié)數(shù),如果返回的值是0 表示已經(jīng)讀到文件的結(jié)束了,小于0表示出現(xiàn)了錯誤.如果錯誤為EINTR說明讀是由中斷引起 的, 如果是ECONNREST表示網(wǎng)絡(luò)連接出了問題. 和上面一樣,我們也寫一個自己的讀函數(shù).
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判斷
這種方法用在服務(wù)端應(yīng)該是比較合理的,因為服務(wù)器需要連接多個客戶端的時候,使用多路復(fù)用就可以連接多個客戶端,但是將這個判斷方法用在客戶端比較麻煩。
 這里有關(guān)于select判斷socket是否斷開的博客,可參考:
https://blog.csdn.net/zzhongcy/article/details/21992123
三、總結(jié)
總的來說使用getsockopt()函數(shù)進(jìn)行判斷是最方便的,直接在write或read之前調(diào)用getsockopt()判斷就可以了。
此博客為本人學(xué)習(xí)筆記,如侵,刪。
參考鏈接:
 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
總結(jié)
以上是生活随笔為你收集整理的(read/write、select、getsockopt、signal)实时判断socket连接状态/是否断开的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: abaqus python_ABAQUS
- 下一篇: 大气湍流退化图像复原技术研究及DSP实现
