UNIX网络编程-TCP相关

目录

相关函数

套接字函数总结

服务端和客户端

调试程序

启动服务端后查看状态

建立连接后 kill客户端

建立连接后 kill服务端

异常退出

FIN_WAIT1状态

FIN_WAIT2和CLOSE_WAIT

FIN_WAIT2的另一种情况

处理僵尸进程

SIGPIPE信号

服务端崩溃

参考


相关函数

socket函数

#include <sys/socket.h>

//family:指定协议族 
//type:指定套接字类型 
//protocol:指定某个协议,设为0,以选择所给定family和type组合的系统默认值
int socket(int family, int type, int protocol);

socket函数的family常数

family 说明
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 秘钥套接字

socket函数的type常量

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

socket函数AF_INET或AF_INET6的protocol常量

protocol 说明
IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

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

  AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_KEY
SOCK_STREAM TCP|SCTP TCP|SCTP    
SOCK_DGRAM UPD UPD    
SOCK_SEQPACKET SCTP SCTP    
SOCK_RAW IPv4 IPv6  

connect函数

#include <sys/socket.h>
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函数

#include <sys/socket.h>

//sockfd:套接字描述符 
//myaddr:套接字地址结构的指针 ,可以不指定
//addrlen:上述结构的长度,防止内核越界,可以不指定
int bind(int sockfd, const struct sockaddr * myaddr,socklen_t addrlen);

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

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

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

listen函数

#include <sys/socket.h>
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函数

#include<sys/socket.h>

//sockfd:套接字描述符 
//cliaddr:对端(客户)的协议地址 
//addr:大小
int accept (int sockfd, struct sockaddr *cliaddr ,socklen_t * addrlen);

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

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


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

其他函数

#include <unisted.h>
pid_t fork(void);

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

int close(int sockfd);

getsockname和getpeername函数

#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr*localaddr,socklen_t *addrlen);
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派生服务器的状态

服务端和客户端

服务端代码如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <netinet/in.h>

int main(int argc, char *argv[]) {

    int listen_fd,conn_fd;
    listen_fd = socket(AF_INET, SOCK_STREAM,0);
    struct sockaddr_in server;
    struct sockaddr_in client;
    socklen_t child_len;
    char msg[100];

    memset(&server, 0, sizeof(struct sockaddr_in));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(9527);
    bind(listen_fd, (struct sockaddr *)&server, sizeof(server));
    listen(listen_fd,1024);

    //sleep(100);
    stpcpy(msg, "hehe\n");
    int msg_len = strlen(msg);
    for(;;) {
        child_len = sizeof(client);
        conn_fd = accept(listen_fd, (struct sockaddr *)&client, &child_len);

        char tmp_client_addr[100];
        int tmp_client_port = ntohs(client.sin_port);
        inet_ntop(AF_INET, (void *)&client.sin_addr, tmp_client_addr, 100);
        //printf("sizeof->%d,content->%d\n", sizeof(client.sin_addr),client.sin_addr);
        printf("client addr->%s, port->%d\n", tmp_client_addr, tmp_client_port);

        pid_t pid = fork();
        if(pid > 0) {
            close(conn_fd);
        }
        else if(pid ==0){
            close(listen_fd);
            write(conn_fd, msg, msg_len);
            sleep(100);
            exit(0);
        } 
        else {
            printf("fork error!\n");
        }
        //sleep(100);
        //close(conn_fd);
    }
    return 0;
}

客户端代码如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_LINE 100

ssize_t readline(int fd, void *vptr, size_t maxlen) {  
    ssize_t n,rc;  
    char c, *ptr;  
  
    ptr = vptr;  
    for(n=1;n<maxlen;n++) {  
        if( (rc=read(fd,&c,1)) ==1) {  
            *ptr++ = c;  
            if(c=='\n') {  
                break;  
            }  
        }  
        else if(rc == 0) {  
            *ptr = 0;  
            return (n-1);  
        }  
        else {  
            if(rc < 0) {  
                continue;  
            }  
            return -1;  
        }  
  
    }  
    *ptr = 0;  
    return n;  
}  

void client_str(FILE *file, int sock_fd) {
    char send_buf[MAX_LINE];
    char recv_buf[MAX_LINE];
    while(fgets(send_buf,MAX_LINE,file)!=NULL) {
        write(sock_fd,send_buf,strlen(send_buf));
        if(readline(sock_fd,recv_buf,MAX_LINE) == 0) {
            printf("client_str server terminated prematurely\n");
            exit(1);
        }
        fputs(recv_buf,stdout);
    } 
 
}

