TCP客户/服务器运行时边界情况初探。

写在开头:

在上篇博客中我们介绍了一个完整的,典型的基于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状态,等待客户的连接。

servaddr.sin_port=htons(SERV_PORT); 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_INTERRUPTact.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */#endif} else {#ifdef SA_RESTARTact.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信号微风吹过,海面上金光闪闪,泛起一道道美丽的浪花,

TCP客户/服务器运行时边界情况初探。

相关文章:

你感兴趣的文章:

标签云: