当前位置:   article > 正文

UNIX网络编程-TCP相关_当进程调用 accept时

当进程调用 accept时

目录

相关函数

套接字函数总结

服务端和客户端

调试程序

启动服务端后查看状态

建立连接后 kill客户端

建立连接后 kill服务端

异常退出

FIN_WAIT1状态

FIN_WAIT2和CLOSE_WAIT

FIN_WAIT2的另一种情况

处理僵尸进程

SIGPIPE信号

服务端崩溃

参考


相关函数

socket函数

  1. #include <sys/socket.h>
  2. //family:指定协议族
  3. //type:指定套接字类型
  4. //protocol:指定某个协议,设为0,以选择所给定family和type组合的系统默认值
  5. int socket(int family, int type, int protocol);

socket函数的family常数

family说明
AF_INETIPv4协议
AF_INET6IPv6协议
AF_LOCALUnix域协议
AF_ROUTE路由套接字
AF_KEY秘钥套接字

socket函数的type常量

type说明
SOCK_STREAM字节流套接字
SOCK_DGRAM数据报套接字
SOCK_SEQPACKET有序分组套接字
SOCK_RAW原始套接字

socket函数AF_INET或AF_INET6的protocol常量

protocol说明
IPPROTO_TCPTCP传输协议
IPPROTO_UDPUDP传输协议
IPPROTO_SCTPSCTP传输协议

socket函数中的family和type参数的组合

 AF_INETAF_INET6AF_LOCALAF_ROUTEAF_KEY
SOCK_STREAMTCP|SCTPTCP|SCTP  
SOCK_DGRAMUPDUPD  
SOCK_SEQPACKETSCTPSCTP  
SOCK_RAWIPv4IPv6 

 

connect函数

  1. #include <sys/socket.h>
  2. int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客户端调用connect时,将向服务器主动发起三路握手连接,直到连接建立和连接出错时才会返回,这里出错返回的可能有一下几种情况:

1)TCP客户没有收到SYN分节的响应。(内核发一个SYN若无响应则等待6s再发一个,若仍无响应则等待24s后再发送一个。总共等待75s仍未收到则返回错误ETIMEDOUT) 
2)若对客户的SYN的响应是RST,表明服务器主机在我们指定的端口上没有进程在等待与之连接,客户端收到RST就会返回ECONNREFUSED错误。 
产生RST的三个条件是:目的地SYN到达却没有监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在连接上的分节。 
3)若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错误,在某个规定时间(比如上述75s)没有收到回应,内核则会把保存的信息作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数,当循环调用函数connect为给定主机尝试各个ip地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并从新调用socket。

 

bind函数

  1. #include <sys/socket.h>
  2. //sockfd:套接字描述符
  3. //myaddr:套接字地址结构的指针 ,可以不指定
  4. //addrlen:上述结构的长度,防止内核越界,可以不指定
  5. int bind(int sockfd, const struct sockaddr * myaddr,socklen_t addrlen);

进程可以把一个特定的IP地址绑定到它的套接字上:对于客户端来说,这没有必要,因为内核将根据所外出网络接口来选择源IP地址。对于服务器来说,这将限定服务器只接收目的地为该IP地址的客户连接。

对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0,它告知内核去选择IP地址,因此我们经常看到如下语句:

  1. struct sockaddr_in servaddr;
  2. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

 

listen函数

  1. #include <sys/socket.h>
  2. int listen (int sockdfd , int backlog);

listen函数主要有两个作用:

1.对于参数sockfd来说:当socket函数创建一个套接字时,它被假设为一个主动套接字。listen函数把该套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。 
2.对于参数backlog:规定了内核应该为相应套接字排队的最大连接数=未完成连接队列+已完成连接队列 
其中: 
未完成连接队列:表示服务器接收到客户端的SYN但还未建立三路握手连接的套接字(SYN_RCVD状态) 
已完成连接队列:表示已完成三路握手连接过程的套接字(ESTABLISHED状态)

结合三路握手的过程: 
1.客户调用connect发送SYN分节 
2.服务器收到SYN分节在未完成队列建立条目 
3.直到三鹿握手的第三个分节(客户对服务器SYN的ACK)到达,此时该项目从未完成队列移动到已完成队列的队尾。 
4.当进程调用accept时,已完成队列出队,当已完成队列为空时,accept函数阻塞,进程睡眠,直到已完成队列入队。

所以说,如果三路握手正常完成,未完成连接队列中的任何一项在其中存留的时间就是服务器在收到客户端的SYN和收到客户端的ACK这段时间(RTT)

 

accpet函数

  1. #include<sys/socket.h>
  2. //sockfd:套接字描述符
  3. //cliaddr:对端(客户)的协议地址
  4. //addr:大小
  5. int accept (int sockfd, struct sockaddr *cliaddr ,socklen_t * addrlen);

当accept调用成功,将返回一个新的套接字描述符,例如:

