Unix 网络编程(五)-TCP客户/服务器运行时边界情况初探。

来源:互联网 发布:直播录像软件 编辑:程序博客网 时间:2024/05/26 09:54

写在开头:

在上篇博客中我们介绍了一个完整的,典型的基于TCP的客户服务器模型程序,同时介绍了一个完整的套接字程序所需要的一些API,并且将这些API的功能与TCP协议的建立连接,传递数据,结束连接等过程进行了对应。但是,在运行上一节的程序时我们考虑的都是一些最理想的情况,服务器先启动起来,然后客户端进行连接,然后客户端输入文本行传递给服务器,服务器读到套接字中的文本然后回传给客户端。所有的过程有序的进行。

然而在实际的应用中并不会像之前描述的那样顺利,有时会出现各种边界条件:比如,服务器在连接的过程中出现故障,崩溃后重启、子程序在任务完成之后成为僵死状态,服务器和客户端的字节序不同等问题。在这一节中我们会尽量多的列出这些边界条件情况,然后分析这些情况发生时所处的网络的层次并且如何反应到套接字的API。这里我们使用的原始程序还是上节中介绍的 tcpserv01.c 、lib/str_echo.c 和 tcpcli01.c、lib/str_cli.c,然后为了解决部分问题我们会添加一些内容。下面我们开始从程序的正常启动,正常结束,调用POSIX信号处理,处理僵死进程,服务器异常等几个方面来学习本节的内容;

-----------------------------------------------------------------------------------------------------------------------------------

正常启动,终止

1. 我们先开启一个终端运行服务器程序,然后调用 netstat -a 命令 查看网络连接状态,可以看到有一个本地端口是 9877,且本地地址和外地地址是通配地址的tcp套接字。这个套接字就是我们的服务器创建的套接字,因为我们在服务器Bind的地址结构中的sin_port是默认的9877号端口(SERV_PORT)。 可以发现,服务器套接字正处于LISTEN状态,等待客户的连接。

  1.     servaddr.sin_port        = htons(SERV_PORT);  
  2.     Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));  


2. 然后我们启动客户端程序,先不输入文本,之后再用 netstat -a 命令查看现在的网络连接情况:

# ./tcpcli01 127.0.0.1


这时候发现有三个tcp连接和端口号9877相关。可以发现,1 号连接是服务器的父进程继续处于LISTEN状态,2 号连接是服务器的子进程处于ESTABLISHED状态用来负责和3 客户端进程进行通信。这时候我们在客户端输入数据,发现通信过程建立,这个时候我们调用 ps -e 命令查看当前的进程情况,可以发现和我们刚才说的情况一样。


3. 这个时候我们用<Ctrl + D> 来终止客户端程序,子进程会调用exit (0) 终结自己,这个时候再查看网络连接以及进程情况:


可以发现刚才的子进程的连接已经不存在了,而且客户端目前处于TIME_WAIT状态等待 终结。再看看这里的进程,发现子进程变成了<defunct> 僵死进程。


而这个进程是我们在服务器端没有捕获子进程在终止时发给父进程的SIGCHLD的结果,因为僵死进程会占用资源,所以我们下面讨论用Unix信号处理的方法来结束僵死进程。

POSIX信号处理

 这里我们简单的介绍一下Unix系统关于信号(Signal)的一些知识,希望对我们编写处理僵死进程等捕获信号的网络程序有帮助。

信号(signal)是一种软件中断(software interrupt),用来通知进程发生了某个事件,这往往是异步发生的,可由一个进程发给另一个进程也可由内核发送给进程。每一个信号都关联着一个对此信号的的处置(disposition),通过调用 sigaction 函数可以完成对信号的处置,这里有三种处置,分别是:捕获、忽略、以及默认。对于捕获信号而言,通过调用sigaction函数并指定信号发生时所调用的函数即可完成操作,其中SIGKILL 和 SIGSTOP两个函数不能被捕获,同时也不能被忽略。忽略一个信号只要将对信号的处置设定为SIG_IGN就可以了。将对信号的处置设为SIG_DFL即可完成对信号的默认处置,默认处置通常是终止进程,但是也有默认处置是忽略信号,比如我们下面要讲的 SIGCHLD。

对于sigaction函数的调用略显复杂,因为sigaction函数的原型是:

int sigaction (int signo, const struct sigaction *act, struct  sigaction *oact);

这里的sigaction 结构体的变量需要我们自己来完善,所以为了简便可以调用signal函数其函数原型是:

typedef   void   (*sighandler_t)(int);sighandler_t     _bsd_signal ( int  sig, sighandler_t   handler) ; 

为了实现向后兼容,这里给出了一个《Unix 网络编程》作者自己编写的一个signal 函数。如下所示:

