第五章-TCP服务器程序及相应情况处理

本文详细探讨了TCP服务器程序的回射服务、正常启动与终止、信号处理,特别是异常情况如连接终止、服务器进程终止、SIGPIPE信号等问题,以及在不同操作系统环境下处理这些问题的方法。通过对信号处理函数和waitpid的使用,避免了僵死进程的产生。
摘要由CSDN通过智能技术生成

一、回射服务器程序

在这里插入图片描述
tcpserv01.c

#include "unp.h"

int main(int argc, char** argv)
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    
    Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
    
    Listen(listenfd, LISTENQ);
    
    while(1){
        clilen = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);
        
        if((childpid = Fork()) == 0){
            Close(listenfd);
            str_echo(connfd);
            exit(0);
        }   
        Close(connfd);
    }
}

str_echo函数:
str_echo.c

#include "unp.h"
void str_echo(int sockfd)
{
	ssize_t     n;
    char        buf[MAXLINE];

again:
    while ( (n = read(sockfd, buf, MAXLINE)) > 0)
        Writen(sockfd, buf, n);

    if (n < 0 && errno == EINTR)
        goto again;
    else if (n < 0)
        err_sys("str_echo: read error");
}

若客户端关闭连接,则接收到客户端的FIN将导致服务器子进程的read函数返回0,则导致str_echo函数返回,从而终止子进程。

tcpcli01.c

#include "unp.h"

int main(int argc, char** argv)
{
    int sockfd, n;
    struct sockaddr_in servaddr;

    if(argc != 2)
        err_quit("usage: tcpcli <IPadress>");

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));

    str_cli(stdin, sockfd);

    exit(0);
}

str_cli函数:
str_cli.c

void str_cli(FILE *fp, int sockfd)
{
    char    sendline[MAXLINE], recvline[MAXLINE];

    while (Fgets(sendline, MAXLINE, fp) != NULL) {

        Writen(sockfd, sendline, strlen(sendline));

        if (Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");
            
        Fputs(recvline, stdout);
    }
}

fgets函数功能为从指定的流中读取数据,每次读取一行。其原型为:char *fgets(char *str, int n, FILE *stream);从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

二、正常启动与终止

2.1 正常启动

客户端调用socket和connect,connect引起三次握手,客户端收到第二个分组后,connect返回,服务端收到第三个分组后,accept返回。

运行服务器后:
在这里插入图片描述
建立连接后:
在这里插入图片描述
查看进程状态:
在这里插入图片描述

2.2 正常终止

在键入终端EOF字符(Control-D)以终止客户端,此时执行netstat可以发现进入TIME_WAIT状态。

当服务器子进程终止时,会给父进程发送一个SIGCHLD信号,但本程序未捕捉该信号,而该信号的默认行为是被忽略,此时子进程进入僵死状态。
在这里插入图片描述

三、信号处理

3.1 POSIX信号处理

信号就是告知某个进程发生了某个事件的通知,有时也被称为软件中断(software inerrupt)。信号通常是异步发生的。

本书例子为了兼容较老的版本,使用sigaction实现signal接口,来处理信号。

#include    "unp.h"

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);
}

设置SA_RESTART标志:
SA_RESTART标志是可选的。若设置,由相应信号中断的系统调用将由内核自动重启。

关于信号的详细介绍,见另一篇博客:
https://blog.csdn.net/sunximei/article/details/120907554

3.2 处理SIGCHLD信号

设置zombie状态的目的是维护子进程的状态,方便父进程在以后某个时候获取。若进程终止,则该进程的所有僵死子进程的父进程ID将被重置为1(init进程),即init进程将wait它们。

为了避免僵死进程,建立信号处理函数并增加调用
Signal(SIGCHLD, sig_chld);

sigchildwait.c

#include "unp.h"

void sig_chld(int signo)
{
    pid_t pid;
    int   stat;

    pid = wait(&stat);
    printf("child %d teminated\n", pid);
    return;
}

注:在信号处理函数中不宜调用如printf这样的I/O函数,这里只是为了测试

若在Solaris 9下运行程序,则使用的是系统函数库下的signal函数,则没有设置SA_RESTART:
在这里插入图片描述
这样的后果就是,当子进程终止时,会发送SIGCHLD信号给父进程,父进程此时阻塞于accept函数,中断此调用,进入sig_chld函数回收子进程,然后return。
既然该信号是父进程阻塞于慢系统调用(accept)时捕获的,内核就会使accept返回一个EINTR错误,而父进程并未处理该错误,故终止。
若在4.4BSD环境下运行,则内核将重启accept,于是不会返回错误。

为了便于移植,我们必须处理慢系统调用返回EINTR的情况:

修改tcpserv02.c中部分代码

while(1){
	clilen = sizeof(cliaddr);
	if((connfd = accept(listenfd, (*SA)&cliaddr, &clilen)) < 0){
		if(errno == EINTR)
			continue;
		else
			err_sys("accept error");
	}	
}

注:connect函数不能重启,若该函数返回EINTR,不能再次调用它

3.3 wait和waitpid函数

关于wait和waitpid的区别,参照另一篇博客:
https://blog.csdn.net/sunximei/article/details/120826727