int connfd = accept(listenfd, (struct sockaddr *)NULL, NULL);


其中我们称listenfd为监听套接字描述符,称connfd为已连接套接字描述符。,区分这两个套接字十分重要,一个服务器进程通常只需要一个监听套接字,但是却能够有很多已连接套接字(比如通过fork创建子进程),也就是说每有一个客户端建立连接时就会创建一个connectfd,当连接结束时,相应的已连接套接字就会被关闭。

 

其他函数

  1. #include <unisted.h>
  2. pid_t fork(void);
  3. int execl(const char *path, const char *arg, ...);
  4. int execlp(const char *file, const char *arg, ...);
  5. int execle(const char *path, const char *arg, ..., char *const envp[]);
  6. int execv(const char *path, char *const argv[]);
  7. int execvp(const char *file, char *const argv[]);
  8. int execve(const char *path, char *const argv[], char *const envp[]);
  9. int close(int sockfd);

getsockname和getpeername函数

  1. #include <sys/socket.h>
  2. int getsockname(int sockfd,struct sockaddr*localaddr,socklen_t *addrlen);
  3. int getpeername(int sockfd,struct sockaddr*peeraddr,socklen_t *addrlen);

这两个函数的作用: 
1.首先我们知道在TCP客户端一般不使用bind函数,当connect返回后,getsockname可以返回客户端本地IP地址和本地端口号。 
2.如果bind绑定了端口号0(内核选择),由于bind的参数是const型的,因此必须通过getsockname去得到内核赋予的本地端口号。 
3.获取某个套接字的地址族 
4.以通配IP地址bind的服务器上,accept成功返回之后,getsockname可以用于返回内核赋予该连接的本地IP地址。其中套接字描述符参数必须是已连接的套接字描述符。

 

 

套接字函数总结

TCP为监控套接字维护的两个队列

accept返回后客户/服务器的状态

fork返回后的客户/服务器状态

父子进程关闭相应套接字后 客户/服务器的状态

inetd派生服务器的状态

 

服务端和客户端

服务端代码如下

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/socket.h>
  5. #include <sys/types.h>
  6. #include <string.h>
  7. #include <netinet/in.h>
  8. int main(int argc, char *argv[]) {
  9. int listen_fd,conn_fd;
  10. listen_fd = socket(AF_INET, SOCK_STREAM,0);
  11. struct sockaddr_in server;
  12. struct sockaddr_in client;
  13. socklen_t child_len;
  14. char msg[100];
  15. memset(&server, 0, sizeof(struct sockaddr_in));
  16. server.sin_family = AF_INET;
  17. server.sin_addr.s_addr = htonl(INADDR_ANY);
  18. server.sin_port = htons(9527);
  19. bind(listen_fd, (struct sockaddr *)&server, sizeof(server));
  20. listen(listen_fd,1024);
  21. //sleep(100);
  22. stpcpy(msg, "hehe\n");
  23. int msg_len = strlen(msg);
  24. for(;;) {
  25. child_len = sizeof(client);
  26. conn_fd = accept(listen_fd, (struct sockaddr *)&client, &child_len);
  27. char tmp_client_addr[100];
  28. int tmp_client_port = ntohs(client.sin_port);
  29. inet_ntop(AF_INET, (void *)&client.sin_addr, tmp_client_addr, 100);
  30. //printf("sizeof->%d,content->%d\n", sizeof(client.sin_addr),client.sin_addr);
  31. printf("client addr->%s, port->%d\n", tmp_client_addr, tmp_client_port);
  32. pid_t pid = fork();
  33. if(pid > 0) {
  34. close(conn_fd);
  35. }
  36. else if(pid ==0){
  37. close(listen_fd);
  38. write(conn_fd, msg, msg_len);
  39. sleep(100);
  40. exit(0);
  41. }
  42. else {
  43. printf("fork error!\n");
  44. }
  45. //sleep(100);
  46. //close(conn_fd);
  47. }
  48. return 0;
  49. }

