linux网络编程之socket编程(七)

今天继续学习socket编程,北京在持续几天的雾霾天之后久违的太阳终于出来了,心情也特别特别的好,于是乎,在这美好的夜晚,该干点啥事吧,那当然就是继续坚持我的程序学习喽,闲话不多说,进入正题:

通过这个状态的学习,进一步复习一下“连接建立三次握手、连接终止四次握手【下面会分别来介绍】”,下面首先来看一张图:

从图中可以数一下,总共有“LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、CLOSE_WAIT、FIN_WAIT_2、LAST_ACK、TIME_WAIT、CLOSED”十个状态,那为啥标题上说有十一个呢?其实还有一个状态叫CLOSING,这个状态产生的原因比较特珠,我们之后再来看它,下面先来分别梳理一下连接建立三次握手和连接终止四次握手状态流程:

连接建立三次握手:

LISTEN:

首先服务端创建一个socket,这时它的状态实际上是CLOSED状态,也就是最后一种状态,虽然没有标识出来:

一旦我们调用bind、listen函数:

这时就处于LISTEN状态,如下:

这时候的套接口就称为被动套接口,这意味着这个套接口不能用于发起连接,只能用来接受连接,这个之前都有介绍,

而这时回到客户端来说:

SYN_SENT:

当创建套接口时,也是一个CLOSED状态,这里也未标明,接着再调用connect进行主动打开,这时的套接口就称为主动套接口,它可以用来发起连接的

这时的状态为SYN_SENT,这时TCP会传输一个发起连接的TCP段"SYN a"这个段给服务器端,如下:

而此时服务端调用了accept方法处理阻塞的状态:

但是TCP协议栈会收到“SYN a”TCP段,这时就处于SYN_RCVD状态:

当收到SYN_RCVD之后,TCP会对序号“SYN a”进行确认,发起一个"ACK a+1"TCP段给客户端,并且也有一个"SYN b"序号,如下:

对于客户端来说,收到"ACK a+1"TCP段之后,就处于"ESTABLISHED"连接的状态,这时connect函数就能够返回;

而对于服务端来说并未处理连接的状态,它需要等到客户端再次发送"ACK b+1"TCP段,这就是连接建立的三次握手,服务端收到这个TCP段之后,则也会处于"ESTABLISHED"状态,如下:

它实际上会将未连接队列当中的一个条目移至已连接队列当中,这时accept就可以返回了,因为它可以从已连接队列的队头返回第一个连接,如下:

 

连接终止四次握手:

当客户端发起关闭请求,这时会向服务器端发起一个"FIN x ACK y"的TCP段给对方,这时客户端的状态就叫作FIN_WAIT_1,如下:

这时服务器端收到一个终止的TCP段,这时read就会返回为0,实际上当服务端收到这个TCP段之后,服务端会对它进行确认,则会向客户端发送"ACK+1"的TCP段,这时服务端的状态为CLOSE_WAIT:

客户端的状态为FIN_WAIT_2:

之后服务端也可以选择发起一个终止的FIN TCP段给客户端,调用close,这时候就处于等待对方的最后一个确认的状态,称为LAST_ACK:

这时候,客户端就处于TIME_WAIT状态:

注意:这个状态要保留2倍的MSL(tcp最大的生命期)时间,为什么呢,这个可以简单说明一下,是由于最后一个"ACK y+1"发送过去,不能确定对方收到了,这个ACK可能会丢失,有了这个时间的存在就确保了可以重传ACK,当然还有其它的原因,这里先了解一下既可,当服务器收到了最后一个确认以后,则就处于CLOSED状态了:

注意:服务端处于CLOSED状态,并不代表发送关闭的这一端(就是客户端)就处于CLOSED状态,需等到2倍的MSL时间消失以后才处理CLOSED状态。

以上是TCP的十点状态,还有一个特珠状态叫CLOSING,它产生的原因是:双方同时关闭,如下图:

具体流程是这样的:客户端和服务端同时调用close,这时客户端和服务端都处于FIN_WAIT_1状态

这时,双方都会发起FIN TCP段,

这时需要对其进行段确认:

