TCP连接的建立与断开
建立连接,三次握手,发起连接方(执行connect()
)向对方发送一个SYN报文,服务器收到后会回复自己的SYN报文以及确认收到ACK报文,客户端收到后在向服务端发送一个确认ACK报文
断开连接,四次挥手,主动关闭的一方会通知对方自己要关闭发送FIN报文,对方收到后会回复确认ACK报文,并且在不久后对方也需要关闭(close()
),也想主动断开方发送一个FIN报文通知主动断开方自己要关闭,随后主动断开方会回复确认报文ACK
状态转移
一开始我们处于一个假象的CLOSED
状态,客户端属于程序主动打开,发送SYN(三次握手第一步),客户端就会从原本假象的状态变为SYN_SENT
主动打开的状态
而服务端属于被动打开,会执行创建套接字、绑定、监听套接字的状态,在收到客户端的连接后,即收到客户端是SYN,会发送自己SYN并回复ACK确认信息(三次握手第二步),会变为SYN_RCVD
状态
当客户端收到来自服务端的SYN以及确认ACK报文,发送ACK确认报文(三次握手第三步),这时候对于客户端三次握手进行完成,继而变成完成三次握手ESTABLISHED
状态
这时候服务端收到来自客户端发送的ACK确认报文,也会转变为ESTABLISHED
完成三次握手状态,当服务端与客户端都完成三次握手,双方都会较长时间处于完成三次握手的状态,直到数据的收发完成,某一端需要进行关闭,开始四次挥手,这时候就会有状态的变化
- 当某一端发送关闭通知FIN,比如客户端进行关闭,会向服务器端发送FIN,客户端自己的状态会变成
FIN_WAIT_1
- 而服务器端收到客户端发送FIN后会回复一个ACK确认收到报文,随后服务器会变成
CLOSE_WAIT
状态 - 然后客户端收到服务端回复的ACK后转变成
FIN_WAIT_2
状态,这时候两次挥手已经完成 - 等待一会后服务器也会进行关闭,向客户端发送一个FIN,服务器就会变成
LAST_ACK
状态 - 等客户端收到来自服务端的FIN并向客户端发送一个ACK,变成
TIME_WAIT
状态 - 客户端收到ACK后就消失
CLOSEED
了
我们主动关闭的一方会最终会变成TIME_WAIT
后面也会变成CLOSED
消失,在这里为什么不直接消失CLOSED
而是先变成TIME_WAIT
再消失呢?这个我们后面讨论
当主动断开一方变成TIME_WAIT
状态,大约会持续两分钟时间,也就是两个报文最大的生存时间
当我们服务端与客户端同时关闭的时候,也就是双方同时发送FIN报文转变成FIN_WAIT_1
状态,随后双方都处于FIN_WAIT_1
状态并收到对方的FIN报文,并发送ACK报文变成CLOSING
状态,然后又接收到来自对方的ACK报文,这时候双方都变成TIME_WAIT
状态
还有一种情况就是,四次挥手演化为三次挥手,当其中一方关闭发送FIN报文转变为FIN_WAIT_1
状态,这时候对方也发送了关闭报文FIN,并且伴随着对关闭报文的回复ACK,这时候首先关闭一方就变成了TIME_WAIT
状态
TIME_WAIT 状态
TIME_WAIT 状态存在的原因有两点:
- 可靠地终止TCP连接
- 保证让迟来的TCP报文段有足够的时间被识别并丢弃
当后断开一方,向处于FIN_WAIT_2
状态的先断开一方发送FIN断开通知,而先断开一方在收到FIN后向对方发送的ACK丢失,后断开一方没有得到ACK回复就会重新发送FIN报文,而先断开一方若直接进入了CLOSED
状态则后断开一方永远也收不到ACK报文,所以当发送ACK后进入TIME_WAIT
状态,进行等待,若在这段时间内存在ACK丢失,FIN会重发,这时候TIME_WAIT
会发送回复ACK
还有一点就是可以让迟来的报文被识别并丢弃,主要针对服务器端,当服务器端与客户端进行数据交互的时候,可能会因为网络延迟造成数据包没有及时到达,当我们关闭服务器端后,可能会重启服务器,这时候网络中迟来的数据包又继续发送过来,而我们就会得到本该发送给上一个服务器的数据,所以在第一次关闭的时候在TIME_WAIT
状态等待两个报文的生存周期,对陆陆续续到达的延迟报文接收并丢弃,等没有延迟的数据后再将服务器重新运行起来
程序中查看TCP的状态
这里使用的TCP服务端与客户端代码都是我们之前使用过的代码
ser.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
//创建套接字地址结构
struct sockaddr_in saddr,caddr;//caddr存放客户端的ip与端口
memset(&saddr,0,sizeof(saddr));//清空saddr
saddr.sin_family = AF_INET;//协议族或者叫地址族
saddr.sin_port = htons(6000);//端口 转换主机序列到网络序列
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip 字符串转换为整形
//绑定套接字
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
//创建监听队列
listen(sockfd,5);
while(1)
{
//循环 接受客户端的连接
int len = sizeof(saddr);
printf("accept wait ......\n");
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c < 0)
{
continue;
}
printf("accept c = %d\n",c);
while(1)
{
char buff[128] = {0};
if(recv(c,buff,127,0) <= 0)
{
break;
}
printf("buff=%s\n",buff);
send(c,"ok",2,0);
}
close(c);//关闭c
printf("close\n");
}
}
cli.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
//socket() connect() send() recv() close()
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;//客户端(caddr)自己的端口ip由系统分配
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
while(1)
{
printf("input:\n");
char buff[128] = {0};
fgets(buff,127,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
send(sockfd,buff,strlen(buff),0);//write()也可以做到
memset(buff,0,128);//清空为了接受 来自服务渠道数据
recv(sockfd,buff,127,0);//read()也可以做到
printf("read:%s\n",buff);
}
close(sockfd);
}
我们将服务端代码运行起来,并且打开netstat -natp
只运行服务端代码,可以看到服务端处于LISTEN
状态
我们运行客户端代码建立连接
这时候可以看到客户端与服务端都已经是ESTABLISHED
状态,三次握手过程很快,所以服务端与客户端处于SYN_RCVD
与SYN_SENT
状态的时间很短
我们再运行了一个客户端代码后查看,①属于监听套接字(sockfd),每当有一个客户端与服务端连接都会有一个ser的连接套接字(②④),③⑤属于客户端
客户端与服务端再ESTABLISHED
状态会持续较常时间,并且发送以及收到数据不会影响连接的状态变化
我们将服务器关闭,这时候四次挥手进行了两次
服务端处于FIN_WAIT_2
状态,服务端进程已经消失但是连接还在,内核还没有将其释放,客户端处于CLOSE_WAIT
状态
现在关闭客户端
这里连接消失,是因为我们关闭服务端后等待时间太长一直没有收到客户端的关闭通知,所以连接会消失
我们这次快速正常的关闭服务端与客户端
服务器端处于TIME_WAIT
状态,这时候我们再次启动服务端显示启动失败,端口还在占用(为了可靠终止TCP连接,以及接收丢弃延迟到达的报文),只有等待TIME_WAIT
状态消失才能再次启动服务端,
当客户端是先关闭的一方,那么TIME_WAIT
状态就会是客户端