okay,今天是我们linux服务器模型的第二篇—TCP预先派生子进程服务程序,accept无上锁保护。
从字面上理解,就是在启动阶段派生一定数量的子进程,当各个客户连接到达时,这些子进程立即就能为他们服务。注意与我们第一篇的不同,我们第一篇是为每一个客户派生一个子进程,来一个,派生一个。另外值得注意的是,如果某个时刻,客户数量正好等于预先派生的子进程,那么对于下一个客户,依然能够执行三次握手,进行连接,只不过得等待到至少有一个子进程可用。
另外,我们的服务器业务还是与第一篇一样,就是根据客户端发的指令判断返回相应数据。
okay,来看我们的代码:
serv.c:
#include "pub.h"
static int nchildren;
static pid_t *pids;
void sig_int(int signo);
int main(int argc, char **argv)
{
int listenfd;
struct sockaddr_in servaddr;
int on = 1;
int i = 0;
pid_t child_make(int, int);
if(argc != 3)
{
fprintf(stderr,"usage: serv <port> <nchildren>\n"); //这里指定端口与预先派生子进程的数目
exit(-1);
}
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
perror("serv socket error ");
exit(-1);
}
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[1]));
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//设置可重用
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
perror("serv bind error ");
exit(-1);
}
if(listen(listenfd, LISTENQ) < 0){
perror("serv listen error ");
exit(-1);
}
nchildren = atoi(argv[2]);
pids = calloc(nchildren, sizeof(pid_t));
for(i = 0;i < nchildren; i++)
pids[i] = child_make(i ,listenfd);
Signal(SIGINT, sig_int);
for( ; ; ) //parent just pause
pause(); //everything done by children
exit(0);
}
void sig_int(int signo)
{
int i = 0;
for(i = 0; i < nchildren; i++){
kill(pids[i], SIGTERM);
}
while(wait(NULL) > 0); //wait for all children
//对于wait
//如果调用的父进程没有子进程则返回-1,并设置errno
if(errno != ECHILD)
{
perror("sig_int error ");
exit(-1);
}
exit(0);
}
关注这里:
for(i = 0;i < nchildren; i++)
pids[i] = child_make(i ,listenfd);
Signal(SIGINT, sig_int);
for( ; ; ) //parent just pause
pause(); //everything done by children
这里的nchildren是从我们的参数中提取出来的,代表预先派生的子进程的数量。对于pids数组,用child_make()函数派生子进程,同时将子进程的PID返回给pids数组的某一项。这里的pids是我们动态分配的数组,用来储存我们的各个子进程的PID。这里的child_make()函数是用来派生子进程,同时包括处理的代码。带会我们来讲解它。
看后面的Signal(SIGINT, sig_int);就是注册SIGINT信号的处理函数。我们将在收到SIGINT信号后,将所有派生的子进程kill.
这与后面的for循环以及pause()函数就是为了让父进程挂起。让所有的处理交由预先派生的子进程。
来看我们的sig_int()信号处理函数:
void sig_int(int signo)
{
int i = 0;
for(i = 0; i < nchildren; i++){
kill(pids[i], SIGTERM);
}
while(wait(NULL) > 0); //wait for all children
//对于wait
//如果调用的父进程没有子进程则返回-1,并设置errno
if(errno != ECHILD)
{
perror("sig_int error ");
exit(-1);
}
exit(0);
}
一目了然,就是我们之前说的,kill掉所有派生子进程。通过while(wait(NULL) > 0);回收所有的紫禁城的资源。注意我们注释中说的:对于wait,如果调用的父进程没有子进程则返回-1,并设置errno。这个errno就是ECHILD。这并不算错误。
okay,接下来是真正的关于派生子进程的函数。
来看child_make()函数:
#include "pub.h"
pid_t child_make(int i, int listenfd)
{
pid_t pid;
void child_main(int, int);
if((pid = fork()) > 0)//parent just return pid of child
return pid;
//child
child_main(i ,listenfd); //nerver return
}
同样很简洁,fork返回如果是大于0,便是子进程的PID,返回即可。留给上面我们讲的pids数组保存。如果是子进程,就进入child_main()函数,这是我们真正进行与客户端连接的函数。
来看一下child_main()函数:
#include "pub.h"
void child_main(int i, int listenfd)
{
int connfd;
void child_handle(int, int);
struct sockaddr_in cliaddr;
socklen_t len;
char *buff;
for( ; ; ){
len = sizeof(cliaddr);
if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len)) < 0){
if(errno == EINTR){
continue;
}else{
perror("the num %d child_main accept error ");
continue;
}
}
buff = inet_ntoa(cliaddr.sin_addr);
fprintf(stdout,"%s has been handled\n",buff);
fflush(stdout);
child_handle(i, connfd);
fprintf(stdout,"%s has left\n",buff);
fflush(stdout);
close(connfd);
}
close(listenfd);
}
不是很难,就是与我们第一篇中的连接差不多。通过一个for循环,等待accept返回,一旦返回并且无错,就先打印某某has been handled,进入child_handle()函数,这是我们处理子进程的业务代码函数,待会再讲。
每当一个子进程处理完业务,就答应某某has left,并关闭connfd,注意我们这个循环并不会退出,永远待命。
再看我们的child_handle()函数:
#include "pub.h"
void child_handle(int i,int sockfd)
{
char buff[MAXLINE];
int n;
time_t mytime;
for( ; ; ){
if((n = read(sockfd, buff, MAXLINE)) <= 0){
if(n < 0 && errno == EINTR)
continue;
else if(n == 0){
//断开连接,交由child_main处理
break;
}else{
perror("child num %d child_handle read error ");
break;
}
}
//比较前几个字符串,是不是GETTIME,是则返回时间,否则返回 Gettime Command Error
if((strncmp(buff,"GETTIME", 7) == 0) ||
(strncmp(buff,"gettime", 7) == 0) )
{
mytime = time(NULL);
snprintf(buff, sizeof(buff), "%s", ctime(&mytime));
writen(sockfd, buff, strlen(buff));//这里最好用writen(自定义)
}
else
{//不是的话,就返回 Gettime Command Error
snprintf(buff, sizeof(buff), "Gettime Command Error ");
writen(sockfd, buff, strlen(buff));
}
}
}
这段业务代码与我们之前第一篇的do_child()函数基本一样。比较前几个字符串,是不是GETTIME或者gettime,是则返回时间,否则返回 Gettime Command Error。
其中还包含writen函数,我把它贴出来,就是第一篇中的writen函数,就不讲解了;
#include "pub.h"
int writen(int sockfd,const char *buff, int n)
{
int nleft = n;
int ncount = 0;
const char *ptr = buff;
while(nleft > 0)
{
if((ncount = write(sockfd, ptr, nleft)) <= 0)
{
if(errno == EINTR)
ncount = 0; //call again
else
return -1;
}
nleft -= ncount;
ptr += ncount;
}
return n - nleft;
}
另外,还有我们的信号处理函数Signal()函数,我也贴出来。
/* include signal */
#include "pub.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);
}
/* end signal */
Sigfunc *
Signal(int signo, Sigfunc *func) /* for our signal() function */
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR)
{
perror("signal error");
exit(1);
}
return(sigfunc);
}
这是一张可以概括我们今天的服务器模型的图片,注意这里我们的每一个子进程都有listenfd,每个子进程都是通过这个listenfd来进行accept,得到客户端连接。同时,这个listenfd描述符只是一个这个FILE结构体的下标而已。当前有那么多的引用计数。
okay,这就是我们今天的linux服务器模型第二篇–TCP预先派生子进程服务程序,accept无上锁保护。你明白了吗?