TCP通信过程详解(三次握手、四次挥手)

Tcp头部

 

SYN:SYN= 1 表示这是一个连接请求或连接接受报文。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若是同意建立连接,则应响应的报文段中使SYN=1、ACK=1。

ACK:确认号只有在该位设置为1的时候才生效,当该位为0表示确认号无效。TCP规定,在TCP连接建立后所有传送的数据报文段ACK都必须设置为1。

FIN:当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。

序号:占4个字节,它的范围在0-2^32-1,序号随着通信的进行不断的递增,当达到最大值的时候重新回到0在开始递增。 

确认号:是对下一个想要接受的字节的期望

TCP中的三次握手,四次挥手

过程描述:首先由Client发出请求连接即 SYN=1 ACK=0 ,TCP规定SYN=1时不能携带数据,但要消耗一个序号,因此声明自己的序号是 seq=x。然后 Server 进行回复确认,即 SYN=1 ACK=1 seq=y,ack=x+1。再然后 Client 再进行一次确认,但不用SYN 了,这时即为 ACK=1, seq=x+1,ack=y+1。

调用socket函数创建一个套接字时,状态是CLOSED,调用listen函数导致套接字从:CLOSED状态转化为:LISTEN状态

只有当服务端的read函数返回为0的时候,服务端才需要,也即才可以发起关闭请求(FIN),发送完成之后,就变成了:LAST_ACK, 当客户端接受到了这个关闭请求之后,状态会变成了:TIME_WAIT(会经过2MSL(TCP报文端最大生存周期的两倍时间)之后,转变为:CLOSED),紧接着客户端会发送最后一次确认:(ACK N+1),等到服务端接收到这个确认后,服务端的状态会变成:CLOSED

为什么要进行三次握手(两次确认):

为什么A还要发送一确认呢?这主要是为了防止已失效的连接请求报文突然又传送到了B,因而产生错误。所谓“已失效的连接请求报文段”是这样产生的。考虑一种正常情况。A发出连接请求,但因连接请求丢失而未收到确认。于是A再次重传一次连接请求。后来收到了确认建立了连接。数据传输完毕后,就释放了连接。A发送了两个连接请求的报文段,其中第一个丢失,第二个到达了B。没有“已失效的连接请求报文段”。

现假定出现一种异常情况,即A发出的第一个连接请求报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到B。本来这是一个已失效的报文段。但是B收到此失效的连接请求报文段后,就误认为是A有发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。假定不采用三次握手,那么只要B发出确认,新的连接就建立了。由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据。B的许多资源就这样拜拜浪费了。

另一种解释:

这个问题的本质是, 信道不可靠, 但是通信双方需要就某个问题达成一致. 而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值. 所以三次握手不是TCP本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的. 请注意这里的本质需求,信道不可靠, 数据传输要可靠. 三次达到了, 那后面你想接着握手也好, 发数据也好, 跟进行可靠信息传输的需求就没关系了. 因此,如果信道是可靠的, 即无论什么时候发出消息, 对方一定能收到, 或者你不关心是否要保证对方收到你的消息, 那就能像UDP那样直接发送消息就可以了”。这可视为对“三次握手”目的的另一种解答思路。

CLOSING:该状态产生的原因是:对于客户端和服务端而言,两者同时关闭的情况(这种情况并不多见),如下图:

在关闭的过程中,不一定可以必须要经过FIN_WAIT_2这个状态。 

TIME_WAIT:

1,我们可以从上面的状态分析中得知,对于TIME_WAIT状态而言,是执行主动关闭的那端经历了这个状态。该端点停留在这个状态的持续时间是最长分节生命期(MAXIMUM SEGMENT LIFETIME, msl)的两倍,有时候称之为:2MSL任何TCP实现都必须为MSL选择一个值,RFC1122的建议值是2分钟,而源自Berkeley的实现传统上改用30秒这个值,又因为:信息的传送是需要一个来回,即,TIME_WAIT状态的持续时间是1分钟到4分钟之间。而MSL是任何IP数据报能够在因特网中存活的最长时间。我们也知道这个时间是有限的,因为每个数据报含有一个跳限(hop limit)的8位字段,它的最大值是255。尽管这是一个跳数限制而不是真正的时间限制,我们仍然假设:具有最大跳限(255)的分组在网络中存在的时间不可能超过MSL秒。 

分组在网络中“迷途”通常是路由异路的结果。某个路由器崩溃或某两个路由器之间的某个链路断开时,路由协议需要花数秒钟到数分钟的时间才能稳定并找出另一条通路。在这段时间内可能发生路由循环(路由器A把分组发送给路由器B,而B再把它们发送给A),我们关心的分组可能就此陷入这样的循环。

假设迷途的分组是一个TCP分节,在它迷途期间,发送端TCP超时重传该分组,而重传的分组却通过某条候选路径到达最终目的。然而不久后(自迷途的分组开始其旅程起最多MSL秒以内)路由循环修复,早先迷失在这个循环中的分组最终也被送到目的地。TCP必须正确处理这些重复的分组。

TIME_WAIT状态存在的两个理由:

1,可靠的实现TCP全双工连接的终止(更好的完善TCP的可靠性)

2,允许老的重复分节在网络中消逝

关于第一点:假设最终的ACK丢失了来解释(并不能保证传输的可靠行)。服务器将重新发送它的最终的那个FIN, 因此客户必须维护状态信息,以允许它重新发送那个ACK。要是客户不维护状态信息,它将响应以一个RST(另外一种类型的TCP分节),该分节将被服务器解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流(即全双工关闭),那么它必须正确处理连接终止序列4个分节中任何一个分节丢失的情况。本例子也说明了为什么执行主动关闭的那一端是处于TIME_WAIT的那一端;因为可能不得不重传最终的那个ACK的就是那一端。

关于第二点:我们假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身,因为他们的IP地址和端口号相同。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一个连接的某个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足矣让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。。。。

四次挥手关闭连接

 当客户A 没有东西要发送时就要释放 A 这边的连接,A会发送一个报文(没有数据),其中 FIN 设置为1,  服务器B收到后会给应用程序一个信,这时A那边的连接已经关闭,即A不再发送信息(但仍可接收信息)。  A收到B的确认后进入等待状态,等待B请求释放连接, B数据发送完成后就向A请求连接释放,也是用FIN=1 表示, 并且用 ack = u+1(如图), A收到后回复一个确认信息,并进入 TIME_WAIT 状态, 等待 2MSL 时间。

l  为什么要等待呢?

l  为了防止这种情况:A接到B的释放连接请求后会发送一个确认信息,但是如果这个确认信息丢了,也就是B没有收到确认释放连接,那么B就会重发一个释放连接请求,这时候A还处于TIME_WAIT状态,所以会再次发送一个确认信息。

l  Q2为什么TIME_WAIT 状态还需要等2*MSL秒之后才能返回到CLOSED 状态呢?

l  A2因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED 状态(就好比从SYN_SENT 状态到ESTABLISH 状态那样),但是我们必须假想网络是不可靠的,你无法保证你最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK 状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT 状态的作用就是用来重发可能丢失的ACK报文。 

11种状态

l  SYN_RCVD :这个状态很短暂,用netstat很难看到这种状态,除非故意写一个监测程序,将三次TCP握手过程中最后一个ACK报文不予发送。

l  SYN_SENT :这个状态与SYN_RCVD 状态相呼应,当客户端SOCKET执行connect()进行连接时,它首先发送SYN报文,然后随即进入到SYN_SENT 状态

l  FIN_WAIT_1 :在实际的正常情况下,无论对方处于任何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1 状态一般是比较难见到的。

l  FIN_WAIT_2 :FIN_WAIT_2状态下的SOCKET表示半连接,即有一方调用close()主动要求关闭连接。注意:FIN_WAIT_2 是没有超时的(不像TIME_WAIT 状态),这种状态下如果对方不关闭(不配合完成4次挥手过程),那这个 FIN_WAIT_2 状态将一直保持到系统重启,越来越多的FIN_WAIT_2 状态会导致内核crash。

l  TIME_WAIT :表示收到了对方的FIN报文,并发送出了ACK报文。 TIME_WAIT状态下的TCP连接会等待2*MSL(Max Segment Lifetime,最大分段生存期,指一个TCP报文在Internet上的最长生存时间。每个具体的TCP协议实现都必须选择一个确定的MSL值,RFC 1122建议是2分钟,但BSD传统实现采用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本机的这个值),然后即可回到CLOSED 可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(这种情况应该就是四次挥手变成三次挥手的那种情况)

l  CLOSING :正常情况下,当一方发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING 状态表示一方发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?那就是当双方几乎在同时close()一个SOCKET的话,就出现了双方同时发送FIN报文的情况,这是就会出现CLOSING 状态,表示双方都正在关闭SOCKET连接。

l  CLOSE_WAIT :表示正在等待关闭。怎么理解呢?当对方close()一个SOCKET后发送FIN报文给自己,你的系统毫无疑问地将会回应一个ACK报文给对方,此时TCP连接则进入到CLOSE_WAIT状态。接下来呢,你需要检查自己是否还有数据要发送给对方,如果没有的话,那你也就可以close()这个SOCKET并发送FIN报文给对方,即关闭自己到对方这个方向的连接。有数据的话则看程序的策略,继续发送或丢弃。简单地说,当你处于CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。

l  LAST_ACK :当收到对方的ACK报文后,也就可以进入到CLOSED 可用状态了。

 

仅仅打开服务端之后(端口号为5188)netstat -an|grep tcp|grep 5188

再次打开客户端,继续观察一下状态:

netstat -an|grep tcp|grep 5188

1,查找服务器进程:ps  -ef | grep echoserv

分析其pid号,知道了我们此刻打开的是中间的这个服务端(21858,21849)

所以,此刻,我们杀死这个进程:kill -9  21858

再次查看一下状态:

为什么会产生一个FIN_WAIT2, 而不是TIME_WAIT状态呢服务端关闭之后,然后客户端接收到啦这个分节,并向服务端发送了当前的分节确认,然后自己阻塞在了从键盘获取字符的这个位置,并不能运行到函数read处去,也就是说,read函数压根就不会返回0,所以客户端就不会重新向服务端重新发送关闭连接的分节,也就停留在此刻了,同样,服务端接受到啦确认分节,那么自己的状态就变成了FIN_WAIT_2

void echo_cli(int sock) {

        char sendbuf[1024] = {0};

        char recvbuf[1024] = {0};

        while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) {

                writen(sock, sendbuf, strlen(sendbuf));

                int ret = readline(sock, recvbuf, sizeof(recvbuf));

                if(ret == -1) ERR_EXIT("READline");

                else if(ret == 0) {

                        printf("client close \n");      

                        break;

                }

                fputs(recvbuf, stdout);      //fgets接受到的数据,默认说明是存在换行符的

                memset(sendbuf, 0 , sizeof(sendbuf));

                memset(recvbuf, 0 , sizeof(recvbuf));

        }

        close(sock);

}

此刻,如果我们再重新输入字符,然后就会执行到read函数处,由于对方已经关闭,对端会接收到(四次挥手)的第一个分节(FIN),然后read返回0,从上面函数可以看出,程序执行break,然后继续执行close(sock)

而对于客户端先关闭的情况,,,则是这个样子的,同理,先打开服务端,再打开客户端,进去之后,直接按:CTRL + C,使客户端退出,我们查看一下状态:

可以知道,出现了TIME_WAIT状态,

void echo_serv(int conn) {

        char recvbuf[1024];

        while(1){

                memset(recvbuf, 0, sizeof(recvbuf));

                int ret = readline(conn, recvbuf, 1024);

                if(ret == -1) ERR_EXIT("READLine");

                if(ret == 0) {

                        printf("client close\n");       

                        break;

                }

                fputs(recvbuf, stdout);

                writen(conn, recvbuf, strlen(recvbuf));  

        }

  close(conn);

}

出现这个状态也是比较简单,因为:客户端结束了之后,服务端开始执行readline(里面封装了read),read 返回为0不会阻塞,紧接着就执行close,会继续发送一个fin分节,,所以会出现后面的TIME_WAIT状态啦,,,

