先看TCP IP的10种状态,如下所示:
三次握手:
客户端A端发送SYN,然后进入SYN_SENT状态,服务器B端接收到SYN后,返回一个响应ACK,同时也发送一个SYN,然后B端进入SYN_RCVD状态,A端收到ACK后进入ESTABLISHED状态,然后发送一个ACK,服务器B端收到ACK后进入ESTABLISHED状态。
四次分手:
先关闭的一端A端发送FIN然后进入FIN_WAIT_1状态,另一端B端会返回一个响应,然后A端进入FIN_WAIT_2状态。当服务器端B端检测到对端已经关闭(read到0)时,也会调用close,发送FIN给A端,然后B端进入LAST_ACK状态,A端收到FIN后回复一个响应然后进入TIME_WAIT状态,服务器B端接收到响应后进入CLOSED状态。
三次握手双方都进入ESTABLISHED状态后,表示双方可以通信了。查看TCP的连接状态可以使用netstat -na。
TCP IP协议为什么要做成三次握手和四次断开?
因为TCP IP是全双工协议,需要互相确认对方的身份,A要确认B收到自己发的包了,B也要确认A收到自己发的包了。
两个银行之间的连接和上述握手过程类似,A银行先发送加密包,B端收到后解密,然后B发送一个加密包,A端收到后解密,在这个过程中,协商出一个秘钥,从此这个秘钥就用来在两个会话之间进行加密。
四次分手中,任何一端都可以先关闭连接,先调用close的那一端,最终状态要推进到TIME_WAIT,TIME_WAIT状态的含义就是等一会再最终关闭。套接字处于TIME_WAIT状态时,再次在这个套接字上启动服务器是起不来的,除非使用套接字的IO复用技术(SO_REUSEADDR)。
调用close相当于发送了一个'\0',另一端会读取到0,读取到0就会知道对方已经关闭了(至少对方的写数据断了)。当A端关闭了socket,不代表B端不能往自己的socket中写数据,最多是写失败,毕竟socket是存在缓冲区的。B端得知A端已关闭后,可以根据自己的业务需要,来关闭自己的socket,也可以不关闭。TCP IP是全双工的,两端都执行关闭,才能将这条通路关闭,如果只有一端关闭,那么这条通路是没有彻底关闭的,只是关闭了一个方向的数据流。
主动关闭的一方进入FIN_WAIT_2就是半连接状态,只要对等方不调用close,这个连接就一直处于半连接状态。服务器端接收到FIN后,是TCP IP内核回复的ACK,对应用透明。
我们用一个实验查看半连接状态。
服务器程序如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ void handler(int num)
{
int mypid = ;
while((mypid = waitpid(-, NULL, WNOHANG)) > )
{
printf("child %d die\n", mypid);
}
} int main()
{
int sockfd = ;
signal(SIGCHLD, handler);
sockfd = socket(AF_INET, SOCK_STREAM, ); if(sockfd == -)
{
perror("socket error");
exit();
} struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.6.249");
//addr.sin_addr.s_addr = INADDR_ANY; int optval = ;
if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < )
{
perror("setsockopt error");
exit();
} if( bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < )
{
perror("bind error");
exit();
} if(listen(sockfd, SOMAXCONN) < )
{
perror("listen error");
exit();
} struct sockaddr_in peeraddr;
socklen_t peerlen; int conn = ; while()
{
conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen);
if(conn == -)
{
perror("accept error");
exit();
} char *p = NULL;
int peerport = ;
p = inet_ntoa(peeraddr.sin_addr);
peerport = ntohs(peeraddr.sin_port);
printf("peeraddr = %s\n peerport = %d\n", p, peerport); pid_t pid = fork(); if(pid == -)
{
perror("fork error");
exit();
} if(pid == )
{
char recvbuf[] = {};
int ret = ;
while()
{
ret = read(conn, recvbuf, sizeof(recvbuf)); if(ret == )
{
printf("peer closed \n");
exit();
}
else if(ret < )
{
perror("read error");
exit();
} fputs(recvbuf, stdout); write(conn, recvbuf, ret);
}
}
} close(conn);
close(sockfd); return ;
}
客户端程序如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ int main()
{
int sockfd[]; int i = ; for(i = ; i < ; i++)
{
sockfd[i] = socket(AF_INET, SOCK_STREAM, ); struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.31.128"); if( connect(sockfd[i], (struct sockaddr *)&addr, sizeof(addr)) == - )
{
perror("connect error");
exit();
} struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sockfd[i], (struct sockaddr*)&localaddr, &addrlen) < )
{
perror("getsockname error");
exit();
} printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); } char recvbuf[] = {};
char sendbuf[] = {};
int ret = ; while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sockfd[], sendbuf, strlen(sendbuf)); ret = read(sockfd[], recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout);
memset(recvbuf, , sizeof(recvbuf));
memset(sendbuf, , sizeof(sendbuf)); } return ;
}
我们首先启动服务器,然后启动客户端,然后关掉服务器(执行主动关闭),并查看套接字状态,如下所示:
服务器执行主动关闭,由程序可以得知,服务器关闭后会发送FIN给客户端,客户端的TCP IP协议栈接收到FIN后恢复ACK给服务器,这时候服务器进入了FIN_WAIT2状态,但是客户端一直阻塞在fgets函数那里,所以没有执行close函数,因此,客户端也就不会主动发送FIN,因此,服务器处于半连接状态(由于我们将服务器进程关闭了,所以这个半连接状态过一段时间也会消失,如果不关闭服务器,则会一直处于半连接状态)。客户端套接字一直处于CLOSE_WAIT状态。
首先关闭的一端存在TIME_WAIT是因为要确保最后一个确认包能发送到对端,保证对端能正常关闭,具体原因见另一篇博客。
以上我们只说了10种状态,现在来看第11种状态,如下所示:
当通信双方同时close时,会出现第11种状态,叫CLOSIING。
发送FIN后进入FIN_WAIT_1状态,收到对端的FIN后,回复一个ACK并进入CLOSIING状态,收到对端的ACK后进入TIME_WAIT状态。
细节知识:
如果发送端向缓冲区写入数据,然后调用close,这样接收端还能收到数据吗?例如调用如下的程序片段:
send(fd, "abcd");
close(fd);
接收端是可以收到数据的,而且可以可靠的收到。发送端会先将abcd顺序写入缓冲区,调用close时,会将'\0'也写入缓冲区,然后开始像流水一样顺序发送出去。接收端的TCP IP协议栈会逐个分析,当发现FIN时就知道发送端关闭了。
服务器向客户端发送了FIN,但是客户端的应用程序没有处理读到的0,而是继续向socket套接字写数据,向服务器发送报文。因为TCP IP是双工的,服务器关闭socket,不等于客户端不能写数据,在这种场景下,如果客户端向服务器发送数据,会引起TCP IP协议RST段重置,会使客户端接收到一个SIGPIPE信号,如果客户端不处理,则默认动作是让进程退出。
管道破裂示例程序如下:
服务器:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ void handler(int num)
{
int mypid = ;
while((mypid = waitpid(-, NULL, WNOHANG)) > )
{
printf("child %d die\n", mypid);
}
} int main()
{
int sockfd = ;
signal(SIGCHLD, handler);
sockfd = socket(AF_INET, SOCK_STREAM, ); if(sockfd == -)
{
perror("socket error");
exit();
} struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.6.249");
//addr.sin_addr.s_addr = INADDR_ANY; int optval = ;
if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < )
{
perror("setsockopt error");
exit();
} if( bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < )
{
perror("bind error");
exit();
} if(listen(sockfd, SOMAXCONN) < )
{
perror("listen error");
exit();
} struct sockaddr_in peeraddr;
socklen_t peerlen; int conn = ; while()
{
conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen);
if(conn == -)
{
perror("accept error");
exit();
} char *p = NULL;
int peerport = ;
p = inet_ntoa(peeraddr.sin_addr);
peerport = ntohs(peeraddr.sin_port);
printf("peeraddr = %s\n peerport = %d\n", p, peerport); pid_t pid = fork(); if(pid == -)
{
perror("fork error");
exit();
} if(pid == )
{
printf("child pid=%d\n", getpid());
char recvbuf[] = {};
int ret = ;
while()
{
ret = read(conn, recvbuf, sizeof(recvbuf)); if(ret == )
{
printf("peer closed \n");
exit();
}
else if(ret < )
{
perror("read error");
exit();
} fputs(recvbuf, stdout); write(conn, recvbuf, ret);
}
} close(conn);
} close(conn);
close(sockfd); return ;
}
客户端:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ int main()
{
int sockfd[]; int i = ; for(i = ; i < ; i++)
{
sockfd[i] = socket(AF_INET, SOCK_STREAM, ); struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.31.128"); if( connect(sockfd[i], (struct sockaddr *)&addr, sizeof(addr)) == - )
{
perror("connect error");
exit();
} struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sockfd[i], (struct sockaddr*)&localaddr, &addrlen) < )
{
perror("getsockname error");
exit();
} printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); } char recvbuf[] = {};
char sendbuf[] = {};
int ret = ; while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//write(sockfd[0], sendbuf, strlen(sendbuf));
write(sockfd[], "aaaaaaaaaa", );
write(sockfd[], "aaaaaaaaaa", );
write(sockfd[], "aaaaaaaaaa", );
write(sockfd[], "aaaaaaaaaa", );
//ret = read(sockfd[0], recvbuf, sizeof(recvbuf)); //fputs(recvbuf, stdout);
memset(recvbuf, , sizeof(recvbuf));
memset(sendbuf, , sizeof(sendbuf)); } return ;
}
我们的实验步骤是这样的,启动服务器,然后启动客户端,使用kill命令杀死服务器中的业务进程(子进程),然后查看套接字状态,这时可以看到客户端进程处于CLOSE_WAIT状态,因为客户端没有调用close,而是阻塞在fgets函数了。这时,我们在终端上随便输入几个字符,让客户端往套接字写数据。这时客户端会退出(这就是管道破裂了),ps -u已经看不到客户端进程了。
结果如下:
网络服务程序中有些进程莫名退出可能就是管道破裂导致的。
当管道破裂时,我们不想让进程退出,而是捕捉信号做其他处理,因此,需要注册信号处理函数,修改后的客户端程序如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ void handler(int num)
{
printf("signal num : %d\n", num);
} int main()
{
int sockfd[]; signal(SIGPIPE, handler); int i = ; for(i = ; i < ; i++)
{
sockfd[i] = socket(AF_INET, SOCK_STREAM, ); struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.31.128"); if( connect(sockfd[i], (struct sockaddr *)&addr, sizeof(addr)) == - )
{
perror("connect error");
exit();
} struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sockfd[i], (struct sockaddr*)&localaddr, &addrlen) < )
{
perror("getsockname error");
exit();
} printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); } char recvbuf[] = {};
char sendbuf[] = {};
int ret = ; while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
//write(sockfd[0], sendbuf, strlen(sendbuf));
write(sockfd[], "aaaaaaaaaa", );
write(sockfd[], "aaaaaaaaaa", );
write(sockfd[], "aaaaaaaaaa", );
write(sockfd[], "aaaaaaaaaa", );
//ret = read(sockfd[0], recvbuf, sizeof(recvbuf)); //fputs(recvbuf, stdout);
memset(recvbuf, , sizeof(recvbuf));
memset(sendbuf, , sizeof(sendbuf)); } return ;
}
管道破裂时,进程不会退出了,执行结果如下:
实际应用中,我们注册SIGPIPE的信号处理函数,然后对write的返回值做异常处理就好了。
调用close相当于将读和写全部关闭,shutdown函数可以有选择的终止某个方向的数据的传送或者终止数据传送的两个方向。当我们只想关闭某一个方向的写,不想关闭收时可以调用这个函数。比如我们发送ABC,然后调用shutdown(发送FIN),则关闭了写,ABC和FIN会传送到对端,我们还可以在这一端读取对端的回信。对端可能会回复DEF,然后关闭对端的套接字。
shutdown how=1就可以保证对等方接收到一个EOF(\0)字符,而不管其他进程是否已经打开了套接字。而close不能保证,直到套接字引用计数减为0时才发送FIN。也就是说直到所有的进程都关闭了套接字。不管文件描述符的引用计数为2,3,5或者其他,但是我们还想关闭这个文件描述符,这时就可以用shutdown了。
how参数可以选择关闭读或者写,下面我们进行实验。
服务器端程序:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ void handler(int num)
{
int mypid = ;
while((mypid = waitpid(-, NULL, WNOHANG)) > )
{
printf("child %d die\n", mypid);
}
} int main()
{
int sockfd = ;
signal(SIGCHLD, handler);
sockfd = socket(AF_INET, SOCK_STREAM, ); if(sockfd == -)
{
perror("socket error");
exit();
} struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.6.249");
//addr.sin_addr.s_addr = INADDR_ANY; int optval = ;
if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < )
{
perror("setsockopt error");
exit();
} if( bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < )
{
perror("bind error");
exit();
} if(listen(sockfd, SOMAXCONN) < )
{
perror("listen error");
exit();
} struct sockaddr_in peeraddr;
socklen_t peerlen; int conn = ; while()
{
conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen);
if(conn == -)
{
perror("accept error");
exit();
} char *p = NULL;
int peerport = ;
p = inet_ntoa(peeraddr.sin_addr);
peerport = ntohs(peeraddr.sin_port);
printf("peeraddr = %s\n peerport = %d\n", p, peerport); pid_t pid = fork(); if(pid == -)
{
perror("fork error");
exit();
} if(pid == )
{
close(sockfd);
printf("child pid=%d\n", getpid());
char recvbuf[] = {};
int ret = ;
while()
{
ret = read(conn, recvbuf, sizeof(recvbuf)); if(ret == )
{
printf("peer closed \n");
exit();
}
else if(ret < )
{
perror("read error");
exit();
} fputs(recvbuf, stdout);
write(conn, recvbuf, ret); if(recvbuf[] == '')
{
close(conn);
//shutdown(conn, SHUT_WR);
} }
} //close(conn);
} close(conn);
close(sockfd); return ;
}
客户端程序如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h> /* superset of previous */ void handler(int num)
{
printf("signal num : %d\n", num);
} int main()
{
int sockfd[]; signal(SIGPIPE, handler); int i = ; for(i = ; i < ; i++)
{
sockfd[i] = socket(AF_INET, SOCK_STREAM, ); struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
inet_aton("192.168.31.128", &addr.sin_addr);
//addr.sin_addr.s_addr = inet_addr("192.168.31.128"); if( connect(sockfd[i], (struct sockaddr *)&addr, sizeof(addr)) == - )
{
perror("connect error");
exit();
} struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if(getsockname(sockfd[i], (struct sockaddr*)&localaddr, &addrlen) < )
{
perror("getsockname error");
exit();
} printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); } char recvbuf[] = {};
char sendbuf[] = {};
int ret = ; while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sockfd[], sendbuf, strlen(sendbuf)); ret = read(sockfd[], recvbuf, sizeof(recvbuf)); if(ret == )
{
perror("server closed");
exit();
} fputs(recvbuf, stdout);
memset(recvbuf, , sizeof(recvbuf));
memset(sendbuf, , sizeof(sendbuf)); } return ;
}
在服务器端程序中,我们将122行的close(conn)注释掉,这时候conn的引用计数为2,第113-117行中,当服务器接收到第一个字符时'2'时,子进程关闭conn,但是这时候conn
的引用计数是1,不会发送FIN,因为调用close时,只有引用计数变为0时才会发送FIN。当客户端接收到FIN时,会进入到67行打印server closed,但是执行时却没有打印,也就是说客户端没有接收到FIN,执行结果如下所示:
下面我们将服务器程序的第115行注释掉,换成116行的shutdown函数,当服务器接收到第一个字符是‘2’时,会执行shutdown,这时即使conn的引用计数是2,也还是会发送FIN给客户端,客户端接收到FIN时,会打印出server closed,执行结果如下: