TCP連接狀態(tài)的多種判斷方法
掃描二維碼
隨時(shí)隨地手機(jī)看文章
liwen01_2020.01.10
-
前言
-
(一)通過錯(cuò)誤碼和信號(hào)判斷
-
(1)寫數(shù)據(jù)信號(hào)和錯(cuò)誤碼判斷
-
(2)讀數(shù)據(jù)判斷返回值
-
(二)通過select系統(tǒng)函數(shù)判斷
-
(三)通過TCP_INFO套接字選項(xiàng)判斷
-
(四)通過SO_KEEPALIVE套接字選項(xiàng)判斷
-
(五)通過SO_RCVTIMEO/SO_SNDTIMEO判斷
-
(六)自定義通信心跳判斷
前言
在TCP網(wǎng)絡(luò)編程模型中,無論是客戶端還是服務(wù)端,在網(wǎng)絡(luò)編程的過程中都需要判斷連接的對方網(wǎng)絡(luò)狀態(tài)是否正常。在linux系統(tǒng)中,有很多種方式可以判斷連接的對方網(wǎng)絡(luò)是否已經(jīng)斷開。
- 通過錯(cuò)誤碼和信號(hào)判斷
- 通過select系統(tǒng)函數(shù)判斷
- 通過TCP_INFO套接字選項(xiàng)判斷
- 通過SO_KEEPALIVE套接字選項(xiàng)判斷
- 通過SO_RCVTIMEO/SO_SNDTIMEO判斷
(一)通過錯(cuò)誤碼和信號(hào)判斷
(1)寫數(shù)據(jù)信號(hào)和錯(cuò)誤碼判斷
在寫TCP連接數(shù)據(jù)的時(shí)候,如果對方連接已經(jīng)正常斷開,那么寫數(shù)據(jù)端將會(huì)收到一個(gè)SIGPIPE信號(hào),可以通過這個(gè)信號(hào)知道對方連接已經(jīng)斷開。該信號(hào)信號(hào)會(huì)終止當(dāng)前進(jìn)程,如果不在對方連接斷開不退出進(jìn)程,那么就應(yīng)該注冊信號(hào)函數(shù)。
同時(shí),如果對方連接已經(jīng)正常斷開,那么write寫數(shù)據(jù)端將會(huì)返回寫錯(cuò)誤。返回的寫長度為-1,此時(shí)的錯(cuò)誤碼為:32,對應(yīng)錯(cuò)誤值為EPIPE;因此可以寫數(shù)據(jù)時(shí)write的返回值和錯(cuò)誤碼來判斷對方連接是否已經(jīng)斷開了。
(2)讀數(shù)據(jù)判斷返回值
如果當(dāng)前是默認(rèn)的阻塞模式讀取,那么此時(shí)read讀取返回的長度為0,錯(cuò)誤碼也是為0,其實(shí)表示讀取成功。這里需要注意read 和recv接口的默認(rèn)返回值是不一樣的,使用recv接口也會(huì)返回EPIPE錯(cuò)誤碼。client_tcp.c
/************************************************************ *Copyright (C),lcb0281at163.com lcb0281atgmail.com *FileName: 01_client_tcp.c *BlogAddr: caibiao-lee.blog.csdn.net *Description: TCP 客戶端收發(fā)數(shù)據(jù) *Date: 2020-01-04 *Author: Caibiao Lee *Version: V1.0 *Others: 通過read write 函數(shù)的返回值和錯(cuò)誤碼判斷對方連接是否已經(jīng)斷開 *History: ***********************************************************/ #include#include #include #include #include #include #include #include #include #include #include #include #include #include #define SERVER_IP_ADDR "192.168.1.111" #define PORT 8888 /* 偵聽端口地址 */ void sig_proccess(int signo) { printf("Catch a exit signal\n"); exit(0); } void sig_pipe(int sign) { printf("Catch a SIGPIPE signal\n"); /* 釋放資源 */ } void process_conn_client(int s32SocketFd) { int size = 0; char buffer[1024] = {0}; char *sendData = "I am client"; for(;;) { size = write(s32SocketFd, sendData, strlen(sendData)+1); if(size!=strlen(sendData)+1) { printf("write data error size=%d errno=%d \n",size,errno); //return ; } size = read(s32SocketFd, buffer, 1024); if(size<=0) { printf("read data error size=%d errno=%d \n",size,errno); //return ; }else { printf("recv Data: %s\n",buffer); } sleep(1); } } int main(int argc, char *argv[]) { struct sockaddr_in server_addr; int l_s32SocketFd = 0; signal(SIGINT, sig_proccess); signal(SIGPIPE, sig_pipe); /* 建立一個(gè)流式套接字 */ l_s32SocketFd = socket(AF_INET, SOCK_STREAM, 0); if(l_s32SocketFd < 0) {/* 出錯(cuò) */ printf("socket error\n"); return -1; } /* 設(shè)置服務(wù)器地址 */ bzero(&server_addr, sizeof(server_addr)); /* 清0 */ server_addr.sin_family = AF_INET; /* 協(xié)議族 */ server_addr.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);/*服務(wù)器IP地址*//* 本地地址 */ server_addr.sin_port = htons(PORT); /* 服務(wù)器端口 */ /* 連接服務(wù)器 */ connect(l_s32SocketFd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)); process_conn_client(l_s32SocketFd); /* 客戶端處理過程 */ close(l_s32SocketFd); /* 關(guān)閉連接 */ return 0; }
server_tcp.c
/************************************************************ *Copyright (C),lcb0281at163.com lcb0281atgmail.com *FileName: 01_server_tcp.c *BlogAddr: caibiao-lee.blog.csdn.net *Description: TCP 客戶端收發(fā)數(shù)據(jù) *Date: 2020-01-04 *Author: Caibiao Lee *Version: V1.0 *Others: 通過read write 函數(shù)的返回值和錯(cuò)誤碼判斷對方連接是否已經(jīng)斷開 *History: ***********************************************************/ #include#include #include #include #include #include #include #include #include #include #include #include #include #include #define SERVER_IP_ADDR "192.168.1.111" #define PORT 8888 /* 偵聽端口地址 */ #define BACKLOG 2 /* 偵聽隊(duì)列長度 */ void sig_proccess(int signo) { printf("Catch a exit signal\n"); exit(0); } void sig_pipe(int sign) { printf("Catch a SIGPIPE signal\n"); /* 釋放資源 */ } /* 服務(wù)器對客戶端的處理 */ void process_conn_server(int s32SocketFd) { int size = 0; char buffer[1024]; /* 數(shù)據(jù)的緩沖區(qū) */ for(;;) { /* 從套接字中讀取數(shù)據(jù)放到緩沖區(qū)buffer中 */ size = read(s32SocketFd, buffer, 1024); if(size==0) {/* 沒有數(shù)據(jù) */ printf("read size = %d, error %d \n",size,errno); //return; }else if(size<0) { printf("read size = %d, error %d \n",size,errno); //return ; }else { printf("recv data:%s \n",buffer); } memset(buffer,0,sizeof(buffer)); /* 構(gòu)建響應(yīng)字符,為接收到客戶端字節(jié)的數(shù)量 */ strcpy(buffer,"I am server"); size = write(s32SocketFd, buffer, strlen(buffer)+1);/* 發(fā)給客戶端 */ if((strlen(buffer)+1)==size) { }else { printf("write data error size = %d, errno=%d\n",size,errno); //return ; } sleep(1); } } int main(int argc, char *argv[]) { int l_s32ServerFd = -1; int l_s32ClientrFd = -1; struct sockaddr_in server_addr; /* 服務(wù)器地址結(jié)構(gòu) */ struct sockaddr_in client_addr; /* 客戶端地址結(jié)構(gòu) */ int l_s32Ret = 0; /* 返回值 */ pid_t pid; /* 分叉的進(jìn)行id */ signal(SIGINT, sig_proccess); signal(SIGPIPE, sig_pipe); /* 建立一個(gè)流式套接字 */ l_s32ServerFd = socket(AF_INET, SOCK_STREAM, 0); if(l_s32ServerFd < 0) {/* 出錯(cuò) */ printf("socket error\n"); return -1; } /* 設(shè)置服務(wù)器地址 */ bzero(&server_addr, sizeof(server_addr)); /* 清0 */ server_addr.sin_family = AF_INET; /* 協(xié)議族 */ server_addr.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);/*服務(wù)器IP地址*/ server_addr.sin_port = htons(PORT); /* 服務(wù)器端口 */ /*設(shè)置IP地址可以重復(fù)綁定*/ int l_s32UseAddr = 1; if(setsockopt(l_s32ServerFd, SOL_SOCKET, SO_REUSEADDR, &l_s32UseAddr, sizeof(int)) < 0) { printf("%s %d\tsetsockopt error! Error code: %d,Error message: %s\n", __FUNCTION__, __LINE__, errno, strerror(errno)); return -2; } /* 綁定地址結(jié)構(gòu)到套接字描述符 */ l_s32Ret = bind(l_s32ServerFd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(l_s32Ret < 0) {/* 出錯(cuò) */ printf("bind error\n"); return -1; } /* 設(shè)置偵聽 */ l_s32Ret = listen(l_s32ServerFd, BACKLOG); if(l_s32Ret < 0) {/* 出錯(cuò) */ printf("listen error\n"); return -1; } /* 主循環(huán)過程 */ for(;;) { int addrlen = sizeof(struct sockaddr); /* 接收客戶端連接 */ l_s32ClientrFd = accept(l_s32ServerFd, (struct sockaddr*)&client_addr, &addrlen); if(l_s32ClientrFd < 0) { /* 出錯(cuò) */ continue; /* 結(jié)束本次循環(huán) */ } /* 建立一個(gè)新的進(jìn)程處理到來的連接 */ pid = fork(); /* 分叉進(jìn)程 */ if( pid == 0 ) { /* 子進(jìn)程中 */ close(l_s32ServerFd); /* 在子進(jìn)程中關(guān)閉服務(wù)器的偵聽 */ process_conn_server(l_s32ClientrFd);/* 處理連接 */ }else { close(l_s32ClientrFd); /* 在父進(jìn)程中關(guān)閉客戶端的連接 */ } } }
(二)通過select系統(tǒng)函數(shù)判斷
select實(shí)際是IO復(fù)用的一個(gè)接口,它可以同時(shí)檢測多個(gè)連接是否有數(shù)據(jù)可讀寫操作,并且可以設(shè)置檢測的超時(shí)時(shí)間。 在點(diǎn)對點(diǎn)的連接中如果select超時(shí),它返回值為0;
- 當(dāng)出現(xiàn)異常的時(shí)候,返回-1,如果對方斷開可能收到104的錯(cuò)誤碼,也就是ECONNRESET,表示連接被重置
- 當(dāng)select返回1,表示正常,如果read此時(shí)返回的值為0,表示對方連接已經(jīng)斷開。
/******************************************************** Function: process_conn_server Description: 服務(wù)器對客戶端的處理 Input: s32SocketFd :服務(wù)端接收到客戶端連接的ID; OutPut: none Return: 0: success,none 0:error Others: 通過select判斷客戶端的連接狀態(tài) Author: Caibiao Lee Date: 2020-01-04 *********************************************************/ void process_conn_server(int s32SocketFd) { int size = 0; int l_s32Ret = 0; char buffer[1024]; /* 數(shù)據(jù)的緩沖區(qū) */ fd_set l_stReadfd; struct timeval l_stTimeout={0}; for(;;) { l_stTimeout.tv_sec=0; l_stTimeout.tv_usec=10000; FD_ZERO(&l_stReadfd); FD_SET(s32SocketFd ,&l_stReadfd); l_s32Ret = select(s32SocketFd+1, &l_stReadfd,NULL,NULL, &l_stTimeout); if (l_s32Ret<=0) { printf("select error l_s32Ret=%d errno=%d\n",l_s32Ret,errno); usleep(100000); } else if(FD_ISSET(s32SocketFd,&l_stReadfd)) { printf("l_s32Ret = %d \n",l_s32Ret); /* 從套接字中讀取數(shù)據(jù)放到緩沖區(qū)buffer中 */ size = read(s32SocketFd, buffer, 1024); if(size==0) {/* 沒有數(shù)據(jù) */ printf("read size = %d, error %d \n",size,errno); //return; }else if(size<0) { printf("read size = %d, error %d \n",size,errno); //return ; }else { printf("recv data:%s \n",buffer); } } memset(buffer,0,sizeof(buffer)); /* 構(gòu)建響應(yīng)字符,為接收到客戶端字節(jié)的數(shù)量 */ strcpy(buffer,"I am server"); size = write(s32SocketFd, buffer, strlen(buffer)+1);/* 發(fā)給客戶端 */ if((strlen(buffer)+1)==size) { }else { printf("write data error size = %d, errno=%d\n",size,errno); //return ; } sleep(1); } }
(三)通過TCP_INFO套接字選項(xiàng)判斷
通過getsockopt函數(shù)可以獲取TCP連接的連接狀態(tài),當(dāng)狀態(tài)為ESTABLISHED的時(shí)候表示該連接正常。TCP的其它狀態(tài)還有:
- CLOSED:表示初始狀態(tài)。對服務(wù)端和C客戶端雙方都一樣。
- LISTEN:表示監(jiān)聽狀態(tài)。服務(wù)端調(diào)用了listen函數(shù),可以開始accept連接了。
- SYN_SENT:表示客戶端已經(jīng)發(fā)送了SYN報(bào)文。當(dāng)客戶端調(diào)用connect函數(shù)發(fā)起連接時(shí),首先發(fā)SYN給服務(wù)端,然后自己進(jìn)入SYN_SENT狀態(tài),并等待服務(wù)端發(fā)送ACK+SYN。
- SYN_RCVD:表示服務(wù)端收到客戶端發(fā)送SYN報(bào)文。服務(wù)端收到這個(gè)報(bào)文后,進(jìn)入SYN_RCVD狀態(tài),然后發(fā)送ACK+SYN給客戶端。
- ESTABLISHED:表示連接已經(jīng)建立成功了。服務(wù)端發(fā)送完ACK+SYN后進(jìn)入該狀態(tài),客戶端收到ACK后也進(jìn)入該狀態(tài)。
- FIN_WAIT_1:表示主動(dòng)關(guān)閉連接。無論哪方調(diào)用close函數(shù)發(fā)送FIN報(bào)文都會(huì)進(jìn)入這個(gè)這個(gè)狀態(tài)。
- FIN_WAIT_2:表示被動(dòng)關(guān)閉方同意關(guān)閉連接。主動(dòng)關(guān)閉連接方收到被動(dòng)關(guān)閉方返回的ACK后,會(huì)進(jìn)入該狀態(tài)。
- TIME_WAIT:表示收到對方的FIN報(bào)文并發(fā)送了ACK報(bào)文,就等2MSL后即可回到CLOSED狀態(tài)了。如果FIN_WAIT_1狀態(tài)下,收到對方同時(shí)帶FIN標(biāo)志和ACK標(biāo)志的報(bào)文時(shí),可以直接進(jìn)入TIME_WAIT狀態(tài),而無須經(jīng)過FIN_WAIT_2狀態(tài)。
- CLOSING:表示雙方同時(shí)關(guān)閉連接。如果雙方幾乎同時(shí)調(diào)用close函數(shù),那么會(huì)出現(xiàn)雙方同時(shí)發(fā)送FIN報(bào)文的情況,此時(shí)就會(huì)出現(xiàn)CLOSING狀態(tài),表示雙方都在關(guān)閉連接。
- CLOSE_WAIT:表示被動(dòng)關(guān)閉方等待關(guān)閉。當(dāng)收到對方調(diào)用close函數(shù)發(fā)送的FIN報(bào)文時(shí),回應(yīng)對方ACK報(bào)文,此時(shí)進(jìn)入CLOSE_WAIT狀態(tài)。
- LAST_ACK:表示被動(dòng)關(guān)閉方發(fā)送FIN報(bào)文后,等待對方的ACK報(bào)文狀態(tài),當(dāng)收到ACK后進(jìn)入CLOSED狀態(tài)。 功能代碼如下:
/******************************************************** Function: check_tcp_alive Description: 通過TCP_INFO查詢網(wǎng)絡(luò)狀態(tài) Input: s32SocketFd :服務(wù)端接收到客戶端連接的ID; OutPut: none Return: 0: success,none 0:error Others: Author: Caibiao Lee Date: 2020-01-04 *********************************************************/ int check_tcp_alive(int s32SocketFd) { while(1) { printf("alive s32SocketFd = %d \n",s32SocketFd); if(s32SocketFd>0) { struct tcp_info info; int len = sizeof(info); getsockopt(s32SocketFd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len); printf("info.tcpi_state = %d\n",info.tcpi_state); if(info.tcpi_state == TCP_ESTABLISHED) { printf("connect ok \r\n"); //return 0; } else { printf("connect error\r\n"); //return -1; } } sleep(1); printf("\n\n"); } }
(四)通過SO_KEEPALIVE套接字選項(xiàng)判斷
選項(xiàng)SO_KEEPALIVE用于設(shè)置TCP連接的保持,當(dāng)設(shè)置此項(xiàng)后,連接會(huì)測試連接的狀態(tài)。這個(gè)選項(xiàng)用于可能長時(shí)間沒有數(shù)據(jù)交流的連接,通常在服務(wù)器端進(jìn)行設(shè)置。
當(dāng)設(shè)置SO_KEEPALIVE選項(xiàng)后,如果在兩個(gè)小時(shí)內(nèi)沒有數(shù)據(jù)通信時(shí),TCP會(huì)自動(dòng)發(fā)送一個(gè)活動(dòng)探測數(shù)據(jù)報(bào)文,對方必須對此進(jìn)行響應(yīng),通常有如下3種情況。
- TCP的連接正常,發(fā)送一個(gè)ACK響應(yīng),這個(gè)過程應(yīng)用層是不知道的。再過兩個(gè)小時(shí),又會(huì)再發(fā)送一個(gè)。
- 對方發(fā)送RST響應(yīng),對方在2個(gè)小時(shí)內(nèi)進(jìn)行了重啟或者崩潰。之前的連接己經(jīng)失效,套接字收到一個(gè)ECONNRESET錯(cuò)誤,之前的套接字關(guān)閉。
- 如果對方?jīng)]有任何響應(yīng),則本機(jī)會(huì)發(fā)送另外8個(gè)活動(dòng)探測報(bào)文,時(shí)間的間隔為75s,當(dāng)?shù)谝粋€(gè)活動(dòng)報(bào)文發(fā)送11分15秒后仍然沒有收到對方的任何響應(yīng),則放棄探測,套接字錯(cuò)誤類型設(shè)置為ETIMEOUT,并關(guān)閉套接字連接。如果收到一個(gè)ICMP控制報(bào)文響應(yīng),此時(shí)套接字也關(guān)閉,這種情況通常收到的是一個(gè)主機(jī)不可達(dá)的ICMP報(bào)文,此時(shí)套接字錯(cuò)誤類型設(shè)置為EHOSTUNREACH,并關(guān)閉套接字連接。SO_KEEPALIVE的使用場景主要是在可能發(fā)送長時(shí)間無數(shù)據(jù)響應(yīng)的TCP連接,例如Telnet會(huì)話,經(jīng)常會(huì)出現(xiàn)打開一個(gè)telnet客戶端后,長時(shí)間不用的情況,這需要服務(wù)器或 者客戶端有一個(gè)探測機(jī)制知道對方是否仍然活動(dòng)。根據(jù)探測結(jié)果服務(wù)器會(huì)釋放己經(jīng)失效的客戶端,保證服務(wù)器資源的有效性,例如有的telnet客戶端沒有按照正常步驟進(jìn)行關(guān)閉。
網(wǎng)上有不少資料介紹不推薦使用SO_KEEPALIVE來判斷網(wǎng)絡(luò)連接是否斷開,具體原因沒有去追蹤,這里不再介紹它的使用。
(五)通過SO_RCVTIMEO/SO_SNDTIMEO判斷
這個(gè)是通過套接字的SO_RCVTIMEO、SO_SNDTIMEO來設(shè)置收發(fā)數(shù)據(jù)超時(shí)。對于前面的前面的幾種判斷方式,都是基于對方正常網(wǎng)絡(luò)斷開后,主機(jī)才能夠正常的判斷到網(wǎng)絡(luò)狀態(tài)。如果連接的某一方突然斷電,主機(jī)并不能知道對方設(shè)備突然斷電,通過TCP_INFO查詢到的也是網(wǎng)絡(luò)正常,但實(shí)際情況是這是網(wǎng)絡(luò)連接已經(jīng)斷開了。
這時(shí),可以使用收發(fā)數(shù)據(jù)超時(shí)來判斷: 如果設(shè)置的時(shí)間沒有收到數(shù)據(jù),read時(shí)會(huì)返回-1,同時(shí)有錯(cuò)誤碼EAGAIN產(chǎn)生,這時(shí)是可以判斷出對連接已經(jīng)斷開了。 這種方式的確定就是,如果設(shè)定的一段時(shí)間沒有收發(fā)數(shù)據(jù),就會(huì)被判斷為超時(shí)斷開連接。
/******************************************************** Function: process_conn_server Description: 通過設(shè)置收發(fā)操作判斷對方連接已經(jīng)斷開了 Input: s32SocketFd :服務(wù)端接收到客戶端連接的ID; OutPut: none Return: 0: success,none 0:error Others: Author: Caibiao Lee Date: 2020-01-04 *********************************************************/ void process_conn_server(int s32SocketFd) { int size = 0; char buffer[1024]; /* 數(shù)據(jù)的緩沖區(qū) */ int optlen = -1; /* 整型的選項(xiàng)類型值 */ int l_s32Ret = 0; /* 設(shè)置發(fā)送和接收超時(shí)時(shí)間 */ struct timeval tv; tv.tv_sec = 10; /* 1秒 */ tv.tv_usec = 200000;/* 200ms */ optlen = sizeof(tv); l_s32Ret = setsockopt(s32SocketFd, SOL_SOCKET, SO_RCVTIMEO, &tv, optlen); /* 設(shè)置接收超時(shí)時(shí)間 */ if(l_s32Ret == -1){/* 設(shè)置接收超時(shí)時(shí)間失敗 */ printf("設(shè)置接收超時(shí)時(shí)間失敗\n"); } l_s32Ret = setsockopt(s32SocketFd, SOL_SOCKET, SO_SNDTIMEO, &tv, optlen);/* 設(shè)置發(fā)送超時(shí)時(shí)間 */ if(l_s32Ret == -1){ printf("設(shè)置發(fā)送超時(shí)時(shí)間失敗\n"); } for(;;) { /* 從套接字中讀取數(shù)據(jù)放到緩沖區(qū)buffer中 */ size = read(s32SocketFd, buffer, 1024); if(size==0) {/* 沒有數(shù)據(jù) */ printf("read size = %d, error %d \n",size,errno); //return; }else if(size<0) { printf("read size = %d, error %d \n",size,errno); //return ; }else { printf("recv data:%s \n",buffer); } memset(buffer,0,sizeof(buffer)); /* 構(gòu)建響應(yīng)字符,為接收到客戶端字節(jié)的數(shù)量 */ strcpy(buffer,"I am server"); size = write(s32SocketFd, buffer, strlen(buffer)+1);/* 發(fā)給客戶端 */ if((strlen(buffer)+1)==size) { }else { printf("write data error size = %d, errno=%d\n",size,errno); //return ; } sleep(1); } }
(六)自定義通信心跳判斷
在一些比較重要的命令收發(fā)鏈接中,一般是客戶端和服務(wù)端會(huì)建立心跳機(jī)制,心跳時(shí)間間隔根據(jù)不同的業(yè)務(wù)需求而不同。當(dāng)約定的時(shí)間段內(nèi)沒有收到心跳數(shù)據(jù)包,就可以判斷對方是否已經(jīng)斷開了連接。
這種方式非常簡單,對于嵌入式設(shè)備而言,主要的缺點(diǎn)是心跳會(huì)耗費(fèi)流量,同時(shí)會(huì)增加一點(diǎn)點(diǎn)系統(tǒng)負(fù)載,并且不適合并發(fā)連接的情況。





