如何编写一个稳定的网络程序(TCP)
本节我们看一下怎样才能编写一个基于TCP稳定的客户端或者服务器程序,主要以试验抓包的方式观察数据包的变化,对网络中出现的多种情况进行分析,分析网络程序中常用的技术及它们出现的原因,在之后的编程中能早一点意识到这些潜在问题。实例代码如下: client.c 和server.c 因在试验过程中代码有所改动,本实例代码仅仅是参考。
#include <sys/socket.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <fcntl.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <errno.h> #define PORT 6666 #define MAXSIZE 1024 void str_cli(FILE *, int); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "./client IP\n"); return 1; } int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); inet_pton(AF_INET, argv[1], &serveraddr.sin_addr); connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); //FILE * fp = fopen("./aa","r"); //str_cli(fp, fd); sleep(60); close(fd); exit(0); } void str_cli(FILE * fp, int fd) { char sendbuff[MAXSIZE], recvbuff[MAXSIZE]; bzero(sendbuff, MAXSIZE); bzero(recvbuff,MAXSIZE); while (fgets(sendbuff, MAXSIZE, fp) != NULL) { write(fd, sendbuff, strlen(sendbuff)); printf("hello\n"); /* if(read(fd, recvbuff, MAXSIZE) == 0) { if(errno == ECONNRESET) { fprintf(stderr, "reconnect\n"); } fprintf(stderr, "server terminated!\n"); exit(1); }*/ fputs(recvbuff, stdout); bzero(sendbuff, MAXSIZE); bzero(recvbuff,MAXSIZE); } }
#include <sys/socket.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #define PORT 6666 #define MAXSIZE 1024 void str_ser(int); int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serveraddr, clientaddr; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //inet_aton("15.15.182.182",&serveraddr.sin_addr); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); bind(fd,(const struct sockaddr *)&serveraddr, sizeof(serveraddr)); listen(fd, 5); socklen_t len = sizeof(clientaddr); for(;;) { // int clientfd = accept(fd, (struct sockaddr *)&clientaddr, &len); /* if(fork() == 0) { close(fd); str_ser(clientfd); exit(0); } */ // close(clientfd); sleep(3); } return 0; } void str_ser(int clientfd) { char buff[MAXSIZE]; size_t n; bzero(buff, MAXSIZE); sleep(2000); while( (n = read(clientfd, buff, MAXSIZE)) > 0) { write(clientfd, buff, n); bzero(buff, MAXSIZE); } if(n <= 0) fprintf(stderr, "read error\n"); }
客户端遇到情形如下:
客户端连接一个没有在监听的主机:
这里必须阐述一个事实,TCP的三次握手在listen之后就可以完成,可以将accept注释掉进行测试,listen系统调用之后会建立两个队列 listen 和accept ,listen队列就是当在三次握手过程中服务器收到了客户端发送的SYN时,就会将客户端结构放入listen队列,然后向客户端回应一个SYN+ACK, 当收到客户端最后的ACK之后,就会将这个客户端相关结构放入accept队列等待accept系统调用将它从队列中取出。
若主机没有监听,客户端直接链接的话:
CLIENT]# tcpdump -i eth0 -A -vvn tcp port 6666 and host 192.168.179.128 tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 13:40:52.874157 IP (tos 0x0, ttl 64, id 59618, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.129.21552 > 192.168.179.128.ircu-2: Flags [S], cksum 0xe881 (incorrect -> 0xffde), seq 1309016102, win 29200, options [mss 1460,sackOK,TS val 1217587396 ecr 0,nop,wscale 7], length 0 E..<..@.@.i.........T0. N..&......r............ H........... 13:40:52.874425 IP (tos 0x0, ttl 64, id 32912, offset 0, flags [DF], proto TCP (6), length 40) 192.168.179.128.ircu-2 > 192.168.179.129.21552: Flags [R.], cksum 0x0b16 (correct), seq 0, ack 1309016103, win 0, length 0 E..(..@.@............ T0....N..'P.............
我的目的主机是192.168.179.128, 当来自192.168.179.129的客户端发起连接请求时,由于服务器相应端口并没有在监听,所以在收到客户端的SYN之后,紧接着就由内核发送了一个RST连接复位发送给客户端。那么客户端是否有办法知道这种事实?答案是肯定的 看一下manpage对connect的描述:
ECONNREFUSED
No-one listening on the remote address.
当收到RST时, 客户端的connect调用会返回错误并将errno值为ECONNREFUSED,此时就可以判断处远程server并没有监听端口,此时客户端可以选择退出或者稍后重试等等。
客户端连接一个网络达不到的主机:
这里又可以分为两种情况: 1. 探测型的网络达不到ETIMEDOUT (如可能就是网络中的某个主机) 2. 已知型的网络达不到(如和客户端在同一局域网主机但是没有开机路由回馈ICMP) EHOSTUNREACH
这里随便找了一个ping长时间没有反馈的网络中的某个主机,用客户端程序连接它:
CLIENT]# tcpdump -i eth0 -A -vvn tcp port 6666 and host 122.155.44.22 tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 13:45:37.932632 IP (tos 0x0, ttl 64, id 60109, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.129.22126 > 122.155.44.22.ircu-2: Flags [S], cksum 0x1b0a (incorrect -> 0x7163), seq 2781849738, win 29200, options [mss 1460,sackOK,TS val 1217872455 ecr 0,nop,wscale 7], length 0 E..<..@.@.5.....z.,.Vn. ..........r.. ......... H.BG........ 13:45:38.934330 IP (tos 0x0, ttl 64, id 60110, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.129.22126 > 122.155.44.22.ircu-2: Flags [S], cksum 0x1b0a (incorrect -> 0x6d79), seq 2781849738, win 29200, options [mss 1460,sackOK,TS val 1217873457 ecr 0,nop,wscale 7], length 0 E..<..@.@.5.....z.,.Vn. ..........r.. ......... H.F1........ 13:45:40.941331 IP (tos 0x0, ttl 64, id 60111, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.129.22126 > 122.155.44.22.ircu-2: Flags [S], cksum 0x1b0a (incorrect -> 0x65a2), seq 2781849738, win 29200, options [mss 1460,sackOK,TS val 1217875464 ecr 0,nop,wscale 7], length 0 E..<..@.@.5.....z.,.Vn. ..........r.. ......... H.N......... 13:45:44.949506 IP (tos 0x0, ttl 64, id 60112, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.129.22126 > 122.155.44.22.ircu-2: Flags [S], cksum 0x1b0a (incorrect -> 0x55fa), seq 2781849738, win 29200, options [mss 1460,sackOK,TS val 1217879472 ecr 0,nop,wscale 7], length 0 E..<..@.@.5.....z.,.Vn. ..........r.. ......... H.].........
可以看到客户端会不断尝试与服务器握手,间隔时间并不确定, 但是它并不会一直这样下去直到connet放弃 connect就会返回ETIMEDOUT。这种形式的错误路由器并不返回ICMP错误。
情况2: 这种情况会很快知道并由路由器返回ICMP错误 告知主机不可达。接下来不会再发送SYN 尝试连接。 connect返回errno == EHOSTUNREACH.
对由这两种情况下errno都可以捕捉到,但是timeout发生的时间稍微有些长,这里可以将connect设为非阻塞,利用select探测描述符是否可读可写,再getsockopt得到相应结果:
SetNONBlock(); tval.tv_sec = timeout; tval.tv_usec = 100; fd_set wfd; FD_ZERO(&wfd); FD_SET(sockfd, &wfd); int resconn = connect(sockfd, (const sockaddr *)&seraddr, sizeof(seraddr)); if(resconn == 0) { write(1, "Connection success.\n",50); return true; } if (resconn == -1) { if(errno == EINPROGRESS) { int nready = select(sockfd+1, NULL, &wfd, NULL, &tval); if(nready == -1) { close(sockfd); HandleError("Select"); }else if(nready == 0) { write(2, "Connect Timeout!\n", 50); close(sockfd); exit(0); }else { int err; socklen_t len = sizeof(err); if(FD_ISSET(sockfd, &wfd)) { if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (void *)&err, &len) == -1) { close(sockfd); HandleError("Getsockopt"); } if(err == 0) { write(1, "Connect success.\n", 50); return true; }else{ close(sockfd); errno = err; HandleError("Connect"); }
客户端已经连接到服务器没有发送数据 这时网络突然断了(或server主机断电):
CLIENT]# tcpdump -i eth0 -A -vvn tcp port 6666 and host 192.168.179.128 tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 13:58:28.613040 IP (tos 0x0, ttl 64, id 25556, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.129.21556 > 192.168.179.128.ircu-2: Flags [S], cksum 0xe881 (incorrect -> 0x74b8), seq 1440245611, win 29200, options [mss 1460,sackOK,TS val 1218643135 ecr 0,nop,wscale 7], length 0 E..<c.@.@...........T4. U.gk......r............ H........... 13:58:28.613408 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) 192.168.179.128.ircu-2 > 192.168.179.129.21556: Flags [S.], cksum 0x87af (correct), seq 466362604, ack 1440245612, win 14480, options [mss 1460,sackOK,TS val 1763749 ecr 1218643135,nop,wscale 7], length 0 E..<..@.@.Ri......... T4.. .U.gl..8............ ....H.......
这里可以看到 client并不知道网络中出现异常,因为它并没有发送数据,server断开网络或者断电并没有跟客户端打招呼,这里TCP协议中的保活计时器就会发现这种情况, 套接字选项中的keeplive。但是这个时间间隔很长不能及时发现,虽然这个值可以改动,一般这种情况就需要心跳检测了,这就是心跳检测出现的原因。心跳就是对于长连接而言,客户端与服务器不断的进行少量的数据交互来保证彼此的存在。(如果这时开始发送数据那么就会不断重传 下述情形)
客户端已经连接到服务器并在发送东西,这时网络断了或者server主机突然断电:
这里测试就是一个简单的Echo模式:客户端发送字符串然后服务器接收后在发送回来 如下抓包结果:
14:13:14.139432 IP (tos 0x0, ttl 64, id 54328, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.128.ircu-2 > 192.168.179.129.21568: Flags [P.], cksum 0xd461 (correct), seq 141:151, ack 151, win 114, options [nop,nop,TS val 2649286 ecr 1219528661], length 10 E..>.8@.@.~.......... //服务器向客户端发送数据 T@..+........r.a..... .(l.H...sdfsdfsdf 14:13:14.139509 IP (tos 0x0, ttl 64, id 60816, offset 0, flags [DF], proto TCP (6), length 52) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [.], cksum 0xe879 (incorrect -> 0xeba2), seq 151, ack 151, win 229, options [nop,nop,TS val 1219528662 ecr 2649286], length 0 E..4..@.@.d.........T@. //客户端确认 ......+......y..... H....(l. 14:13:16.140328 IP (tos 0x0, ttl 64, id 60817, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xcc12), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219530663 ecr 2649286], length 10 E..>..@.@.d.........T@. //客户端再发送 这时服务器已经断开了网络 ......+............ H....(l.sdfsdfsdf 14:13:16.342204 IP (tos 0x0, ttl 64, id 60818, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xcb49), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219530864 ecr 2649286], length 10 E..>..@.@.d.........T@. //看它的序号 ......+............ H..p.(l.sdfsdfsdf 14:13:16.542401 IP (tos 0x0, ttl 64, id 60819, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xca80), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219531065 ecr 2649286], length 10 E..>..@.@.d.........T@. //序号不变 接下来都是不变的 ......+............ H..9.(l.sdfsdfsdf 14:13:16.945409 IP (tos 0x0, ttl 64, id 60820, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xc8ed), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219531468 ecr 2649286], length 10 E..>..@.@.d.........T@. ......+............ H....(l.sdfsdfsdf 14:13:17.751262 IP (tos 0x0, ttl 64, id 60821, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xc5c7), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219532274 ecr 2649286], length 10 E..>..@.@.d.........T@. ......+............ H....(l.sdfsdfsdf 14:13:19.365418 IP (tos 0x0, ttl 64, id 60822, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xbf79), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219533888 ecr 2649286], length 10 E..>..@.@.d.........T@. ......+............ H..@.(l.sdfsdfsdf 14:13:22.589323 IP (tos 0x0, ttl 64, id 60823, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.21568 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xb2e1), seq 151:161, ack 151, win 229, options [nop,nop,TS val 1219537112 ecr 2649286], length 10 E..>..@.@.d.........T@. ......+............ H....(l.sdfsdfsdf
这里分析结果可以看到,client后续一直再重传了,因为当server之间断开网络时,client再次发送数据 此时已经收不到server的ACK确认信息并且以后任何信息都接收不到了,client就反复的重传大约会坚持8-15min。当网络恢复正常时 又可以重新发送(TCP协议的实现)。 那么client怎么才能知道这件事呢?记得套接字选项里有sendtimeout 和recvtimeoutout 但是sendtimeout并不是指收到ACK的超时设定。那么一直收不到ACK造成重传的这个问题到底要怎么办呢?我这里试验方式如下:1. 将文件描述符改为非阻塞行不行? 2. 加上套接字选项send 超时行不行? 3. 这种情况一直send会造成EPIPE信号产生吗? 4. 什么时候才会由这个EPIPE的信号产生?
#include <sys/socket.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <fcntl.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <errno.h> #define PORT 6666 #define MAXSIZE 1024 void str_cli(FILE *, int); int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "./client IP\n"); return 1; } int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); inet_pton(AF_INET, argv[1], &serveraddr.sin_addr); struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 12; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); FILE * fp = fopen("./aa","r"); str_cli(fp, fd); sleep(60); close(fd); exit(0); } void str_cli(FILE * fp, int fd) { int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, O_NONBLOCK | flags); char sendbuff[MAXSIZE], recvbuff[MAXSIZE]; bzero(sendbuff, MAXSIZE); bzero(recvbuff,MAXSIZE); fgets(sendbuff, MAXSIZE, fp); while ( 1) { if(-1==send(fd, sendbuff, strlen(sendbuff), 0)) { fprintf(stderr, "%s\n", strerror(errno)); } fprintf(stderr, "p>>>>>\n"); if(read(fd, recvbuff, MAXSIZE) == 0) { if(errno == ECONNRESET) { fprintf(stderr, "reconnect\n"); } fprintf(stderr, "server terminated!\n"); exit(1); } fputs(recvbuff, stdout); // bzero(sendbuff, MAXSIZE); bzero(recvbuff,MAXSIZE); sleep(2); } }
#include <sys/socket.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <assert.h> #define PORT 6666 #define MAXSIZE 1024 void str_ser(int); int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serveraddr, clientaddr; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //inet_aton("15.15.182.182",&serveraddr.sin_addr); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); bind(fd,(const struct sockaddr *)&serveraddr, sizeof(serveraddr)); listen(fd, 5); socklen_t len = sizeof(clientaddr); for(;;) { int clientfd = accept(fd, (struct sockaddr *)&clientaddr, &len); str_ser(clientfd); close(clientfd); sleep(3); } return 0; } void str_ser(int clientfd) { char buff[MAXSIZE]; size_t n; bzero(buff, MAXSIZE); int num = 1; //sleep(2000); while( (n = read(clientfd, buff, MAXSIZE)) > 0) { num++; if (num == 5) { assert(num > 4); } write(clientfd, buff, n); bzero(buff, MAXSIZE); } if(n <= 0) fprintf(stderr, "read error\n"); }
1. 将文件描述符设为非阻塞.
在断开网络后,仍然可以send,一段时间内并不会返回-1,也不会造成epipe信号产生。直到tcp已经放弃重传后,send 返回-1,errno=EHOSTUNREACH 也可能是ETIMEDOUT。如果这里使用IO复用的话,返回可读时间read也会返回-1,errno相同。
2. 加上套接字选项sendtimeout
这种情况并不会发生send 超时的异常,这里套接字选项中的超时只是应对延迟发送的,并不能保证send在超时时间内收到来自对方的确认ACK。
3. 断开网络一直send不会产生EPIPE异常
那什么时候才会产生这个异常信号呢? 这里在发送过程中,将server进程杀掉注意这里目标主机还能达到只是server进程死了,让client并不处理server发来的FIN 或者RST复位 client仍然再次发送数据,这时client就会返回EPIPE异常。默认情况下进程会终止,服务器常常会发生这种情况可将此信号忽略。
试验结果就是即使改为非阻塞,send也并不会马上返回错误,仍然会继续往内核缓冲区写,因为没有收到对方的确认这时会重新发送一段时间,后续如果网络恢复那么就会继续正常发送。这个重传时间为8-15min。超过这个时间那么就会产生路由发送ICMP错误 告诉client已经路由不到目的主机了 client send就会返回-1 errno = EHOSTUNREACH网络不可达的错误(也可能没有收到ICMP,返回ETIMEDOUT)。(那么还有一种情况就是重传还没有超时间,但是内核的缓冲区被我们写满了,那么send也会返回-1, errno=ENOBUFS,manpage中有这个说法)。
对于这种情况无论是客户端还是服务端都会发生,对于服务端影响还是很大的,例如如果客户端断网服务器没有发现就一直保留相关client的数据结构并不断尝试重传。这种情形在真实的网络中常有发生,对于服务器对客户端发送数据时一直重传的话那么效率必定会下降。其中心跳可以让我们发现这个问题,例如我们规定心跳包发送2min内没有收到反馈,那么就认为是对方断网了,不必一直等待重传 可将它直接close 再删除相关数据结构。那么这里还有个细节就是close时 发送FIN可能也不会得到对方确认啊!? 抓包截这看: client与服务器断开网络之后 我在client又send多次数据之后close连接。
10:46:49.231527 IP (tos 0x0, ttl 64, id 31328, offset 0, flags [DF], proto TCP (6), length 62) 192.168.179.129.61420 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0xa82e), seq 71:81, ack 71, win 229, options [nop,nop,TS val 71250752 ecr 2765237], length 10 E..>z`@.@.............. ...Z...2........... 此处为重传 看这里还是那一个包 .?3@.*1.sdfsdfsdf 10:47:07.799865 IP (tos 0x0, ttl 64, id 31329, offset 0, flags [DF], proto TCP (6), length 272) 192.168.179.129.61420 > 192.168.179.128.ircu-2: Flags [FP.], cksum 0xe955 (incorrect -> 0x6d95), seq 81:301, ack 71, win 229, options [nop,nop,TS val 71269321 ecr 2765237], length 220 E...za@.@..3........... //这里close 连接 刚才数据还在一直send 所以会有很多的数据一次被发送出去 ...d...2.....U..... .?{..*1.sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf 10:47:15.151565 IP (tos 0x0, ttl 64, id 31330, offset 0, flags [DF], proto TCP (6), length 282) 192.168.179.129.61420 > 192.168.179.128.ircu-2: Flags [FP.], cksum 0xe95f (incorrect -> 0x3931), seq 71:301, ack 71, win 229, options [nop,nop,TS val 71276672 ecr 2765237], length 230 E...zb@.@..(........... //重传 ...Z...2....._..... .?...*1.sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf sdfsdfsdf
再看close时网络状态转换:
这里可以看到client发送一个FIN后会立即进入FIN-WAIT-1状态,当收到ACK之后就会进入FIN-WAIT-2状态。由抓包结果FIN发送之后并没有等到确认,那么还是会重传,再看网络状态:
CLIENT]$ netstat -an | grep 6666 tcp 0 231 192.168.179.129:61420 192.168.179.128:6666 FIN_WAIT1 unix 3 [ ] STREAM CONNECTED 26666 CLIENT]$ netstat -an | grep 6666 tcp 0 231 192.168.179.129:61420 192.168.179.128:6666 FIN_WAIT1 unix 3 [ ] STREAM CONNECTED 26666 CLIENT]$ netstat -an | grep 6666 tcp 0 231 192.168.179.129:61420 192.168.179.128:6666 FIN_WAIT1 unix 3 [ ] STREAM CONNECTED 26666
这里的FIN_WAIT1会维持一段时间,大约10秒左右就没有了,最后并不会进入TIME_WAIT状态,估计内核会把所有数据和状态清空。即使close掉还是造成重传,但是应用层数据已经清空了,剩下的只是内核在处理,但是如果你在close之后恰好网络又恢复了正常,最后的FIN和push都被收到的话,这种情况对端也会正常接收数据并发送FIN。
客户端已连接服务器但并没有在发送数据 server进程崩溃 server主机正常关机(LINUX):
192.168.179.129.21604 > 192.168.179.128.ircu-2: Flags [.], cksum 0xe879 (incorrect -> 0xcb3b), seq 1, ack 1, win 229, options [nop,nop,TS val 1221249014 ecr 4369660], length 0 E..4..@.@.J.........Td. .C.'/........y..... H....B.. 14:42:15.546958 IP (tos 0x0, ttl 64, id 44816, offset 0, flags [DF], proto TCP (6), length 52) 192.168.179.128.ircu-2 > 192.168.179.129.21604: Flags [F.], cksum 0x796d (correct), seq 1, ack 1, win 114, options [nop,nop,TS val 4390716 ecr 1221249014], length 0 E..4..@.@..`......... Td/....C.'...rym..... .B.<H... 14:42:15.547715 IP (tos 0x0, ttl 64, id 2005, offset 0, flags [DF], proto TCP (6), length 52) 192.168.179.129.21604 > 192.168.179.128.ircu-2: Flags [.], cksum 0xe879 (incorrect -> 0x26ba), seq 1, ack 2, win 229, options [nop,nop,TS val 1221270070 ecr 4390716], length 0 E..4..@.@.J.........Td. .C.'/........y..... H..6.B.<
由抓包结果看即使进程死了但是内核会回收资源告知已关闭。客户端或者服务器都可以当正常处理。
client正在发送数据 server进程崩溃server正常关机:
192.168.179.129.21580 > 192.168.179.128.ircu-2: Flags [P.], cksum 0xe883 (incorrect -> 0x2c64), seq 31:41, ack 31, win 229, options [nop,nop,TS val 1220079889 ecr 3198519], length 10 E..>..@.@.=.........TL. ...*r..C........... H....0.7sdfsdfsdf 14:22:25.423049 IP (tos 0x0, ttl 64, id 14522, offset 0, flags [DF], proto TCP (6), length 52) 192.168.179.128.ircu-2 > 192.168.179.129.21580: Flags [.], cksum 0x3c92 (correct), seq 31, ack 41, win 114, options [nop,nop,TS val 3200561 ecr 1220079889], length 0 E..48.@.@............ TLr..C...4...r<...... .0.1H... 14:22:25.744845 IP (tos 0x0, ttl 64, id 14523, offset 0, flags [DF], proto TCP (6), length 52) 192.168.179.128.ircu-2 > 192.168.179.129.21580: Flags [F.], cksum 0x3b3f (correct), seq 31, ack 41, win 114, options [nop,nop,TS val 3200899 ecr 1220079889], length 0 E..48.@.@............ TLr..C...4...r;?..... .0..H...
这时也会收到断开信息,但是如果接收端对这个断开忽略掉 仍坚持send的话 那么就会造成EPIPE信号 这个信号默认情况下是终止进程,对于服务器来讲当然不希望这样,一个客户端异常关闭了服务器仍然坚持写的话就造成了服务器进程退出,在服务器程序中往往重写这个信号处理函数。服务器端也可能检测到这个文件描述符异常在将其关闭。
总结:
宗上的几种情况都是实际网络中容易发生的,其实无论是客户端和服务器都应该注意这些细节问题,当编写服务器或者客户端时考虑这些情形才会让程序更加健壮。尤其对于服务器开发而言,这些知识都十分重要。