文章目录
一、回射服务器程序
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信号终止。当服务器进程终止时,就和之前讨论的进程终止情况相同。