客户端代码如下

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/socket.h>
  5. #include <sys/types.h>
  6. #include <string.h>
  7. #include <netinet/in.h>
  8. #include <errno.h>
  9. #define MAX_LINE 100
  10. ssize_t readline(int fd, void *vptr, size_t maxlen) {
  11. ssize_t n,rc;
  12. char c, *ptr;
  13. ptr = vptr;
  14. for(n=1;n<maxlen;n++) {
  15. if( (rc=read(fd,&c,1)) ==1) {
  16. *ptr++ = c;
  17. if(c=='\n') {
  18. break;
  19. }
  20. }
  21. else if(rc == 0) {
  22. *ptr = 0;
  23. return (n-1);
  24. }
  25. else {
  26. if(rc < 0) {
  27. continue;
  28. }
  29. return -1;
  30. }
  31. }
  32. *ptr = 0;
  33. return n;
  34. }
  35. void client_str(FILE *file, int sock_fd) {
  36. char send_buf[MAX_LINE];
  37. char recv_buf[MAX_LINE];
  38. while(fgets(send_buf,MAX_LINE,file)!=NULL) {
  39. write(sock_fd,send_buf,strlen(send_buf));
  40. if(readline(sock_fd,recv_buf,MAX_LINE) == 0) {
  41. printf("client_str server terminated prematurely\n");
  42. exit(1);
  43. }
  44. fputs(recv_buf,stdout);
  45. }
  46. }
  47. int main(int argc, char *argv[]) {
  48. int sock_fd;
  49. struct sockaddr_in server_addr;
  50. if(argc != 3) {
  51. printf("input <IP> <port>\n");
  52. exit(0);
  53. }
  54. int port = atoi(argv[2]);
  55. sock_fd = socket(AF_INET,SOCK_STREAM,0);
  56. memset(&server_addr,0,sizeof(struct sockaddr_in));
  57. server_addr.sin_family = AF_INET;
  58. server_addr.sin_port = htons(port);
  59. inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
  60. connect(sock_fd,(struct sockaddr *)&server_addr, sizeof(server_addr));
  61. client_str(stdin,sock_fd);
  62. exit(0);
  63. }

 

调试程序

启动服务端后查看状态

  1. netstat -anpt | grep 4021
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 4021/./my_server
  3. ps -eo pid,ppid,tty,stat,args,wchan | grep 6794
  4. 6794 6486 pts/4 S+ ./my_server inet_csk_accept
  5. 6796 6794 pts/4 S+ ./my_server hrtimer_nanosleep
  6. telnet ip地址 9527
  7. client addr->1.2.3.196, port->58908
  8. client的telnet则打印
  9. hehe

strace服务器的启动,接收到一个连接的 系统函数调用过程

  1. 。。。。
  2. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  3. bind(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
  4. listen(3, 1024) = 0
  5. accept(3,
  6. {sa_family=AF_INET, sin_port=htons(59763), sin_addr=inet_addr("1.2.3.196")}, [16]) = 4
  7. fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
  8. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcad5ade000
  9. write(1, "client addr->【客户端地址】, por"..., 41client addr->【客户端地址】, port->59763
  10. ) = 41
  11. write(4, "hehe\n", 5) = 5
  12. close(4) = 0
  13. accept(3,

用hping3发一些SYN包过去,server端还跟两个client在交互,一瞬间的结果如下

  1. netstat -anpt | grep 9527
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 4187/./my_server
  3. tcp 0 0 my_server_ip:9527 client_ip_A:59851 TIME_WAIT -
  4. tcp 0 0 my_server_ip:9527 client_ip_A:59859 ESTABLISHED 4187/./my_server
  5. tcp 0 0 my_server_ip:9527 client_ip_B:1977 SYN_RECV -

 


建立连接后 kill客户端

server端启动,client端通过telnet建立连接,之后客户端kill进程

  1. netstat -anpt | grep 9527
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 29542/./my_server
  3. tcp 0 0 服务端:9527 客户端:58117 ESTABLISHED 31707/./my_server

tcpdump 服务端

  1. #连接创建
  2. 14:39:49.014872 IP 客户端.41850 > 服务端.9527: Flags [S], seq 4276092775, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 9], length 0
  3. 14:39:49.014899 IP 服务端 > 客户端.41850: Flags [S.], seq 2100291920, ack 4276092776, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 9], length 0
  4. 14:39:49.015327 IP 客户端.41850 > 服务端.9527: Flags [.], ack 1, win 29, length 0
  5. #发送数据
  6. 14:39:52.015914 IP 服务端.9527 > 客户端.41850: Flags [P.], seq 1:6, ack 1, win 29, length 5
  7. 14:39:52.016395 IP 服务端.41850 > 客户端.9527: Flags [.], ack 6, win 29, length 0
  8. #连接断开
  9. 14:40:13.854557 IP 客户端41850 > 服务端.9527: Flags [F.], seq 1, ack 6, win 29, length 0
  10. 14:40:13.855186 IP 服务端.9527 > 客户端.41850: Flags [.], ack 2, win 29, length 0
  11. 14:41:32.016266 IP 服务端.9527 > 客户端.41850: Flags [F.], seq 6, ack 2, win 29, length 0
  12. 14:41:32.016747 IP 客户端.41850 > 服务端.9527: Flags [R], seq 4276092777, win 0, length 0
  13. netstat -anpt | grep 9527
  14. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 29542/./my_server
  15. tcp 1 0 服务端:9527 客户端:41850 CLOSE_WAIT 29635/./my_server