假设客户端与服务器建立了5个TCP连接,当客户端终止时,所有打开的描述符(几乎在同一时刻)全部关闭,引发了5个FIN,使得5个服务器子进程几乎在同一时刻终止,递交给父进程5个SIGCHLD信号。

运行结果是只执行了一次sig_chld函数,但5个子进程全部终止,且有4个处于僵死。原因是UNIX信号一般是不排队的,而且这种情况甚至是不确定的,有时候取决于FIN到达服务器主机的时机,还可能会执行2~5次。

正确的解决办法是调用waitpid而不是wait,最终 正确的信号处理函数版本:

#include "unp.h"

void sig_chld(int signo)
{
	pid_t pid;
	int stat;
	while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
		printf("child %d terminated\n", pid);
	return;
}

四、各种异常情况

4.1 accept返回前连接终止

若三次握手完成后,客户端TCP发送了一个RST分组,服务器在调用accept之前收到了收到这个RST:
在这里插入图片描述
注:这里可以看到,在服务端调用accept之前,已经完成三次握手,进入ESTABLISHED,连接进入TCP连接队列。即已完成的连接放入TCP连接队列,accept负责取连接,而取的时机可能在之前(则阻塞)或之后。

如何处理这种中止的连接取决于不同的实现。
Berkly:完全在内核中处理,服务器进程看不到
SVR4:返回一个错误给服务器进程,作为accept的返回结果,错误类型也取决于实现。这些SVR4返回EPROTO,而POSIX指出返回的errno值必须是ECONNABORTED(software caused connection abort),此时应再次调用accept。

4.2 服务器进程终止

启动服务器和客户端,然后杀死服务器子进程,然后查看状态
在这里插入图片描述
发现服务器子进程进入TIME_WAIT状态,客户端进入CLOSE_WAIT状态。

所发生步骤是:
(1)服务端子进程发送FIN,客户端响应ACK。服务端进入FIN_WAIT1,客户端进入CLOSE_WAIT(服务端收到ACK后进入FIN_WAIT2)
(2)处理SIGCHLD信号,子进程终止
(3)问题是此刻客户端阻塞在fgets调用上,TCP收到FIN只是表示不再向其中发送数据而已,但仍可调用相关API尝试写数据。
在这里插入图片描述
此时再键入文本, 客户端调用written,然后立即调用readline:
在这里插入图片描述
当服务器此时收到客户端发来的数据时,由于此套接字已关闭,则响应一个RST。
(4)客户进程看不到这个RST,readine由于接收到之前的FIN,所以立即返回0,由于并未预期收到EOF,则以出错信息“server terminated prematurely”

Writen(sockfd, sendline, strlen(sendline));
if (Readline(sockfd, recvline, MAXLINE) == 0)
    err_quit("str_cli: server terminated prematurely");

本例子的问题在于:当FIN到达套接字时,客户正阻塞在fgets调用上。客户实际上在同时应对两个描述符——套接字和用户输入,而它不应该单独地阻塞在某一个特定源的输入上。select和poll的目的之一就是解决这个问题。正常情况下,在服务器子进程终止时,客户端就立即被告知已收到FIN

4.3 SIGPIPE信号

若一个进程向某个已经收到RST的套接字执行写操作时,内核会向其发送SIGPIPE信号,该信号的默认行为是终止进程。而无论进程是处理该函数还是忽略,写操作都返回EPIPE错误。

也就是说,第一次写一个已经接收了FIN的套接字不成问题,会收到RST,但是收到RST后仍然写,就是一个错误

4.4 服务器主机崩溃

主机崩溃可以用断开网络来模拟,也就是模拟服务器主机不可达的情况(与服务器主机关机区别)。
服务器主机崩溃时,已有的网络连接上不发出任何东西。若此时客户端调用writen和readline后,阻塞在readline等待应答情况下,客户TCP会进入重传模式,直至超时。此时分为两种情况:(1)主机崩溃,完全无响应,则返回错误ETIMEOUT (2)某个中间路由判定主机不可达,从而响应一个“destination unreachable”的ICMP消息,则返回EHOSTUNREACH或ENETUNREACH。

有时我们需要更快地检测出这种情况(通过对readline设置超时),后续讨论。

上述情况是在向服务器主机发送数据时才能检测它已经崩溃,若不通过这种方法,则需要采用另外一个技术——SO_KEEPALIVE套接字选项,以及服务器心博函数。

4.5 服务器主机崩溃后重启

上节提到,崩溃后若客户端不主动发消息则检测不到(未使用SO_KEEPALIVE选项)。服务端重启后,在客户端键入一行文本,再发送至服务端,服务端重启后TCP丢失了崩溃前的所有连接信息,因此会响应RST,此时阻塞在readline的客户端会返回ECONNRESET错误。

4.6 服务器主机关机

Unix系统关机时,init进程通常先给所有进程发送SIGTERM信号(可被捕获),等待一个固定的时间,然后给所有进程发送SIGKILL信号(不可被捕获)。

这么做是为了留给所有运行的进程一小段时间来清除和终止,若我们并未捕获SIGTERM信号并终止,则会被SIGKILL信号终止。当服务器进程终止时,就和之前讨论的进程终止情况相同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值