1. 程式人生 > >UNIX網路程式設計-TCP相關

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)