在TCP网络编程模型中,无论是客户端还是服务端,在网络编程的过程中都需要判断连接的对方网络状态是否正常。在linux系统中,有很多种方式可以判断连接的对方网络是否已经断开。
- 通过错误码和信号判断
- 通过select系统函数判断
- 通过TCP_INFO套接字选项判断
- 通过SO_KEEPALIVE套接字选项判断
- 通过SO_RCVTIMEO/SO_SNDTIMEO判断
(一)通过错误码和信号判断
(1)写数据信号和错误码判断
在写TCP连接数据的时候,如果对方连接已经正常断开,那么写数据端将会收到一个SIGPIPE信号,可以通过这个信号知道对方连接已经断开。该信号信号会终止当前进程,如果不在对方连接断开不退出进程,那么就应该注册信号函数。
同时,如果对方连接已经正常断开,那么write写数据端将会返回写错误。返回的写长度为-1,此时的错误码为:32,对应错误值为EPIPE;因此可以写数据时write的返回值和错误码来判断对方连接是否已经断开了。
(2)读数据判断返回值
如果当前是默认的阻塞模式读取,那么此时read读取返回的长度为0,错误码也是为0,其实表示读取成功。这里需要注意read 和recv接口的默认返回值是不一样的,使用recv接口也会返回EPIPE错误码。
client_tcp.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107/************************************************************ *Copyright (C),lcb0281at163.com lcb0281atgmail.com *FileName: 01_client_tcp.c *BlogAddr: caibiao-lee.blog.csdn.net *Description: TCP 客户端收发数据 *Date: 2020-01-04 *Author: Caibiao Lee *Version: V1.0 *Others: 通过read write 函数的返回值和错误码判断对方连接是否已经断开 *History: ***********************************************************/ #include <sys/uio.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <signal.h> #include <errno.h> #include <netinet/ip.h> #include <netinet/tcp.h> #define SERVER_IP_ADDR "192.168.1.111" #define PORT 8888 /* 侦听端口地址 */ void sig_proccess(int signo) { printf("Catch a exit signaln"); exit(0); } void sig_pipe(int sign) { printf("Catch a SIGPIPE signaln"); /* 释放资源 */ } 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: %sn",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); /* 建立一个流式套接字 */ l_s32SocketFd = socket(AF_INET, SOCK_STREAM, 0); if(l_s32SocketFd < 0) {/* 出错 */ printf("socket errorn"); return -1; } /* 设置服务器地址 */ bzero(&server_addr, sizeof(server_addr)); /* 清0 */ server_addr.sin_family = AF_INET; /* 协议族 */ server_addr.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);/*服务器IP地址*//* 本地地址 */ server_addr.sin_port = htons(PORT); /* 服务器端口 */ /* 连接服务器 */ connect(l_s32SocketFd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)); process_conn_client(l_s32SocketFd); /* 客户端处理过程 */ close(l_s32SocketFd); /* 关闭连接 */ return 0; }
server_tcp.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161/************************************************************ *Copyright (C),lcb0281at163.com lcb0281atgmail.com *FileName: 01_server_tcp.c *BlogAddr: caibiao-lee.blog.csdn.net *Description: TCP 客户端收发数据 *Date: 2020-01-04 *Author: Caibiao Lee *Version: V1.0 *Others: 通过read write 函数的返回值和错误码判断对方连接是否已经断开 *History: ***********************************************************/ #include <sys/uio.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <signal.h> #include <errno.h> #include <netinet/ip.h> #include <netinet/tcp.h> #define SERVER_IP_ADDR "192.168.1.111" #define PORT 8888 /* 侦听端口地址 */ #define BACKLOG 2 /* 侦听队列长度 */ void sig_proccess(int signo) { printf("Catch a exit signaln"); exit(0); } void sig_pipe(int sign) { printf("Catch a SIGPIPE signaln"); /* 释放资源 */ } /* 服务器对客户端的处理 */ void process_conn_server(int s32SocketFd) { int size = 0; char buffer[1024]; /* 数据的缓冲区 */ for(;;) { /* 从套接字中读取数据放到缓冲区buffer中 */ size = read(s32SocketFd, buffer, 1024); if(size==0) {/* 没有数据 */ 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)); /* 构建响应字符,为接收到客户端字节的数量 */ strcpy(buffer,"I am server"); size = write(s32SocketFd, buffer, strlen(buffer)+1);/* 发给客户端 */ if((strlen(buffer)+1)==size) { }else { printf("write data error size = %d, errno=%dn",size,errno); //return ; } sleep(1); } } int main(int argc, char *argv[]) { int l_s32ServerFd = -1; int l_s32ClientrFd = -1; struct sockaddr_in server_addr; /* 服务器地址结构 */ struct sockaddr_in client_addr; /* 客户端地址结构 */ int l_s32Ret = 0; /* 返回值 */ pid_t pid; /* 分叉的进行id */ signal(SIGINT, sig_proccess); signal(SIGPIPE, sig_pipe); /* 建立一个流式套接字 */ l_s32ServerFd = socket(AF_INET, SOCK_STREAM, 0); if(l_s32ServerFd < 0) {/* 出错 */ printf("socket errorn"); return -1; } /* 设置服务器地址 */ bzero(&server_addr, sizeof(server_addr)); /* 清0 */ server_addr.sin_family = AF_INET; /* 协议族 */ server_addr.sin_addr.s_addr = inet_addr(SERVER_IP_ADDR);/*服务器IP地址*/ server_addr.sin_port = htons(PORT); /* 服务器端口 */ /*设置IP地址可以重复绑定*/ int l_s32UseAddr = 1; if(setsockopt(l_s32ServerFd, SOL_SOCKET, SO_REUSEADDR, &l_s32UseAddr, sizeof(int)) < 0) { printf("%s %dtsetsockopt error! Error code: %d,Error message: %sn", __FUNCTION__, __LINE__, errno, strerror(errno)); return -2; } /* 绑定地址结构到套接字描述符 */ l_s32Ret = bind(l_s32ServerFd, (struct sockaddr*)&server_addr, sizeof(server_addr)); if(l_s32Ret < 0) {/* 出错 */ printf("bind errorn"); return -1; } /* 设置侦听 */ l_s32Ret = listen(l_s32ServerFd, BACKLOG); if(l_s32Ret < 0) {/* 出错 */ printf("listen errorn"); return -1; } /* 主循环过程 */ for(;;) { int addrlen = sizeof(struct sockaddr); /* 接收客户端连接 */ l_s32ClientrFd = accept(l_s32ServerFd, (struct sockaddr*)&client_addr, &addrlen); if(l_s32ClientrFd < 0) { /* 出错 */ continue; /* 结束本次循环 */ } /* 建立一个新的进程处理到来的连接 */ pid = fork(); /* 分叉进程 */ if( pid == 0 ) { /* 子进程中 */ close(l_s32ServerFd); /* 在子进程中关闭服务器的侦听 */ process_conn_server(l_s32ClientrFd);/* 处理连接 */ }else { close(l_s32ClientrFd); /* 在父进程中关闭客户端的连接 */ } } }
(二)通过select系统函数判断
select实际是IO复用的一个接口,它可以同时检测多个连接是否有数据可读写操作,并且可以设置检测的超时时间。
在点对点的连接中如果select超时,它返回值为0;
- 当出现异常的时候,返回-1,如果对方断开可能收到104的错误码,也就是ECONNRESET,表示连接被重置
- 当select返回1,表示正常,如果read此时返回的值为0,表示对方连接已经断开。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64/******************************************************** Function: process_conn_server Description: 服务器对客户端的处理 Input: s32SocketFd :服务端接收到客户端连接的ID; OutPut: none Return: 0: success,none 0:error Others: 通过select判断客户端的连接状态 Author: Caibiao Lee Date: 2020-01-04 *********************************************************/ void process_conn_server(int s32SocketFd) { int size = 0; int l_s32Ret = 0; char buffer[1024]; /* 数据的缓冲区 */ 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=%dn",l_s32Ret,errno); usleep(100000); } else if(FD_ISSET(s32SocketFd,&l_stReadfd)) { printf("l_s32Ret = %d n",l_s32Ret); /* 从套接字中读取数据放到缓冲区buffer中 */ size = read(s32SocketFd, buffer, 1024); if(size==0) {/* 没有数据 */ 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)); /* 构建响应字符,为接收到客户端字节的数量 */ strcpy(buffer,"I am server"); size = write(s32SocketFd, buffer, strlen(buffer)+1);/* 发给客户端 */ if((strlen(buffer)+1)==size) { }else { printf("write data error size = %d, errno=%dn",size,errno); //return ; } sleep(1); } }
(三)通过TCP_INFO套接字选项判断
通过getsockopt函数可以获取TCP连接的连接状态,当状态为ESTABLISHED的时候表示该连接正常。TCP的其它状态还有:
- CLOSED:表示初始状态。对服务端和C客户端双方都一样。
- LISTEN:表示监听状态。服务端调用了listen函数,可以开始accept连接了。
- SYN_SENT:表示客户端已经发送了SYN报文。当客户端调用connect函数发起连接时,首先发SYN给服务端,然后自己进入SYN_SENT状态,并等待服务端发送ACK+SYN。
- SYN_RCVD:表示服务端收到客户端发送SYN报文。服务端收到这个报文后,进入SYN_RCVD状态,然后发送ACK+SYN给客户端。
- ESTABLISHED:表示连接已经建立成功了。服务端发送完ACK+SYN后进入该状态,客户端收到ACK后也进入该状态。
- FIN_WAIT_1:表示主动关闭连接。无论哪方调用close函数发送FIN报文都会进入这个这个状态。
- FIN_WAIT_2:表示被动关闭方同意关闭连接。主动关闭连接方收到被动关闭方返回的ACK后,会进入该状态。
- TIME_WAIT:表示收到对方的FIN报文并发送了ACK报文,就等2MSL后即可回到CLOSED状态了。如果FIN_WAIT_1状态下,收到对方同时带FIN标志和ACK标志的报文时,可以直接进入TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
- CLOSING:表示双方同时关闭连接。如果双方几乎同时调用close函数,那么会出现双方同时发送FIN报文的情况,此时就会出现CLOSING状态,表示双方都在关闭连接。
- CLOSE_WAIT:表示被动关闭方等待关闭。当收到对方调用close函数发送的FIN报文时,回应对方ACK报文,此时进入CLOSE_WAIT状态。
- LAST_ACK:表示被动关闭方发送FIN报文后,等待对方的ACK报文状态,当收到ACK后进入CLOSED状态。
功能代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38/******************************************************** Function: check_tcp_alive Description: 通过TCP_INFO查询网络状态 Input: s32SocketFd :服务端接收到客户端连接的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 = %dn",info.tcpi_state); if(info.tcpi_state == TCP_ESTABLISHED) { printf("connect ok rn"); //return 0; } else { printf("connect errorrn"); //return -1; } } sleep(1); printf("nn"); } }
(四)通过SO_KEEPALIVE套接字选项判断
选项SO_KEEPALIVE用于设置TCP连接的保持,当设置此项后,连接会测试连接的状态。这个选项用于可能长时间没有数据交流的连接,通常在服务器端进行设置。
当设置SO_KEEPALIVE选项后,如果在两个小时内没有数据通信时,TCP会自动发送一个活动探测数据报文,对方必须对此进行响应,通常有如下3种情况。
- TCP的连接正常,发送一个ACK响应,这个过程应用层是不知道的。再过两个小时,又会再发送一个。
- 对方发送RST响应,对方在2个小时内进行了重启或者崩溃。之前的连接己经失效,套接字收到一个ECONNRESET错误,之前的套接字关闭。
- 如果对方没有任何响应,则本机会发送另外8个活动探测报文,时间的间隔为75s,当第一个活动报文发送11分15秒后仍然没有收到对方的任何响应,则放弃探测,套接字错误类型设置为ETIMEOUT,并关闭套接字连接。如果收到一个ICMP控制报文响应,此时套接字也关闭,这种情况通常收到的是一个主机不可达的ICMP报文,此时套接字错误类型设置为EHOSTUNREACH,并关闭套接字连接。
SO_KEEPALIVE的使用场景主要是在可能发送长时间无数据响应的TCP连接,例如Telnet会话,经常会出现打开一个telnet客户端后,长时间不用的情况,这需要服务器或 者客户端有一个探测机制知道对方是否仍然活动。根据探测结果服务器会释放己经失效的客户端,保证服务器资源的有效性,例如有的telnet客户端没有按照正常步骤进行关闭。
网上有不少资料介绍不推荐使用SO_KEEPALIVE来判断网络连接是否断开,具体原因没有去追踪,这里不再介绍它的使用。
(五)通过SO_RCVTIMEO/SO_SNDTIMEO判断
这个是通过套接字的SO_RCVTIMEO、SO_SNDTIMEO来设置收发数据超时。对于前面的前面的几种判断方式,都是基于对方正常网络断开后,主机才能够正常的判断到网络状态。如果连接的某一方突然断电,主机并不能知道对方设备突然断电,通过TCP_INFO查询到的也是网络正常,但实际情况是这是网络连接已经断开了。
这时,可以使用收发数据超时来判断:
如果设置的时间没有收到数据,read时会返回-1,同时有错误码EAGAIN产生,这时是可以判断出对连接已经断开了。
这种方式的确定就是,如果设定的一段时间没有收发数据,就会被判断为超时断开连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64/******************************************************** Function: process_conn_server Description: 通过设置收发操作判断对方连接已经断开了 Input: s32SocketFd :服务端接收到客户端连接的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]; /* 数据的缓冲区 */ int optlen = -1; /* 整型的选项类型值 */ int l_s32Ret = 0; /* 设置发送和接收超时时间 */ 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); /* 设置接收超时时间 */ if(l_s32Ret == -1){/* 设置接收超时时间失败 */ printf("设置接收超时时间失败n"); } l_s32Ret = setsockopt(s32SocketFd, SOL_SOCKET, SO_SNDTIMEO, &tv, optlen);/* 设置发送超时时间 */ if(l_s32Ret == -1){ printf("设置发送超时时间失败n"); } for(;;) { /* 从套接字中读取数据放到缓冲区buffer中 */ size = read(s32SocketFd, buffer, 1024); if(size==0) {/* 没有数据 */ 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)); /* 构建响应字符,为接收到客户端字节的数量 */ strcpy(buffer,"I am server"); size = write(s32SocketFd, buffer, strlen(buffer)+1);/* 发给客户端 */ if((strlen(buffer)+1)==size) { }else { printf("write data error size = %d, errno=%dn",size,errno); //return ; } sleep(1); } }
(六)自定义通信心跳判断
在一些比较重要的命令收发链接中,一般是客户端和服务端会建立心跳机制,心跳时间间隔根据不同的业务需求而不同。当约定的时间段内没有收到心跳数据包,就可以判断对方是否已经断开了连接。
这种方式非常简单,对于嵌入式设备而言,主要的缺点是心跳会耗费流量,同时会增加一点点系统负载,并且不适合并发连接的情况。
以上就是现在比较常用的判断网络连接的方法。 如有错误,欢迎指出!
----------------------------------------------------------------2022.08.28----------------------------------------------------------------
|公|_新的文章内容和附件工程文件
|众|_已更新在博客首页和:
|号|:liwen01
最后
以上就是矮小皮卡丘最近收集整理的关于linux网络编程学习笔记(6)——TCP连接状态的多种判断方法(一)通过错误码和信号判断(二)通过select系统函数判断(三)通过TCP_INFO套接字选项判断(四)通过SO_KEEPALIVE套接字选项判断(五)通过SO_RCVTIMEO/SO_SNDTIMEO判断(六)自定义通信心跳判断的全部内容,更多相关linux网络编程学习笔记(6)——TCP连接状态内容请搜索靠谱客的其他文章。
发表评论 取消回复