tcpdump客户端

  1. #连接建立
  2. 14:39:49.025872 IP 客户端.41850 > 服务端.9527: Flags [S], seq 4276092775, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 9], length 0
  3. 14:39:49.026420 IP 服务端.9527 > 客户端.41850: Flags [S.], seq 2100291920, ack 4276092776, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 9], length 0
  4. 14:39:49.026433 IP 客户端.41850 > 服务端.9527: Flags [.], ack 1, win 29, length 0
  5. #接收数据
  6. 14:39:52.025965 IP 服务端.9527 > 客户端.41850: Flags [P.], seq 1:6, ack 1, win 29, length 5
  7. 14:39:52.025997 IP 客户端.41850 > 服务端.9527: Flags [.], ack 6, win 29, length 0
  8. #连接断开
  9. 14:40:13.856778 IP 客户端.41850 > 服务端.9527: Flags [F.], seq 1, ack 6, win 29, length 0
  10. 14:40:13.857761 IP 服务端.9527 > 客户端.41850: Flags [.], ack 2, win 29, length 0
  11. 14:41:32.000661 IP 服务端.9527 > 客户端.41850: Flags [F.], seq 6, ack 2, win 29, length 0
  12. 14:41:32.000683 IP 客户端.41850 > 服务端.9527: Flags [R], seq 4276092777, win 0, length 0

 

建立连接后 kill服务端

tcpdump服务端

  1. 15:06:14.195605 IP 服务端.9527 > 客户端.41908: Flags [F.], seq 6, ack 1, win 29, length 0
  2. 15:06:14.196223 IP 客户端.41908 > 服务端.9527: Flags [F.], seq 1, ack 7, win 29, length 0
  3. 15:06:14.196242 IP 服务端.9527 > 客户端.41908: Flags [.], ack 2, win 29, length 0

查看服务端口状态

  1. netstat -anpt | grep 9527
  2. tcp 0 0 10.104.109.241:9527 10.104.110.137:41908 TIME_WAIT -

tcpdump客户端

  1. 15:06:14.198239 IP 服务端.9527 > 客户端.41908: Flags [F.], seq 6, ack 1, win 29, length 0
  2. 15:06:14.198369 IP 客户端.41908 > 服务端.9527: Flags [F.], seq 1, ack 7, win 29, length 0
  3. 15:06:14.198832 IP 服务端.9527 > 客户端.41908: Flags [.], ack 2, win 29, length 0

 

 

异常退出

client 连接到 server,然后server端立刻退出程序,此时服务端处于 TIME_WAIT 状态
之后client 再去连接 server
此时会发现,client已经连不上了,服务端会返回RST复位的响应包,client之后会不断重试

  1. #查看服务器状态
  2. netstat -anpt | grep 9527
  3. tcp 0 0 172.17.6.131:9527 114.242.122.147:55610 TIME_WAIT -
  4. #tcpdump port 9527 结果
  5. 13:06:19.839944 IP 客户端.55460 > 服务端.9527: Flags [S], seq 369954675, win 8192, options [mss 1460,nop,wscale 2,nop,nop,sackOK], length 0
  6. 13:06:19.840023 IP 服务端.9527 > 客户端.55460: Flags [R.], seq 0, ack 369954676, win 0, length 0
  7. 13:06:20.346321 IP 客户端.55460 > 服务端.9527: Flags [S], seq 369954675, win 8192, options [mss 1460,nop,wscale 2,nop,nop,sackOK], length 0
  8. 13:06:20.346389 IP 服务端.9527 > 客户端.55460: Flags [R.], seq 0, ack 1, win 0, length 0
  9. 13:06:20.858551 IP 客户端.55460 > 服务端.9527: Flags [S], seq 369954675, win 8192, options [mss 1460,nop,nop,sackOK], length 0
  10. 13:06:20.858635 IP 服务端.9527 > 客户端.55460: Flags [R.], seq 0, ack 1, win 0, length 0

客户端连接到服务端后,突然关闭后
显示的连接关系

  1. netstat -anpt | grep 9527
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 5598/./my_server
  3. tcp 1 0 服务端:9527 客户端:54898 CLOSE_WAIT 5598/./my_server
  4. tcp 0 0 客户端:54898 服务端:9527 FIN_WAIT2 -

