首先是一个TCP客户/服务器编程的例子
客户端
#include "unp.h"
void cli_echo(FILE* fp, int fd);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
{
err_quit("usage:echotcp <IPADDRESS>");
}
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));
cli_echo(stdin, sockfd);
exit(0);
}
void cli_echo(FILE* fp, int fd)
{
int nread;
char sendline[MAXLINE],recvline[MAXLINE];
while ( Fgets(sendline,MAXLINE,fp) != NULL )
{
Writen(fd,sendline,strlen(sendline));
if ( Readline(fd,recvline,MAXLINE)==0 )
err_quit("error:server terminated!");
Fputs(recvline,stdout);
}
}
服务器端
#include "unp.h"
void serv_echo(int clifd);
void sig_child(int signo);
int main(int argc, char **argv)
{
int listenfd,clifd;
struct sockaddr_in cliaddr,servaddr;
socklen_t clilen;
pid_t childpid;
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);
signal(SIGCHLD,sig_child);
for(;;)
{
clilen = sizeof(cliaddr);
if ((clifd = Accept(listenfd,(SA*)&cliaddr,&clilen)) < 0 )
{
if (errno == EINTR)
continue;
else
err_sys("accept error");
}
if ( (childpid=fork()) == 0 )
{
Close(listenfd);
printf("child process %d\n", getpid());
serv_echo(clifd);
exit(0);
}
Close(clifd);
}
exit(0);
}
void serv_echo(int clifd)
{
int nread;
char recvline[MAXLINE];
while ( (nread=Readline(clifd,recvline,MAXLINE)) > 0 )
{
Writen(clifd,recvline,nread);
}
}
void sig_child(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d ternminated\n", pid);
return;
}
正常启动
首先服务器程序启动后,调用socket bind listen 和accept,并阻塞在accept。可以用netstat来检测到服务器进程有一个处于LISTEN状态的套接口。然后启动客户程序,客户调用socket connect后,引起TCP的三路握手过程。三路握手完成后,客户程序从connect返回,服务器程序从accept返回,到此TCP连接建立。服务器进程产生一个子进程用来处理于客户的连接,服务器主进程则继续阻塞于accept调用,等待下一个客户连接。
连接建立后,客户程序阻塞于fgets调用,等待用户输到此TCP连接建立。入一行。服务器程序则阻塞于readline调用,等待客户程序送出一行。此时可以通过netstat来检测到:服务器主进程处于LISTEN状态,服务器子进程和客户进程则处于ESTABLISHED状态。
正常终止
当对客户程序键入EOL字符(Ctrl+C键)以终止客户,立即运行netstat。可以检测到客户程序的套接口处于TIME_WAIT状态,服务器程序一个套接口则处于LISTEN状态。
正常终止过程如下:客户程序接受到终止字符后,从fgets返回一个空指针,客户进程调用exit后结束。于是,客户套接口被内核关闭,并向服务器发送一个FIN,服务器则以ACK相应,此时客户套接口处于FIN_WAIT_2状态,服务器套接口处于CLOSE_WAIT状态。
服务器收到FIN时,从readline调用返回并返回0,这导致服务器子进程结束。服务器的连接套接口随之被关闭,服务器子进程向客户发送一个FIN,客户则以ACK回应之,连接终止。客户进程虽然结束,但是客户进程套接口仍然处于TIME_WAIT状态。
Posix信号
信号是发生某事件时对进程的通知,也称为为软中断。可以由一个进程发往另一个进程,也可以由内核发往进程。SIGCHLD信号就是进程终止时,内核发送给终止进程父进程的信号。每个信号都有一个处理方法,我们可以通过调用sigaction来设置我们自己的信号处理程序,也可以通过设置SIG_IGN来忽略信号,设置SIG_DFL来设置默认的处理方法。
僵尸进程
设置进程为僵尸状态的目的是为了维护子进程的信息(子进程ID、终止状态和子进程的资源利用信息)。如果一个进程终止,且该进程仍有子进程处于僵尸状态,则所有僵尸进程的父进程设为init进程。僵尸进程占用内核空间,所以我们应该避免进程变为僵尸进程,方法是wait waitpid系统调用。可以设置信号SIGCHLD的信号处理程序,在此信号处理程序里调用wait或waitpid。这样可以避免僵尸进程。
在某些系统上这样作可能会导致服务器进程错误。原因是服务器子进程终止后,向父进程发送一个SIGCHLD信号,导致accept调用被中断,某些系统在信号处理程序执行完后不会重启accept调用,而是返回EINTR错误。由于我们的服务程序没有处理这种错误,所以会导致错误。为了移植性,当我们编写信号处理程序时,必须对慢系统调用(可能常时间阻塞的系统调用)返回EINTR有所装备。收到EINTR错误时,自己来重启被中断的系统调用。
用waitpid来防止产生僵尸进程
waitpid的WNOHANG选项,通知内核在没有子进程终止时不要阻塞。
假如服务器程序有5个子进程,每个进程负责处理一个连接。则可能5个子进程同时终止,同时向父进程发送SIGCHLD信号。而信号处理程序只能对一个信号进行处理,只有一个子进程正常终止,其余4个子进程则成为僵尸进程。可以通过ps -s命令来进行验证的确有4个子进程成为了僵尸进程。解决方法就是,循环调用witpid(加上WNOHANG选项)。
网络编程时注意的三种情况:
1 当派生子进程时,必须捕捉SIGCHLD信号,防止子进程结束时成为僵尸进程。2 当捕捉信号时,必须处理被中断的系统断用,因为有些系统不能重启被中断的系统调用,需要我们自己来重启。3 SIGCHLD信号的处理程序必须正确便学,应使用waitpid来避免多个子进程同时终止时,留下僵尸进程。