本节我们看一下怎样才能编写一个基于 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");
- }
- 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。但是这个时间间隔很长不能及时发现,虽然这个值可以改动,一般这种情况就需要心跳检测了,这就是心跳检测出现的原因。心跳就是对于长连接而言,客户端与服务器不断的进行少量的数据交互来保证彼此的存在。(如果这时开始发送数据那么就会不断重传 下述情形)
这里测试就是一个简单的 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 但是这些选项并不是指收到 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。
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。
- 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.<
由抓包结果看即使进程死了但是内核会回收资源告知已关闭。客户端或者服务器都可以当正常处理。
- 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 信号 这个信号默认情况下是终止进程,对于服务器来讲当然不希望这样,一个客户端异常关闭了服务器仍然坚持写的话就造成了服务器进程退出,在服务器程序中往往重写这个信号处理函数。服务器端也可能检测到这个文件描述符异常在将其关闭。
宗上的几种情况都是实际网络中容易发生的,其实无论是客户端和服务器都应该注意这些细节问题,当编写服务器或者客户端时考虑这些情形才会让程序更加健壮。尤其对于服务器开发而言,这些知识都十分重要。
来源: http://www.cnblogs.com/MaAce/p/8039119.html