如果端口已经处于 TIME_WAIT状态,再启动服务端,是可以启动的
strace进程之后是报错了,但没有处理这个错误,进程继续执行,客户端telnet服务端后,就连不上
通过tcpdump看,直接返回给客户端一个RST的复位包

  1. netstat -anpt | grep 9527
  2. tcp 0 0 【服务端地址】:9527 【客户端地址】:54928 TIME_WAIT -
  3. strace ./my_server
  4. 。。。。
  5. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  6. bind(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)
  7. listen(3, 1024) = 0
  8. accept(3,
  9. #再次执行netstat,没有任何结果
  10. netstat -anpt | grep 9527
  11. tcpdump port 9527
  12. 16:33:52.685549 IP 客户端.54930 > 服务端.9527: Flags [S], seq 2034225034, win 29200, options [mss 1460,sackOK,TS val 88082989 ecr 0,nop,wscale 7], length 0
  13. 16:33:52.685577 IP 服务端.9527 > 客户端.54930: Flags [R.], seq 0, ack 2034225035, win 0, length 0

 

 

FIN_WAIT1状态

客户端程序连接到服务器后,用iptables禁止客户端的端口

iptables -A INPUT -p tcp --sport 54980 -j DROP

kill掉客户端程序,tcmdump的结果如下

  1. 22:00:23.391748 IP 客户端.54980 > 服务端.9527: Flags [R.], seq 1, ack 6, win 229, options [nop,nop,TS val 0 ecr 107640264], length 0
  2. #等待100秒后,服务端子进程结束,然后会发送FIN包
  3. 22:01:29.960293 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107740265 ecr 107640266], length 0
  4. 22:01:30.163273 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107740468 ecr 107640266], length 0
  5. 22:01:30.366275 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107740671 ecr 107640266], length 0
  6. 22:01:30.773272 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107741078 ecr 107640266], length 0
  7. 22:01:31.587260 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107741892 ecr 107640266], length 0
  8. 22:01:33.215307 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107743520 ecr 107640266], length 0
  9. 22:01:36.471262 IP 服务端.9527 > 客户端.54980: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 107746776 ecr 107640266], length 0

在看端口状态

  1. netstat -anpt | grep 9527
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 7245/./my_server
  3. tcp 0 1 【服务端】:9527 【客户端】:54980 FIN_WAIT1 -
  4. tcp 0 0 【服务端】:9527 【客户端】:56729 ESTABLISHED 7513/./my_server

此时服务端发送了一个FIN包,于是TCP状态就变成了 FIN_WAIT1
因为客户端早早的就被杀掉了,iptables把RST包也给挡住了,所以服务端子进程感知不到
于是过了一段时间服务端子进程继续发送FIN包,这里应该是有一个定时器,等待一段时间后看对方始终没有反应,于是FIN_WAIT1状态就直接退出
再netstat,就看不到FIN_WAIT1状态了

 

 

FIN_WAIT2和CLOSE_WAIT

当客户端连接到服务器后,kill telnet进程
此时服务端的的子进程是卡在了sleep()函数上了
根据TCP的状态机,telnet杀掉后会主动发送FIN,然后服务端会响应ACK
当客户端发送FIN会变成FIN_WAIT_1状态,收到服务端的ACK后变成FIN_WAIT_2状态
而服务端收到FIN后会变成CLOSE_WAIT,这时候服务端子进程就一直卡住,等100秒过去后,退出进程,然后发送FIN,变成LAST_ACK状态

  1. kill 客户端的telnet
  2. netstat -anpt | grep 9527
  3. tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      6356/./my_server    
  4. tcp        1      0 【服务端】:9527       【客户端】:54934        CLOSE_WAIT  6365/./my_server    
  5. tcp        0      0 【客户端】:54934      【服务端】:9527         FIN_WAIT2   -  
  6. 服务端的状态
  7. netstat -anpt | grep 9527
  8. tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      6356/./my_server    
  9. tcp        1      1 【服务端】:9527       【客户端】:54934        LAST_ACK    -   

tcpdump的结果

  1. tcpdump结果,客户端发送了FIN,服务端响应了ACK,所以服务端处于CLOSE_WAIT状态,客户端处于FIN_WAIT2状态
  2. 7:19:00.778371 IP 客户端.54934 > 服务端.9527: Flags [F.], seq 1, ack 6, win 229, options [nop,nop,TS val 90791083 ecr 90778352], length 0
  3. 17:19:00.780233 IP 服务端.9527 > 客户端.54934: Flags [.], ack 2, win 227, options [nop,nop,TS val 90791085 ecr 90791083], length 0
  4. 服务端sleep 100秒后,关闭连接,因为客户端已经不在了,所以发的FIN没有响应,此时服务端是LAST_ACK状态
  5. 17:20:28.048354 IP 服务端.9527 > 客户端.54934: Flags [F.], seq 6, ack 2, win 227, options [nop,nop,TS val 90878353 ecr 90791083], length 0
  6. 17:20:28.454273 IP 服务端.9527 > 客户端.54934: Flags [F.], seq 6, ack 2, win 227, options [nop,nop,TS val 90878759 ecr 90791083], length 0
  7. 17:20:29.675264 IP 服务端.9527 > 客户端.54934: Flags [F.], seq 6, ack 2, win 227, options [nop,nop,TS val 90879980 ecr 90791083], length 0
  8. 17:20:34.559281 IP 服务端.9527 > 客户端.54934: Flags [F.], seq 6, ack 2, win 227, options [nop,nop,TS val 90884864 ecr 90791083], length 0
  9. 17:20:54.079260 IP 服务端.9527 > 客户端.54934: Flags [F.], seq 6, ack 2, win 227, options [nop,nop,TS val 90904384 ecr 90791083], length 0

TCP状态机的关闭时序图如下

 

FIN_WAIT2的另一种情况