Sigfunc *signal(int signo, Sigfunc *func){    struct sigaction    act, oact;    act.sa_handler = func;    sigemptyset(&act.sa_mask);    act.sa_flags = 0;    if (signo == SIGALRM) {#ifdef  SA_INTERRUPT        act.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */#endif    } else {#ifdef  SA_RESTART                                                                             act.sa_flags |= SA_RESTART;     /* SVR4, 44BSD */#endif    }       if (sigaction(signo, &act, &oact) < 0)        return(SIG_ERR);    return(oact.sa_handler);}/* end signal */Sigfunc *Signal(int signo, Sigfunc *func)    /* for our signal() function */{    Sigfunc *sigfunc;    if ( (sigfunc = signal(signo, func)) == SIG_ERR)        err_sys("signal error");    return(sigfunc);}
line 6中 sigaction 的 sa_handler 成员被设置为func 函数,line 7对sa_mask进行设置可以实现对某一些信号的阻塞,而这里设置成为空表明在处理此信号期间如果有别的信号过来,并不阻塞。当然相同的信号是肯定会被阻塞的。line9-17判断信号的类型并对sa_flags进行设置。SA_RESTART标志用来判断是不是需要重启被信号中断的系统调用,而这里面对SIGALAM信号进行特殊处理因为我们不希望这种信号的系统调用也要重启。line18-20调用sigaction函数。这里的Signal函数是signal的包裹函数用来处理一些错误。以上大概就是调用signal 函数的整个工作流程。这里需要再次强调的是,unix信号默认是不排队的,如果一个信号在阻塞期间被调用了多次,那么该信号会被解阻塞之后也只是调用一次。

处理SIGCHLD信号

SIGCHLD 信号是子进程终止时向父进程发送的信号,按照默认的处置该信号是被父进程忽略的,但是如果被忽略那么这个进程就变成了僵死状态,直到父进程结束这些僵死进程会被Init进程处理掉。但是我们并不希望保存僵死进程,因为他们仍然占用内核中的空间。下面我们编写函数捕获SIGCHLD信号,然后杀死这些进程。

这里我们在tcpserv01.c Listen(listenfd, LISTENQ);  之后加上 Signal(SIGCHLD, sig_chld)  的调用 , 其中sig_chld是我们自己定义的:

#include    "unp.h"                                                                                      voidsig_chld(int signo){    pid_t   pid;    int     stat;    pid = wait(&stat);    printf("child %d terminated\n", pid);    return;}
这时候我们重新运行模型,在客户终断的时候发生如下:


这里当我们在客户端键入终止符的时候子进程终止,向父进程发送SIGCHLD信号,然后Signal函数捕捉到之后进行处理。这个时候我们再用ps -e 查看就不会发现僵死进程。这

里我们调用了wait 函数来处理子进程,同时waitpid也具有相同的功能,下面我们详细来讲解这个函数。

wait 和 waitpid 函数

wait和waitpid两个函数都用来处理子进程终止时向父进程发送的SIGCHLD信号。其函数原型是:

#include <sys/wait.h>pid_t  wait(int *statloc);pid_t  waitpid( pid_t  pid, int  *statloc, int options);   /*返回值,若成功时返回进程ID,若出错则为0或-1*/

这两个函数的返回值都有两个一个是通过 return 返回的处理的子进程的进程id,一个是statloc指针指向的子进程终止状态。这个状态用来表示进程是正常终止,还是由某个信号杀死,还是由作业控制停止等。

 如果调用wait 的进程 (这里就是父进程),没有已终止的子进程,但是有一个或多个子进程正在执行,那么wait将阻塞一直到有一个子进程终止,然后去处理。

waitpid 因为有三个参数,所以给了我们更多的控制选择。其中pid 可以用来选择我们想等待的进程id,如果 id > 0 ,那么waitpid等待的进程id 就是pid, 如果id = -1 ,表示等待任何一个进程id,也即第一个结束的子进程。第三个参数 options 用来选择附加项,其值一般为WNOHANG它告知waitpid 在尚有未终止的子进程在运行时不要阻塞,

而这也是wait 和 waitpid 的一个很大区别,即 waitpid 可以循环的判断是否有新的进程阻塞并进行处理,而wait 只能处理一个信号。以上就是我们分析的网络编程中对SIGCHLD信号进行捕获处理的过程。下面我们分析一下服务器几种异常情况的发生。

服务器进程终止

在这个情形之下,客户端和服务器已建立连接并已通信,这个时候如果服务器的子进程被杀死,而客户端又向服务器发送消息请求,会发生什么呢?

因为虽然客户TCP接收来自服务器TCP的FIN并相应以一个ACK,但是在之前的例子中客户进程会阻塞在fgets调用上,从而忽略了FIN。这时候如果客户端将文本送往服务器端是允许的,只是服务器在收到数据之后由于先前打开的套接字进程已经终止了所以会向客户端发送一个RST。所以从这里可以看出,当服务器子进程被杀死时,客户端进程并没有及时的读取到FIN而是阻塞在用户输入上,为了让客户端立即被告知已收到FIN,客户端需要阻塞在任何一个源的输入上。后面的select 和 poll 函数会被设计用来解决这个问题。

SIGPIPE信号在一个SOCK_STREAM类型的套接字不在连接时,如果再往这个套接字里写信息发生。所以,如果客户端在收到服务器发送的RST之后仍然往套接字里面写信息就会引发错误,SIGPIPE默认的处理方式是SIG_IGN,并假设后续的处理是默认终止的。

服务器主机崩溃

这里面的情形是服务器和客户端已经建立连接,这个时候将服务器从网络断开,如果此时客户端再向服务器发送数据那么就不会得到服务器的应答。此时,客户TCP会持续重传数据分节,在重传一定的次数之后这里是(12次大约9分钟)后内核会给客户进程返回一个错误。

而如果是主机关机之后重启,那么即使恢复了网络而之前的TCP连接也都已经丢失,所以如果服务器接受到客户的数据分节响应那么会发送RST消息给客户端,客户端收到RST之后会返回一个错误,如果这里我们不希望非要等很长时间或直到服务器返回RST客户端才检测到服务器的异常,那么我们需要使用SO_KEEPALIVE套接字选项或某些客户/服务器心跳函数等来检测; 

----------------------------------------------------------------------------------------------------------------------------------------------

总结:

以上就是本篇文章的内容,介绍了TCP客户服务器模型运行之后的一些状态,包括正常开启和结束,僵死进程的处理,服务器主机终止和服务器崩溃时发生的一些异常等。

2015/02/06 于南京CSDN 如需转载请标明出处谢谢  http://blog.csdn.net/michael_kong_nju/article/details/43535507

0 0
原创粉丝点击