这时状态则称为CLOSING状态,这时就不会进行到FIN_WAIT_2这种状态了。

一旦收到对方的ACK,则会处于TIME_WAIT状态:

可见TIME_WAIT状态是主动关闭的一方才产生的状态。

说了这么多理论,下面用代码来进行论证,以便加强理解,还是用之前的服务端/客户端回显的例子,首先启动服务端,这时查看下状态:

接着启动一个客户端,发起连接:

由于目前做实验是在同一台机器上进行的,所以打印了三个状态,实际上应该是服务端的状态和客户端的状态是分开的。

【注意】:由于在运行时这两个状态SYN_SENT、SYN_RCVD过快,所以看不到。

下面来看下连接终止的状态,先关闭服务端,首先找到服务端的进程,通过kill掉的办法来关闭服务端:

杀掉服务端进程来模拟服务端的close:

【注意】:这里的服务端进程是指与客户端通讯的进程。

这时查看一下状态:

为什么不会处于TIME_WAIT状态呢?

原因在于,read函数没有机会返回0:

这时应该查看一下客户端的程序才知道问题,客户端此时是阻塞在fgets函数来键盘的消息:

这就意味着客户端这个进程没有机会调用close,所以服务器端无法进入TIME_WAIT,它只能保留在FIN_WAIT_2状态了

这时候再来看一下状态:

只有LISTEN状态了,这是为什么呢?

还是得回到客户端的程序来分析,由于从键盘敲入了字符,所以:

这时就会走如下流程:

而由于服务器的进程已经杀掉了,所以说不会显示TIME_WAIT状态了。

【注意】:该实验在最后会阐述一个SIGPIPE的信号问题。

如果先关闭客户端,这时就会看到这个状态了,如下:

另外这个状态上面也提到了,会保留2倍的MSL时间才会消失,如果服务器端保留两倍的MSL时间,这时候就会导致服务器端无法重新启动,如果没有调用SO_REUSEADDR话(关于这个具体可以参考博文:http://www.cnblogs.com/webor2006/p/3932917.html),以上就是TCP的十一种状态的学习。

 

SIGPIPE信号产生的原因:

对于上面做的一个实验,就是服务端先关闭之后,然后客户端还可以向服务端发起数据,这是由于客户端收到FIN仅仅代表服务端不能发送数据了,如下图:

而如果发送数据给对方,但是对方进程又已经不存在,会导致对方发送一个RST重启TCP段给发送方(这里指的就是上面做实验的客户端),但是在收到RST段之后,如果再调用write就会产生SIGPIPE信号,而产生该信号默认就会终止程序,下面来修改一下客户端的代码,还是基于上面的实验,如下:

这时,再来看一下效果:

首先运行客户端与服务端:

然后将服务端与客户端的进程找到,并杀掉来模拟关闭服务端:

然后这时在客户端中敲入字符,并回车,看下结果:

结合代码来看一下:

 

为了证明确实是收到了SIGPIPE信号,我们捕捉一下该信号,修改代码如下:

再次编译运行,这一次运行步骤还跟上次一样,需要先杀掉父进程,然后再在客户端敲入一个字符,这里就不说明了,只看一下结果:

实际上,对于这个信号的处理我们通常忽略即可,可以加入这条语句:signal(SIGPIPE, SIG_IGN);

修改代码如下:

实际上,对于SIGPIPE信号在学习管道时有说过它的产生,如果没有任何读端进程,然后往管道当中写入数据,这时候就会出现段开的管道,而对于TCP我们可以看成是一个全双工的管道,当某一端收到FIN之后,它并不能确认对等方的进程是否已经消失了,因为对方调用了close并不意味着对进程就已经消失了,用图来理解:

这时候,就需要客户调用一次write,这次并不会产生断开的管道,发现对等方的进程不存在了,则对等方就会发送RST段给客户端,这就意味着全双工管道的读端进程不存在了,所以说如果再次调用write,就会导致SIGPIPE信号的产生,所以说可以利用管道来理解它。

好了,这节的学习有些难懂,需要多想,多做实验,下节见~

posted on 2014-10-13 21:31  cexo  阅读(643)  评论(0编辑  收藏  举报

导航