本文转自:http://edsionte.com/techblog/archives/2934
在基于TCP的客户/服务器模型中,服务器和客户程序之间通过一系列的socket接口形成TCP连接。服务器启动后的首要工作就是创建监听套接字,监听套接字是通过socket、bind和listen三个函数完成的。在这之后,监听套接字通过accept函数一直处于阻塞状态,等待客户程序的连接请求。客户程序在连接至服务器之前,必须先创建自己的套接字,再通过connect函数连接到服务器。如果一切顺利,那么服务器和客户之间就形成了一条TCP连接。
上述TCP连接的过程是从编程角度触发,接下来我们将从网络通信的角度分析TCP的建立和终止过程,并且结合相关的socket函数接口分析其建立和终止过程中客户程序和服务器状态的变化。
1.TCP的建立TCP连接建立过程即通常我们所说的“三路握手”。整个连接过程是由客户端的connect函数发起的,其具体的步骤如下:
1.客户端向服务器发送一个SYN(同步)报文段,这个报文段包含了客户端将要发送数据的初始序列号,假设该序号为J。该SYN段为整个TCP连接过程的报文段1。
2.服务器向客户端发送一个SYN段作为对客户端的应答,该报文段包含服务器在此连接中将要发送数据的起始序列号K。该SYN段为整个TCP连接过程的报文段2;在该报文段中还包含服务器对客户端SYN段的确认(ACK),该确认包含的序号为J+1,表明服务器期望客户端下次发送的数据序号是从J+1开始的。也就是说报文段2包含服务器发送的SYN和服务器对客户SYN的ACK。
3.客户端向服务器发送ACK以对服务器SYN进行确认,该报文段的序号为K+1。这是报文段3。
由于服务器在接收到客户端的TCP连接请求之前调用了accpet函数,所以一直处于阻塞状态。当客户通过connect函数向服务器发送第一个SYN段时,它执行了主动打开TCP连接这个动作。而接受这个SYN的服务器则执行被动打开动作,它向客户发送SYN以及ACK确认。
接下来我们以上文所举例的回射客户/服务器程序为例,说明TCP连接过程中客户端和服务器的状态变化。
1.从后台启动服务器程序。通过netstat命令我们可以看到当前服务器处于监听状态,因此此刻还没有任何客户请求。
在本地地址一栏中,IP地址为是因为我们在绑定(bind)服务器套接字时使用了INADDR_ANY参数。由于此刻没有任何客户请求,所以外来地址处均为。
2。运行客户程序,TCP连接建立。第一条信息为主服务器进程,它此刻仍然处于监听状态,等待其他客户程序的连接请求;第二条信息为子服务器进程,它此刻已经建立了TCP连接,本地端口为6666,而外来连接端口为54260。第三条信息为客户客户进程,他此刻也处于已建立连接的状态,本地端口为54260,而外来端口正好为6666。
此刻,我们在客户端并没有输入任何数据,因此此刻主服务器进程、子服务器进程和客户进程均处于阻塞状态,但是其阻塞原因却不同。主服务器进程是因为等待(accept)其他客户请求而阻塞;子服务器进程是因为等待(read())客户进程发送的数据而阻塞;客户进程则是因为等待从标准输入读取(fgets())数据而阻塞。我们可以进一步使用ps命令查看三个进程之间的状态。如下:
对ps使用-t选项可以查看指定伪终端下的进程,而通过-o选项则可以输入指定的进程状态信息,WCHAN参数可以进程睡眠的原因。ps的输出结果和我们上面的分析的结果一致,三个进程确实处于睡眠状态(S)。当进程阻塞于accept或connect时,其睡眠条件为inet_csk_wait_for_connect;当进程阻塞于套接字输入或输出时,其睡眠条件为sk_wait_data;当进程阻塞于终端的读操作时,其睡眠条件为n_tty_read。
2.TCP的终止由于TCP连接是全双工的,因此必须分别关闭两个方向上的连接。通常一个TCP连接的终止需要4个分节。整个TCP的终止过程通常是由客户端的close函数发起的。
1.客户端进程调用close函数关闭套接字,它执行了主动关闭TCP连接,该端TCP向服务器发送一个FIN报文段(报文段1),表示该端的数据已经发送完毕。假设该报文段的序号为M。
2.服务器接收到这个FIN段后执行被动关闭。为了确认受到这个FIN,服务器向客户端发送一个ACK(报文段2),该确认报文的序号为M+1。服务器接收了客户端发送来的FIN说明服务器在该TCP连接上再无数据可接收。
3.服务器程序调用close关闭套接字,这将导致服务器向客户端发送一个FIN报文(报文段3),假设该报文的序号为N。
4.客户端为了确认服务器发送来的FIN报文,它向服务器发送一个ACK(报文4),该报文的序号为N+1。
上述所提及的关闭套接字有时候并不是只能通过close函数来实现。客户进程不论是正常终止(通过exit或者从主函数中返回)还是通过某个信号而终止时,都会关闭所有已经打开的描述符,这里当然包括套接字描述符。
当用户在标准输入端输入EOF时(Contrl+D),客户正常退出而关闭套接字,紧接着就会发生上述的TCP终止过程。通过netstat命令可以看到当前TCP的连接状态。
当前连接的客户端进入了TIME_WAIT状态,而服务器仍然在等待其他客户的连接。
我们继续使用ps可以查看到当前客户和服务器进程的状态。
可以看到客户进程已经退出,主服务器进程由于等待其他客户的连接而处于睡眠状态,子服务器进程虽然已经停止了服务,但是仍然处于“阴魂不散”的僵死状态。这是由于主服务器进程并未对子服务器进程发出的SIGCHLD信号进行捕获,具体的捕获方法在这里的文章已经说明。