TCP中的三次握手,四次挥手是我们所熟知的,可是,我们熟悉里面的各种状态吗???
(SYN_SENT, ESTABLISHED, CLOSE_WAIT.............),试问一句,我们了解里面的状态转化吗???
1,大家先看一个简单的通信图(图片转载与:UNIX网络编程,page:36,图2-5)
可以很明显的看到,在通信双方,客户端,服务端的状态变化过程
有人可能会说:我们上面不是说,有11中状态吗??为什么到啦这里变成了只有10中
(1,(主动打开:SYN_SENT) 2,ESTABLISHED 3,(主动关闭:FIN_WAIT_1) 4,FIN_WAIT_2
5,TIME_WAIT 6,SYN_RCVD 7,CLOSE_WAIT(被动关闭) 8,LAST_ACK 9,CLOSED
10,LISTEN)
为什么不是11个呢???
哈哈,其实还有一种状态叫做:CLOSING(这个状态产生的原因比较特殊,后面分析)
接下来我们分析一下,这些状态的变化过程,,,
主动套接口:用来发起连接 被动套接口:用来接受连接
1,对于服务器端来说:
当调用socket函数创建一个套接字时,状态是CLOSED,它被假设为一个主动套接字,也就是说,它是一个
将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转化成一个被动套接字,指示内核
应接受指向该套接字的连接请求。结合TCP的状态转化图:
调用listen函数导致套接字从:CLOSED状态转化为:LISTEN状态
2,对于客户端来说:
调用socket函数创建一个套接口时,状态也是CLOSED,同样的,它也被假设为一个主动套接字,紧接着,调
用connect主动打开套接口,并且一直阻塞着,等待三次握手的完成,我们把这个状态称之为:主动套接口。
当客户端发起了三次握手的第一次(SYN J,MSS = 536)的时候,套接口的状态变成了:
SYN_SENT(主动打开)
3,对于服务器端而言,调用了listen之后,然后状态就变成了LISTEN状态,接着调用accept函数,使自身一直
保持阻塞的状态,直到三次握手的第一次来到(来自TCP协议栈的TCP的第一个分节),即接收到(SYN J,
MSS = 536),此刻状态由:LISTEN转变为SYN_RCVD
4,对于客户端来说,刚才发送了TCP协议栈中TCP三次握手的第一个分节,此刻应该接受来自服务器发送过来的
TCP三次握手的第二个分节,这时服务器发送过来:(SYN K, ACK J+1, MSS = 1460),此刻,服务器
的状态不变,还是SYN_RCVD,然后,客户端接受服务器发送过来的TCP三次握手的第二次分节,此刻状态
由之前的:SYN_SENT转变为ESTABLISHED,(客户端已经建立完成),这时,connect函数返回
5,然后客户端保持ESTABLISHED状态,并且发出TCP协议栈中TCP三次握手的第三个分节(ACK K+1)
服务端的状态由:SYN_RCVD转变为:ESTABLISHED,从未完成的队列中取出队首的第一个连接放在已完成
队列,这样accept函数就会返回。
此刻,两者都建立完成,这个时候可以完成通信了
6,那么接下来就是连接终止的四次握手,,,
当双方都变成ESTABLISHED状态之后,双方就可以通信了,在双方通信的过程中,由于状态都没有变化,
所以这里,我们暂且不讨论。在通信的时候呢,双方都可以主动发起关闭,那么:我们假定客户端发起一个
关闭请求(调用close函数):会向服务端发送一个TCP分节(TCP协议栈中四次握手的的第一个分节:
FIN M),然后客户端的状态会变成:FIN_WAIT_1(主动关闭),此刻,服务端接收到这个TCP分节后,
并且会对刚才发过来的连接进行确认(ACK M+1),,服务端的状态会变成 CLOSE_WAIT(被动关
闭),当,客户端接收到这个确认之后(ACK M+1),客户端的状态转变
为:FIN_WAIT_2 , 只有当服务端的read函数返回为0的时候,服务端才需要,也是才可以发起关闭请求(FIN
N),发送完成之后,就变成了:
LAST_ACK, 当客户端接受到了这个关闭请求之后,状态会变成了:TIME_WAIT(会经过
2MSL(TCP报文端最大生存周期的两倍时间)之后,转变为:CLOSED),紧接着客户端会发送
最后一次确认:(ACK N+1),等到服务端接收到这个确认后,服务端的状态会变成:CLOSED
关于CLOSING:
该状态产生的原因是:对于客户端和服务端而言,两者同时关闭的情况(这种情况并不多见),如下图:
、 两者同时关闭,后状态同时变成了FIN_WAIT_1,然后当另外一端接收到关闭分节后,状态同时变成CLOSING,然后都对刚才那个分节进行确认,当对端收到之后,两者又都变成了TIME_WAIT,
所以说:在关闭的过程中,不一定可以必须要经过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连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。。。。
大家可以过来看看!!!
当我们仅仅打开服务端之后(端口号为5188),我们来看看所处的状态。
打开服务端:
调用命令查看所有的网络状态:netstat
然后,我们通过命令:摘取有关tcp的状态:netstat -an |grep tcp
紧接着为了删减出有效的信息,我们只需要tcp协议,5188这个端口,我们可以这样做:
netstat -an|grep tcp|grep 5188
嗯嗯,此刻,可以看到,我们这里的状态是处于LISTEN,调用的accept函数还是在阻塞着,等待着返回。
这时,我们再次打开客户端,继续观察一下状态:
然后,我们继续调用之前的命令:
netstat -an|grep tcp|grep 5188
当客户端一打开,那么就完成了TCP的建立,这里,我们可以看到有两个是:ESTABLISHED
其中第二行的42555表示的是客户端所打开的端口,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状态,,,
同样的,这里,我们也需要查看一下echoserv具体的实现:
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,会继续发送一个fin分节,,所以会出现后面的TIME_WAIT状态啦,,,
我们的服务器端会处于TIME_WAIT状态,这时如果我们继续打开服务器会出现:地址占用,
我们可以随时打开服务器,不用等待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)); }
协议栈会发送一个RST分节,紧接着我们再次调用了write函数,此刻就产生了一个SIGPIPE的信号中断,直接终止当
前进程,倘使不退出程序的话,那么read会返回0(readline中封装着read),所以ret等于0,应该会打印client close
(打开相应的客户端,服务端)
观察状态:
服务端关闭:
观察状态:
给客户端一个字符串,满足fgets函数
程序直接退出了,所以看得出来,并没有打印client close
所以说,我们上面的分析是合理的。。。。。。
接下来我们修改一下程序:
<span style="color:#000000;">void handle_sigpipe(int sig) { printf("recv is a sig = %d\n", sig); } int main() { signal(SIGPIPE, handle_sigpipe); int sock; if((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket");</span>同样的道理,我们来运行一下程序:
这里还能输出:client close,为什么呢???这是因为产生了sigpipe中断信号后,我们对中断信号进行了处理了,所以不会退出程序了
同样的,我们来查看一下这个:sig = 13
可以看到,这里的正是sigpipe信号
上面看啦这么多,我们貌似好像看到了用kill杀死一个进程和CTRL + C,我们来看看区别!!!
同理,打开客户端,服务端
查看状态:
调用CTRL + C,关闭服务器
紧接着,我们再来看看状态:
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信号中断。。。。。。。。。。