这个是由客户端引起的
客户端不用telnet,而是用程序启动的,等客户端连上服务端后,kill掉服务端子进程
这时候服务端子进程会发送一个FIN包给客户端,客户端响应ACK
于是服务端子进程就变成了FIN_WAIT_2状态,而客户端会卡在read()函数上等待终端输入,所以无法响应FIN,这个情况跟服务端sleep()一样,相当于上层应用在执行一些事情而卡主了

tcpdump port 9527

  1. 08:42:08.185089 IP 服务端.9527 > 客户端.55050: Flags [F.], seq 6, ack 1, win 227, options [nop,nop,TS val 146178489 ecr 146152169], length 0
  2. 08:42:08.225237 IP 客户端.55050 > 服务端.9527: Flags [.], ack 7, win 229, options [nop,nop,TS val 146178530 ecr 146178489], length 0

查看进程状态

  1. netstat -anpt | grep 9527
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 8395/./my_server
  3. tcp 0 0 客户端:9527 服务端:55050 FIN_WAIT2 -
  4. tcp 6 0 客户端:55050 服务端:9527 CLOSE_WAIT 8498/./client

strace客户端程序结果如下

  1. 。。。。
  2. munmap(0x7f801ed51000, 34375) = 0
  3. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  4. connect(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端IP")}, 16) = 0
  5. fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0
  6. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f801ed59000
  7. read(0,

在服务端子进程处于FIN_WAIT_2状态时,客户端等待终端输入,此时随便输入一些内容,让客户端拿到终端输入的数据再发送过去
因为服务端子进程已经关闭了,所以客户端发送数据之后,会收到对方的一个复位包

如果收到的RST包,在客户端调用read()之前,那么就会触发一个EOF
如果是客户端read()之后触发的RST包,则会收到一个ECONNRESET(connection reset by peer)对方复位连接错误
tcpdump可以看到客户端发送了数据,服务端响应了RST

  1. 09:10:57.927158 IP 客户端.55050 > 服务端.9527: Flags [P.], seq 1:5, ack 7, win 229, options [nop,nop,TS val 147908231 ecr 146178489], length 4
  2. 09:10:57.928398 IP 服务端.9527 > 客户端.55050: Flags [R], seq 3757393450, win 0, length 0

 

FIN_WAIT2还有可能是因为客户端/服务端调用了shutdown,主动关闭了读或者写,这时候也会变成FIN_WAIT_2状态

FIN_WAIT_2状态说明了一方是要主动关闭连接,进入到FIN_WAIT_2后,会启动一个定时器,观察对方是否也有FIN过来,如果长时间接收不到FIN,就关闭这个连接
另一方没有发送FIN,可能是因为正在处理某些事情,卡主了所以来不及响应
比如之前的服务端的sleep(),还有客户端的read(),都是导致他们无法影响的所以出现了这种情况
另一方会持续处于CLOSE_WAIT状态一段时间,等事情处理完了,就会影响FIN,然后进入LAST_ACK状态,可能对方的socket连接早就不在了,于是LAST_ACK之后,又等了一段时间(应该也是有一个定时器)超时,于是系统将连接关闭回收这个端口

在正常的服务器上,会有多路复用的,是可以接收到对方发送的FIN,也就不会出现上述这种情况了
 

 

处理僵尸进程

增加如下代码

  1. #include <signal.h>
  2. void handler_child(int sig_num) {
  3. pid_t pid;
  4. int status;
  5. while( (pid=waitpid(-1, &status, WNOHANG)) > 0 ) {
  6. printf("child %d terminated\n",pid);
  7. }
  8. //pid = wait(&status);
  9. //printf("child %d terminated\n",pid);
  10. return;
  11. }
  12. void install_singal() {
  13. struct sigaction act;
  14. struct sigaction o_act;
  15. act.sa_handler = handler_child;
  16. sigemptyset(&act.sa_mask);
  17. act.sa_flags = 0;
  18. //act.sa_flags = SA_RESTART;
  19. sigaction(SIGCHLD, &act, &o_act);
  20. }
  21. main() {
  22. bind(listen_fd, (struct sockaddr *)&server, sizeof(server));
  23. listen(listen_fd,1024);
  24. install_singal();
  25. //signal(SIGCHLD, handler_child);
  26. 。。。
  27. for(;;)
  28. 。。。
  29. }

启动服务端,然后客户端去连接服务端,之后会发现虽然父进程处理了子进程,但又重新执行了,导致又创建了一个进程,通过ps看,就是进程怎么也杀不掉,kill之后又出现一个新的,再杀掉又出现一个
具体原因可能是
act.sa_flags = 0;
这句话导致的

  1. ./my_server
  2. pid->10926, conn_fd->4
  3. client addr->【客户端】, port->55168
  4. child 10932 terminated
  5. pid->10926, conn_fd->-1
  6. client addr->【客户端】, port->55168
  7. child 10933 terminated
  8. pid->10926, conn_fd->-1
  9. client addr->【客户端】, port->55168
  10. child 10934 terminated
  11. #strace 进程,发现kill了之后又起来了一个新的
  12. rt_sigreturn({mask=[]}) = -1 EINTR (Interrupted system call)
  13. write(1, "client addr->【客户端】, port->5"..., 37client addr->【客户端】, port->55166
  14. ) = 37
  15. clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb1f7e69a10) = 10851
  16. close(-1) = -1 EBADF (Bad file descriptor)
  17. accept(3, strace: Process 10851 attached
  18. <unfinished ...>
  19. [pid 10851] close(3) = 0
  20. [pid 10851] write(-1, "hehe\n", 5) = -1 EBADF (Bad file descriptor)
  21. [pid 10851] exit_group(0) = ?
  22. [pid 10851] +++ exited with 0 +++
  23. <... accept resumed> 0x7ffcd2229d40, 0x7ffcd2229d3c) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
  24. --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=10851, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
  25. wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 10851
  26. write(1, "child 10851 terminated\n", 23child 10851 terminated
  27. ) = 23
  28. rt_sigreturn({mask=[]}) = -1 EINTR (Interrupted system call)
  29. write(1, "client addr->【客户端】, port->5"..., 37client addr->【客户端】, port->55166
  30. ) = 37
  31. clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb1f7e69a10) = 10852
  32. close(-1) = -1 EBADF (Bad file descriptor)
  33. accept(3, strace: Process 10852 attached
  34. <unfinished ...>
  35. [pid 10852] close(3) = 0
  36. [pid 10852] write(-1, "hehe\n", 5) = -1 EBADF (Bad file descriptor)
  37. [pid 10852] exit_group(0) = ?
  38. [pid 10852] +++ exited with 0 +++
  39. <... accept resumed> 0x7ffcd2229d40, 0x7ffcd2229d3c) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)

