在阅读完《unix 网络编程:卷一》之后,感觉作者真是unix下编程的大师级的人物。而对于我个人而言,每次阅读完一本技术书籍之后,一定还是得自己重新再写一遍程序(换点内容),复习书本中的内容(大致结构,或者说思想,相同),否则,你很难做到真的理解并掌握的地步。
Okay,今天我带来的是服务器模型中的第一种,也是最基本最常用的一种模型–TCP并发服务器,每个客户一个子进程。
先简单介绍一下:TCP并发服务器,每个客户一个子进程,也就是说并发服务器调用fork派生一个子进程来处理每个子进程,使得服务器能够同时为多个客户服务,每个进程一个客户。客户数目的唯一限制是操作系统对以其名义运行服务器的用户ID能够同时拥有多少子进程的限制。
具体到我们的需求,我们的客户端发送某个指令,服务端接收。如果符合服务端的要求,就将当时的时间发回给客户端。需求很简单,我们的着重点在服务器的模型。
Okay,来看代码:
这是服务端的主代码(serv.c):
#include "pub.h"
#define LISTENQ 1024
void sig_child(int signo);
//serv <port>
int main(int argc, char **argv)
{
int listenfd,connfd;
pid_t childpid;
int on = 1;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen;
char *ptr;
if(argc != 2)
{
fprintf(stderr,"usage: serv <port>\n");
exit(1);
}
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("serv socket error ");
exit(-1);
}
memset(&servaddr, 0, sizeof(servaddr));
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);
}
//信号处理函数
Signal(SIGCHLD, sig_child); //每一个子进程终止时就会产生SIGCHLD信号,默认是忽略
for( ; ; )
{
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0)
{
if(errno == EINTR)
continue;
else
{
perror("serv accept error");
exit(-1);
}
}
ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
fprintf(stdout,"%s has connected\n", ptr);
if((childpid = fork()) == 0)//child
{
//注意这里是另一个进程
close(listenfd); //子进程关闭listenfd
do_child(connfd);
ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
fprintf(stdout,"%s has disconnected\n", ptr);
close(connfd);
exit(0);
}
close(connfd);//父进程关闭connfd
}
close(listenfd);
exit(0);
}
void sig_child(int signo)
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) //不阻塞,返回child pid
{//防止同时有几个子进程killed,加while,知道处理完所有killed child
fprintf(stdout,"%d terminated\n",pid);
fflush(stdout);
}
return;
}
看这小段连接的代码:
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0)
{
if(errno == EINTR)
continue;
else
{
perror("serv accept error");
exit(-1);
}
}
ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
fprintf(stdout,"%s has connected\n", ptr);
注意我们这里的大循环,每次循环的开始都是accept,也就是从已连接的队列中返回其中一个,如果返回成功,就打印一下IP,显示是哪个IP来连接。
这里采用的是inet_ntoa()函数,这是个专门为IPV4准备的函数。你也可以使用inet_pton()函数,这是IPV4和IPV6通用的函数。
再看下面的一小段:
if((childpid = fork()) == 0)//child
{
//注意这里是另一个进程
close(listenfd); //子进程关闭listenfd
do_child(connfd);
ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
fprintf(stdout,"%s has disconnected\n", ptr);
close(connfd);
exit(0);
}
这里就是真正的fork出一个子进程。
先close(listenfd),注意这里close并不会真正的关闭listenfd,这只是减少了listenfd的一个引用次数,父进程还有一个引用。
然后是do_child(connfd),这里是我们自定义的来处理连接的函数,注意已经将connfd传递给这个函数。稍后我们在讨论一下这个函数。
如果能从do_child()函数返回,就再打印一下,某一个IP 已经离开。
最后就是close(connfd),再exit(0)退出,这里的close(connfd),可以省略,子进程退出,就会关掉自身所打开的描述符。
注意到我们这里还有一信号处理函数,sig_child,如下所示:
void sig_child(int signo)
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) //不阻塞,返回child pid
{//防止同时有几个子进程killed,加while,知道处理完所有killed child
fprintf(stdout,"%d terminated\n",pid);
fflush(stdout);
}
return;
}
我们在之前已经注册了这个信号处理函数,
//信号处理函数
Signal(SIGCHLD, sig_child); //每一个子进程终止时就会产生SIGCHLD信号,默认是忽略
这里我们的处理函数,就是打印一下离开的子进程的pid,就如同注释所示,采用这种形式:
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) 可以防止同时有几个子进程killed,加while,知道处理完所有killed child
到了这里,服务器主要的模型已经展示出来了,现在我们来看主要的处理客户端连接的do_child()函数。
来看代码:
#include "pub.h"
void do_child(int sockfd)
{
time_t mytime;
char buff[MAXLINE];
int n;
for( ; ; )
{
if((n = read(sockfd, buff, sizeof(buff))) <= 0)
{
if( n < 0 && errno == EINTR)
continue;
else if(n == 0){
//交给外面的主循环处理(打印某某离开)
break;
}else{
perror("child read error");
exit(-1);
}
}
else
{
//比较前几个字符串,是不是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));
}
}
}
}
这里采用的处理很简单,就如同我注释中讲的,比较前几个字符串,是不是GETTIME或者gettime,是则返回时间,否则返回 Gettime Command Error
看这里:
mytime = time(NULL);
snprintf(buff, sizeof(buff), "%s", ctime(&mytime));
writen(sockfd, buff, strlen(buff));//这里最好用writen(自定义)
通过time()和ctime()用字符串的形式打印出当前时间。注意这里采用的是writen()函数,自定义函数。
其实就是write相当数目的buff,同时防止信号打断
来看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;
}
接下来,我们写了一个测试用的客户端(client.c):
来看代码:
#include "pub.h"
//client <ip> <port>
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
int n;
if(argc != 3)
{
fprintf(stderr,"usage: <ip> <port> \n");
exit(1);
}
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("client socket error ");
exit(-1);
}
memset(&servaddr, 0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &servaddr.sin_addr);
if((connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) < 0)
{
perror("client connect error");
exit(-1);
}
strcli(sockfd);
exit(0);
}
这里就是简单的客户端代码,通过参数传进来要连接的ip与port,发送请求的函数是str_cli()。
来看str_cli函数:
#include "pub.h"
void strcli(int sockfd)
{
char sendbuff[MAXLINE], recvbuff[MAXLINE];
while(fgets(sendbuff, sizeof(sendbuff), stdin) != NULL)
{
writen(sockfd, sendbuff, strlen(sendbuff));
if(read(sockfd, recvbuff, sizeof(recvbuff)) == 0)
fprintf(stderr,"server has terminated\n");
fputs(recvbuff, stdout);
}
}
这里其实就是读取标准输入,发送给服务端,并read阻塞,等服务端发送回后,将服务端发送回的数据打印到标准输出上。
另外,还有一个注册信号处理函数Signal函数,之前讲过,但没有给出代码。
我这里直接将unix网络编程里的signal拿了过来,可以直接使用。
也来看一下,signal.c:
/* 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);
}
至此,我们的第一种服务器模型就已经完成了。全部的代码都已经晒了出来,并经过测试。你可以在你自己的linux上试试,看行不行。
另外,我们这里就是简单的客户端与服务端查询时间的处理请求。你也可以进行别的处理请求。比如可以双方通信,采用多线程,或者试试发送文件,都可以。Just depend on yourself.而且,你只需要修改do_child()和str_cli()函数,其他的可以不修改,除非你有别的需求。
最后,还得讲一下这种模型的问题。主要就是为每一个客户fork一个子进程比较消耗CPU时间,几百或几千的的客户是没有问题的,但是现在的每天的TCP连接都是上百万的,这里就会有问题。当然,如果系统负载较轻,这种模型是不错好的选择。
好了,这就是我们的今天博客的全部内容,你懂了吗?如果我有出错的地方,欢迎大家指出,如果觉得好,也可以点赞哦。