int main(int argc, char *argv[]) {
    int sock_fd;
    struct sockaddr_in server_addr;
    if(argc != 3) {
        printf("input <IP> <port>\n");
        exit(0);
    }
    int port = atoi(argv[2]);
    sock_fd = socket(AF_INET,SOCK_STREAM,0);
    memset(&server_addr,0,sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
    connect(sock_fd,(struct sockaddr *)&server_addr, sizeof(server_addr));
 
    client_str(stdin,sock_fd);
    exit(0);
}

调试程序

启动服务端后查看状态

netstat -anpt | grep 4021
tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      4021/./my_server 


ps -eo pid,ppid,tty,stat,args,wchan | grep 6794
 6794  6486 pts/4    S+   ./my_server                 inet_csk_accept
 6796  6794 pts/4    S+   ./my_server                 hrtimer_nanosleep


telnet ip地址 9527
client addr->1.2.3.196, port->58908

client的telnet则打印
hehe

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

。。。。
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1024)                         = 0
accept(3, 



{sa_family=AF_INET, sin_port=htons(59763), sin_addr=inet_addr("1.2.3.196")}, [16]) = 4
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcad5ade000
write(1, "client addr->113.47.177.196, por"..., 41client addr->113.47.177.196, port->59763
) = 41
write(4, "hehe\n", 5)                   = 5
close(4)                                = 0
accept(3, 

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

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


建立连接后 kill客户端

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

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

tcpdump 服务端

#连接创建
14:39:49.014872 IP 客户端.41850 > 服务端.9527: Flags [S], seq 4276092775, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 9], length 0
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
14:39:49.015327 IP 客户端.41850 > 服务端.9527: Flags [.], ack 1, win 29, length 0
#发送数据
14:39:52.015914 IP 服务端.9527 > 客户端.41850: Flags [P.], seq 1:6, ack 1, win 29, length 5
14:39:52.016395 IP 服务端.41850 > 客户端.9527: Flags [.], ack 6, win 29, length 0
#连接断开
14:40:13.854557 IP 客户端41850 > 服务端.9527: Flags [F.], seq 1, ack 6, win 29, length 0
14:40:13.855186 IP 服务端.9527 > 客户端.41850: Flags [.], ack 2, win 29, length 0
14:41:32.016266 IP 服务端.9527 > 客户端.41850: Flags [F.], seq 6, ack 2, win 29, length 0
14:41:32.016747 IP 客户端.41850 > 服务端.9527: Flags [R], seq 4276092777, win 0, length 0

netstat -anpt | grep 9527
tcp        0      0 0.0.0.0:9527         0.0.0.0:*          LISTEN          29542/./my_server   
tcp        1      0 服务端:9527         客户端:41850        CLOSE_WAIT      29635/./my_server

tcpdump客户端

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

建立连接后 kill服务端

tcpdump服务端

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

查看服务端口状态

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

tcpdump客户端

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

异常退出

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

#查看服务器状态
netstat -anpt | grep 9527
tcp        0      0 172.17.6.131:9527       114.242.122.147:55610   TIME_WAIT   - 


#tcpdump port 9527 结果
13:06:19.839944 IP 客户端.55460 > 服务端.9527: Flags [S], seq 369954675, win 8192, options [mss 1460,nop,wscale 2,nop,nop,sackOK], length 0
13:06:19.840023 IP 服务端.9527 > 客户端.55460: Flags [R.], seq 0, ack 369954676, win 0, length 0
13:06:20.346321 IP 客户端.55460 > 服务端.9527: Flags [S], seq 369954675, win 8192, options [mss 1460,nop,wscale 2,nop,nop,sackOK], length 0
13:06:20.346389 IP 服务端.9527 > 客户端.55460: Flags [R.], seq 0, ack 1, win 0, length 0
13:06:20.858551 IP 客户端.55460 > 服务端.9527: Flags [S], seq 369954675, win 8192, options [mss 1460,nop,nop,sackOK], length 0
13:06:20.858635 IP 服务端.9527 > 客户端.55460: Flags [R.], seq 0, ack 1, win 0, length 0

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

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

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

netstat -anpt | grep 9527
tcp        0      0 172.17.6.131:9527       47.93.18.8:54928        TIME_WAIT   - 