修改代码,将再运行一遍程序

  1. act.sa_flags = 0;
  2. //act.sa_flags = SA_RESTART;
  3. 改为
  4. //act.sa_flags = 0;
  5. act.sa_flags = SA_RESTART;

再次执行后的结果如下,可以看到,这次父进程是正确处理了僵尸进程

  1. ./my_server
  2. pid->13947, conn_fd->4
  3. client addr->【客户端】, port->55184
  4. child 13954 terminated
  5. pid->13947, conn_fd->4
  6. client addr->【客户端】, port->55186
  7. child 13960 terminated
  8. pid->13947, conn_fd->4
  9. client addr->【客户端】, port->55188
  10. child 13962 terminated
  11. pid->13947, conn_fd->4
  12. client addr->【客户端】, port->55190
  13. child 13964 terminated
  14. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  15. bind(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
  16. listen(3, 1024) = 0
  17. rt_sigaction(SIGCHLD, {0x400b12, [], SA_RESTORER|SA_RESTART, 0x7fafb4aaa280}, {SIG_DFL, [], 0}, 8) = 0
  18. accept(3,
  19. {sa_family=AF_INET, sin_port=htons(55196), sin_addr=inet_addr("47.93.18.8")}, [16]) = 4
  20. getpid() = 14006
  21. fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 6), ...}) = 0
  22. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fafb5060000
  23. write(1, "pid->14006, conn_fd->4\n", 23pid->14006, conn_fd->4
  24. ) = 23
  25. write(1, "client addr->【客户端】, port->5"..., 37client addr->【客户端】, port->55196
  26. ) = 37
  27. clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fafb5055a10) = 14008
  28. close(4) = 0
  29. accept(3, strace: Process 14008 attached
  30. <unfinished ...>
  31. [pid 14008] close(3) = 0
  32. [pid 14008] write(4, "hehe\n", 5) = 5
  33. [pid 14008] exit_group(0) = ?
  34. [pid 14008] +++ exited with 0 +++
  35. <... accept resumed> 0x7ffec87a2930, 0x7ffec87a292c) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
  36. --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=14008, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
  37. wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 14008
  38. write(1, "child 14008 terminated\n", 23child 14008 terminated
  39. ) = 23
  40. rt_sigreturn({mask=[]}) = 43
  41. accept(3,

模拟连续创建5个连接,修改客户端代码

  1. 。。。
  2. int fd[5];
  3. for(;i<5;i++) {
  4. fd[i] = socket(AF_INET,SOCK_STREAM,0);
  5. memset(&server_addr,0,sizeof(struct sockaddr_in));
  6. server_addr.sin_family = AF_INET;
  7. server_addr.sin_port = htons(port);
  8. inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
  9. connect(fd[i],(struct sockaddr *)&server_addr, sizeof(server_addr));
  10. //client_str(stdin,fd[i]);
  11. //close(fd[i]);
  12. }
  13. sleep(10);
  14. exit(0);
  15. 。。。

通过strace看,确实是创建了5次连接,再通过netstat和ps看,僵尸进程都已经正确处理了

  1. netstat -anpt | grep 9527
  2. tcp 0 0 0.0.0.0:9527 0.0.0.0:* LISTEN 2612/./my_server
  3. tcp 6 0 客户端:54224 服务端:9527 CLOSE_WAIT 2626/./client
  4. tcp 6 0 客户端:54226 服务端:9527 CLOSE_WAIT 2626/./client
  5. tcp 6 0 客户端:54230 服务端:9527 CLOSE_WAIT 2626/./client
  6. tcp 0 0 服务端:9527 客户端:54222 FIN_WAIT2 -
  7. tcp 6 0 客户端:54222 服务端:9527 CLOSE_WAIT 2626/./client
  8. tcp 0 0 服务端:9527 客户端:54230 FIN_WAIT2 -
  9. tcp 6 0 客户端:54228 服务端:9527 CLOSE_WAIT 2626/./client
  10. tcp 0 0 服务端:9527 客户端:54228 FIN_WAIT2 -
  11. tcp 0 0 服务端:9527 客户端:54224 FIN_WAIT2 -
  12. tcp 0 0 服务端:9527 客户端:54226 FIN_WAIT2 -
  13. ps aux | grep my_
  14. root 2612 0.0 0.1 4224 1412 pts/0 S+ 11:48 0:00 ./my_server
  15. root 2635 0.0 0.2 112720 2220 pts/3 S+ 11:49 0:00 grep --color=auto my_
  16. strace客户端
  17. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  18. connect(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
  19. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
  20. connect(4, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
  21. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 5
  22. connect(5, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
  23. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
  24. connect(6, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
  25. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 7
  26. connect(7, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0

 

 

SIGPIPE信号

客户端连上服务端,之后服务端就退出了,客户端继续卡在read()终端输入上
客户端发送了数据包过去后,因为服务端已经关闭了,所以会返回一个RST包
之后客户端再次发送一个数据包过去,这时候服务端不再响应了
通过strace可以看到,客户端进程直接触发了 SIGPIPE 信号,于是客户端进程退出

  1. # 客户端连上服务端之后,服务端就退出了,所以发送了FIN包
  2. 13:05:45.489525 IP 服务端.9527 > 客户端.54236: Flags [F.], seq 6, ack 1, win 510, options [nop,nop,TS val 2787010343 ecr 2787010342], length 0
  3. 13:05:45.490602 IP 客户端.54236 > 服务端.9527: Flags [.], ack 6, win 502, options [nop,nop,TS val 2787010344 ecr 2787010343], length 0
  4. # 客户端继续写入数据,因为服务端已经退出了,所以响应了RST包
  5. 13:09:31.230884 IP 客户端.54236 > 服务端.9527: Flags [P.], seq 1:2, ack 7, win 502, options [nop,nop,TS val 2787236086 ecr 2787010343], length 1
  6. 13:09:31.232153 IP 服务端.9527 > 客户端.54236: Flags [R], seq 723649255, win 0, length 0

通过strace客户端结果如下

  1. socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
  2. connect(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
  3. fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
  4. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5482496000
  5. read(0,
  6. "\n", 1024) = 1
  7. write(3, "\n", 1) = 1
  8. read(3, "h", 1) = 1
  9. read(3, "e", 1) = 1
  10. read(3, "h", 1) = 1
  11. read(3, "e", 1) = 1
  12. read(3, "\n", 1) = 1
  13. fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
  14. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5482495000
  15. write(1, "hehe\n", 5hehe
  16. ) = 5
  17. read(0,
  18. "\n", 1024) = 1
  19. write(3, "\n", 1) = -1 EPIPE (Broken pipe)
  20. --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=2755, si_uid=0} ---
  21. +++ killed by SIGPIPE +++

 

服务端崩溃

服务端崩溃后,假设中间路由没有感知到服务端发送的FIN,或者FIN包丢失了,其结果就是客户端发送了数据之后,等不到对方的ACK,于是会持续重传,当持续一段时间后客户端放弃重传,然后连接断开
中间路由会反应一个ICMP的错误消息, EHOSTUNREACH 或 ENETUNREACH 目的地不可达的错误

如果服务器重启,中间路由没有感知到,客户端继续给服务器发送数据,服务器会返回一个RST复位包
当客户端收到RST后,会返回 ECONNRESET的错误,connection reset by peer 对方连接复位错误

对于客户端来说,如果不想发送数据也能检测出服务端是否存活,就需要SO_KEEPALIVE套接字选项,或者一些内置的心跳技术

如果服务端执行关机操作,init进程会给所有进程发送 SIGTERM信号(这个信号可以被捕获),等待5-20秒后,给仍然在运行的所有进程发送 SIGKILL信号
 

 

 

参考

UNIX网络编程笔记(3)—基本TCP套接字编程

ip(7) - Linux man page

INET_NTOP(3)

ACCEPT(2)

 

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/158768
推荐阅读
  

闽ICP备14008679号