我们的服务器端会处于TIME_WAIT状态,这时如果我们继续打开服务器会出现:地址占用,

bind:address already in use

如果不使用REUSEADDR的话,如果使用这个REUSEADDR,并且设置选项的话,setsockopt的话,那么我们可以随时打开服务器,不用等待2MSL个时间

关于RST分节,

1,对于RST分节,其实是这个样子的,我们打开服务端,客户端,然后关闭服务端(会向客户端发送一个FIN 分节),但是这个时候,我们的客户端是阻塞在fgets函数的,我们从键盘给一个字符串,让其满足fgets函数,执行到write函数,将刚才的字符串输出给服务端,由于刚才的服务端已经终止了并且发送了一个FIN,只是说明不能在发送新的段,并不能说明不能接受,由于此时服务端已经终止,所以上面客户端发送给服务端的信息,也就找不到归宿这个时候(对方进程不存在了),TCP协议栈就会发送一个RST的tcp分节过去。如果这个时候,我们在调用write函数,那么就会产生SIGPIPE,

while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL){

        //      writen(sock, sendbuf, strlen(sendbuf));

                write(sock , sendbuf, 1);      //分两次发送,先发送1个,然后在发送剩余的

                write(sock , sendbuf + 1, strlen(sendbuf) - 1);

                int ret = readline(sock, recvbuf, sizeof(recvbuf));

                if(ret == -1) ERR_EXIT("READline");

                else if(ret == 0) {

                        printf("client close \n");      

                        break;

                }

                fputs(recvbuf, stdout);      //fgets接受到的数据,默认说明是存在换行符的

                memset(sendbuf, 0 , sizeof(sendbuf));

                memset(recvbuf, 0 , sizeof(recvbuf));

        }

第一次write函数(发送字符的时候),对面的进程已经不在了,TCP协议栈会发送一个RST分节,紧接着我们再次调用了write函数,此刻就产生了一个SIGPIPE的信号中断,直接终止当前进程,倘使不退出程序的话

打开相应的客户端,服务端

服务端关闭ctrl+c观察状态:

给客户端一个字符串,满足fgets函数:程序直接退出了,并没有打印client close

所以说,我们上面的分析是合理的。。。。。。

接下来我们修改一下程序:

void handle_sigpipe(int sig){

        printf("recv is a sig = %d\n", sig);    

}

int main(){                                                                                                                                               

        signal(SIGPIPE, handle_sigpipe);

同样的道理,我们来运行一下程序:

这里还能输出:client close,为什么呢???这是因为产生了sigpipe中断信号后,我们对中断信号进行了处理了,所以不会退出程序了

来查看一下这个:sig = 13

上面看啦这么多,我们貌似好像看到了用kill杀死一个进程和CTRL + C,我们来看看区别!!!

同理,打开客户端,服务端

调用CTRL + C,关闭服务器

如果我们:调用kill杀死相应的服务端进程的话!!!

CTRL+C:发送SIGINT信号给前台进程组中的所有进程。常用于终止正在运行的程序,强制中断程序的执行

CTRL+Z:发送SIGTSTP信号给前台进程组中的所有进程,常用于挂起一个进程,是将任务中断,但是此任务并没有结束,它仍然在进程中他只是维持挂起的状态,用户可以使用fg/bg操作继续前台或后台的任务,fg命令重新启动前台被中断的任务,bg命令把被中断的任务放在后台执行

可知,如果我们调用kill的话,那么我们还能观察到对等的状态,如果我们调用CTRL + C的话,那么我们的整个服务端

程序都被中断

总之:上面说了这么多的原因,就是说,一端A调用close退出的话,会发送FIN分节给对端B,但是对于B接收到A的分节之后,并不能保证A端的进程是不是已经消失,,,因为对方调用close,并不意味着对方的进程会消失,,,当然,上面我们是通过kill或者CTRL + C来确保的,如果这时B端再调用write,发现A端不存在,那么TCP协议栈会发送一个RST分节(连接重置的TCP端),对于当前的全双工管道而言,如果再次调用write函数的话,那么就会产生SIGPIPE信号中断。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值