本章中演示的是一个基于TCP的client/server程序示例。本文记录了在编程中一些需要注意的细节问题。
1.c/s正常启动
当c/s都启动后,3个进程(server父子进程,client进程)都处于阻塞(睡眠)状态:
server父进程阻塞在accept()
server子进程阻塞在read()
client进程阻塞在fgets()
在client连接server过程中,注意三次握手:三次握手是由client的connet()发出第一个分节,server收到后,会回复第二个分节,这时connect()函数才返回,而server直到收到第三个分节才返回。
可以用
netstat -a
ps -t
命令来查看c/s进程的执行状态。
2.client正常终止
client进程结束,导致client TCP发送一个FIN给server,server则以ACK响应,这就是四次挥手的前本部分。此时,server套接字处于CLOSE_WAIT状态,client套接字处于FIN_WAIT_2状态。
当server收到FIN时,导致子进程终止,那么子进程打开的所有文件描述符也随之关闭,在这里会引发TCP FIN的发送,client则响应ACK,四次挥手的后半部分也结束了,至此server/client都进入TIME_WAIT状态。
这里需要注意的是,server子进程终止时,会给server父进程发送一个SIGCHLD信号,该信号的默认行为是被忽略,如果父进程未处理,那子进程就会进入僵死状态,我们必须处理僵死进程,因为它会占用系统资源。
3.POSIX信号
信号通常是异步发生的,可以由一个进程发给另一个进程,也可以由内核发给某个进程,我们可以设置信号处理函数来捕捉某些信号,但有两个信号不能被捕捉,即SIGKILL和SIGSTOP。
信号处理函数的原型是:
void handler(int signo);
可以通过signal()来设置,比如:
singal(SIGCHLD, handler);
4.wait()/waitpid()
两个函数都可以用来处理已终止的进程,但用法有区别:
wait()可以用来等待单个子进程结束,当由多个子进程时,它将阻塞直到第一个子进程终止为止。
void sigChild(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
return;
}
waitpid()通过指定附加选项来对子进程进行更多的控制,最常用的参数是WNOHANG,它告诉内核在没有已终止子进程时不要阻塞。
void sigChild(int signo)
{
pid_t pid;
int stat;
while ((pid=waitpid(-1, &stat, WNOHANG)) > 0) ;
return;
}
5.处理SIGCHLD信号
如果我们忽略SIGCHLD信号,那僵死的子进程在父进程结束后会被交给init进程去处理,但在服务器中,程序通常是一直运行的,需要手动处理僵死进程。那就需要捕获SIGCHLD信号了:
singal(SIGCHLD, sigChild);
慢系统调用是指那些可能会永远阻塞的系统调用,多数网络支持函数都属于此,慢系统调用基本规则是:当阻塞于某个慢系统调用的进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
因此,当server父进程阻塞于accept()时,在sigChild()返回时,内核就会使accept()可能返回一个EINTR错误,如果server父进程不处理EINTR错误,就会中止。可以这么处理:
if ((connFd=accept()) < 0)
{
if (errno == EINTR)
continue;
else
err_sys("accept error")
}
6.accept返回前连接中止
当三次握手完成,连接建立后,client发送了一个RST,在server看来,该连接已经由TCP排队,正在等待server进程调用accept()的时候RST到达了,稍后,server进程调用accept()。此种情况在不同系统中有不同的处理,大多数系统会返回一个错误给server进程,POSIX下的errno为ECONNABORTED。
如何模拟这种情况呢?
server调用listen()后,sleep一小段时间,此时启动client,一旦connect()返回,就设置SO_LINGER选项,就可以产生RST。
7.服务器进程终止
先模拟这种情况: c/s正常启动连接后,killserver子进程,作为进程终止处理的部分工作,子进程中所有打开的文件描述符都被关闭,这导致向client发送一个FIN,client响应ACK,四次挥手前半部分正常进行。注意这里,client收到FIN只是表示server进程关闭了连接的服务器端,不再往其中发送任何数据而已,因此client还不知道server已经终止了。此时client继续向连接套接字中写数据,server接收到数据时,发现该进程已终止,于是响应一个RST,如果client阻塞在read()函数时,会正确处理RST反馈,client会有错误信息“server terminated prematurely”;但是当client没有阻塞在read()函数时,client对此毫不知情。鉴于此,select和poll函数的作用就在此,当前进程立即检测到所持有的套接字状态。
8.SIGPIPE信号
当一个进程向某个已收到RST的套接字上进行写操作时,内核向该进程发送SIGPIPE信号,该信号默认行为是终止进程,因此进程必须捕获这个信号,以免意外地被终止。
正确的做法时捕捉后,对该信号进行忽略。
signal(SIGPIPE, SIG_IGN)
9.服务器主机崩溃
c/s正常启动连接后,主机崩溃,已有的网络连接上不发出任何东西,client会阻塞于write(),会启动重传机制,当尝试重传失败后放弃时,client进程会返回一个错误,如果主机崩溃,对client连接无响应,错误是ETIMEOUT;如果中间路由判定主机不可达,则错误是EHOSTUNREACH。
如果想让client自己发现连接断开了,则可以用SO_KEEPLIVE选项
10.服务器主机崩溃后重启
c/s正常启动连接后,主机崩溃重启,此时client发送数据给server,由于server重启丢失了所有连接信息,因此回复client RST。
11.服务器主机关机
关机操作时,OS会给仍在运行的进程发送SIGKILL信号,这样就给进程一小段时间来清除和终止,当服务器进程终止时,所有打开的文件描述符也被关闭,之后情况和7一样。