strace ./my_server
。。。。
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
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)
listen(3, 1024)                         = 0
accept(3, 


#再次执行netstat,没有任何结果
netstat -anpt | grep 9527

tcpdump port 9527
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
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的结果如下

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


#等待100秒后,服务端子进程结束,然后会发送FIN包
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
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
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
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
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
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
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

在看端口状态

netstat -anpt | grep 9527
tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      7245/./my_server    
tcp        0      1 172.17.6.131:9527       47.93.18.8:54980        FIN_WAIT1   -                   
tcp        0      0 172.17.6.131:9527       113.47.177.196: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状态

kill 客户端的telnet
netstat -anpt | grep 9527
tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      6356/./my_server    
tcp        1      0 172.17.6.131:9527       47.93.18.8:54934        CLOSE_WAIT  6365/./my_server    
tcp        0      0 172.17.6.131:54934      47.93.18.8:9527         FIN_WAIT2   -  

服务端的状态
netstat -anpt | grep 9527
tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      6356/./my_server    
tcp        1      1 172.17.6.131:9527       47.93.18.8:54934        LAST_ACK    -   

tcpdump的结果

tcpdump结果,客户端发送了FIN,服务端响应了ACK,所以服务端处于CLOSE_WAIT状态,客户端处于FIN_WAIT2状态
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
17:19:00.780233 IP 服务端.9527 > 客户端.54934: Flags [.], ack 2, win 227, options [nop,nop,TS val 90791085 ecr 90791083], length 0



服务端sleep 100秒后,关闭连接,因为客户端已经不在了,所以发的FIN没有响应,此时服务端是LAST_ACK状态
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
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
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
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
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

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
08:42:08.225237 IP 客户端.55050 > 服务端.9527: Flags [.], ack 7, win 229, options [nop,nop,TS val 146178530 ecr 146178489], length 0

查看进程状态

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

strace客户端程序结果如下

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

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

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

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
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,也就不会出现上述这种情况了
 

处理僵尸进程

增加如下代码

#include <signal.h>

void handler_child(int sig_num) {
    pid_t pid;
    int status;
    while( (pid=waitpid(-1, &status, WNOHANG)) > 0 ) {
        printf("child %d terminated\n",pid); 
    }
    //pid = wait(&status);
    //printf("child %d terminated\n",pid); 
    return;
}

void install_singal() {
    struct sigaction act;
    struct sigaction o_act;
    act.sa_handler = handler_child;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    //act.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &act, &o_act);
}

main() {
    bind(listen_fd, (struct sockaddr *)&server, sizeof(server));
    listen(listen_fd,1024);
    install_singal();
    //signal(SIGCHLD, handler_child);
	。。。
	for(;;)
	。。。
}

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

./my_server 
pid->10926, conn_fd->4
client addr->47.93.18.8, port->55168
child 10932 terminated
pid->10926, conn_fd->-1
client addr->47.93.18.8, port->55168
child 10933 terminated
pid->10926, conn_fd->-1
client addr->47.93.18.8, port->55168
child 10934 terminated


#strace 进程,发现kill了之后又起来了一个新的
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
write(1, "client addr->47.93.18.8, port->5"..., 37client addr->47.93.18.8, port->55166
) = 37
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb1f7e69a10) = 10851
close(-1)                               = -1 EBADF (Bad file descriptor)
accept(3, strace: Process 10851 attached
 <unfinished ...>
[pid 10851] close(3)                    = 0
[pid 10851] write(-1, "hehe\n", 5)      = -1 EBADF (Bad file descriptor)
[pid 10851] exit_group(0)               = ?
[pid 10851] +++ exited with 0 +++

<... accept resumed> 0x7ffcd2229d40, 0x7ffcd2229d3c) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=10851, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 10851
write(1, "child 10851 terminated\n", 23child 10851 terminated
) = 23
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
write(1, "client addr->47.93.18.8, port->5"..., 37client addr->47.93.18.8, port->55166
) = 37
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb1f7e69a10) = 10852
close(-1)                               = -1 EBADF (Bad file descriptor)
accept(3, strace: Process 10852 attached
 <unfinished ...>
[pid 10852] close(3)                    = 0
[pid 10852] write(-1, "hehe\n", 5)      = -1 EBADF (Bad file descriptor)
[pid 10852] exit_group(0)               = ?
[pid 10852] +++ exited with 0 +++
<... accept resumed> 0x7ffcd2229d40, 0x7ffcd2229d3c) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)

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

act.sa_flags = 0;
//act.sa_flags = SA_RESTART;
改为
//act.sa_flags = 0;
act.sa_flags = SA_RESTART;

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


./my_server 
pid->13947, conn_fd->4
client addr->47.93.18.8, port->55184
child 13954 terminated
pid->13947, conn_fd->4
client addr->47.93.18.8, port->55186
child 13960 terminated
pid->13947, conn_fd->4
client addr->47.93.18.8, port->55188
child 13962 terminated
pid->13947, conn_fd->4
client addr->47.93.18.8, port->55190
child 13964 terminated



socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1024)                         = 0
rt_sigaction(SIGCHLD, {0x400b12, [], SA_RESTORER|SA_RESTART, 0x7fafb4aaa280}, {SIG_DFL, [], 0}, 8) = 0
accept(3, 


{sa_family=AF_INET, sin_port=htons(55196), sin_addr=inet_addr("47.93.18.8")}, [16]) = 4
getpid()                                = 14006
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 6), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fafb5060000
write(1, "pid->14006, conn_fd->4\n", 23pid->14006, conn_fd->4
) = 23
write(1, "client addr->47.93.18.8, port->5"..., 37client addr->47.93.18.8, port->55196
) = 37
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fafb5055a10) = 14008
close(4)                                = 0
accept(3, strace: Process 14008 attached
 <unfinished ...>
 
[pid 14008] close(3)                    = 0
[pid 14008] write(4, "hehe\n", 5)       = 5
[pid 14008] exit_group(0)               = ?
[pid 14008] +++ exited with 0 +++

<... accept resumed> 0x7ffec87a2930, 0x7ffec87a292c) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=14008, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 14008
write(1, "child 14008 terminated\n", 23child 14008 terminated
) = 23
rt_sigreturn({mask=[]})                 = 43
accept(3, 

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

 。。。 
    int fd[5];
    for(;i<5;i++) {
        fd[i] = socket(AF_INET,SOCK_STREAM,0);
        memset(&server_addr,0,sizeof(struct sockaddr_in));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port);
        inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
        connect(fd[i],(struct sockaddr *)&server_addr, sizeof(server_addr));
        //client_str(stdin,fd[i]);
        //close(fd[i]);
    }
    sleep(10);
    exit(0);
。。。

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


netstat -anpt | grep 9527
tcp        0      0 0.0.0.0:9527            0.0.0.0:*               LISTEN      2612/./my_server    
tcp        6      0 客户端:54224      		服务端:9527         	CLOSE_WAIT  2626/./client       
tcp        6      0 客户端:54226      		服务端:9527         	CLOSE_WAIT  2626/./client       
tcp        6      0 客户端:54230      		服务端:9527         	CLOSE_WAIT  2626/./client       
tcp        0      0 服务端:9527       		客户端:54222        	FIN_WAIT2   -                   
tcp        6      0 客户端:54222      		服务端:9527         	CLOSE_WAIT  2626/./client       
tcp        0      0 服务端:9527       		客户端:54230        	FIN_WAIT2   -                   
tcp        6      0 客户端:54228      		服务端:9527         	CLOSE_WAIT  2626/./client       
tcp        0      0 服务端:9527       		客户端:54228        	FIN_WAIT2   -                   
tcp        0      0 服务端:9527       		客户端:54224        	FIN_WAIT2   -                   
tcp        0      0 服务端:9527       		客户端:54226        	FIN_WAIT2   -   

ps aux | grep my_        
root      2612  0.0  0.1   4224  1412 pts/0    S+   11:48   0:00 ./my_server
root      2635  0.0  0.2 112720  2220 pts/3    S+   11:49   0:00 grep --color=auto my_


strace客户端
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
connect(4, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 5
connect(5, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
connect(6, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 7
connect(7, {sa_family=AF_INET, sin_port=htons(9527), sin_addr=inet_addr("服务端")}, 16) = 0

SIGPIPE信号

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

# 客户端连上服务端之后,服务端就退出了,所以发送了FIN包
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
13:05:45.490602 IP 客户端.54236 > 服务端.9527: Flags [.], ack 6, win 502, options [nop,nop,TS val 2787010344 ecr 2787010343], length 0

# 客户端继续写入数据,因为服务端已经退出了,所以响应了RST包
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
13:09:31.232153 IP 服务端.9527 > 客户端.54236: Flags [R], seq 723649255, win 0, length 0

通过strace客户端结果如下

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

"\n", 1024)                     = 1
write(3, "\n", 1)                       = 1
read(3, "h", 1)                         = 1
read(3, "e", 1)                         = 1
read(3, "h", 1)                         = 1
read(3, "e", 1)                         = 1
read(3, "\n", 1)                        = 1
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5482495000
write(1, "hehe\n", 5hehe
)                   = 5
read(0, 

"\n", 1024)                     = 1
write(3, "\n", 1)                       = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=2755, si_uid=0} ---
+++ 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)

猜你喜欢

转载自blog.csdn.net/hixiaoxiaoniao/article/details/85269249
今日推荐