您的位置:首页 > 理论基础 > 计算机网络

UNIX网络编程笔记(3):简单的并发服务器

2015-12-02 09:46 441 查看
上一讲中的简单时间获取服务器是一个迭代服务器,对于获取时间来说够用了。迭代服务器有这样的特点:同一时间只能给一个客户服务。也就是说,如果某一时刻服务器与某个客户正在连接,其它客户必须等到上一个客户与服务器断开连接后才能连接成功。这对于需要花长时间处理客户请求的服务器来说并不适用。

解决办法是将服务器改为并发服务器。这样,即使有一个客户正在和服务器连接,其它的客户也能与服务器建立连接获得服务。最简单的办法就是fork函数。

1、fork函数

fork函数是UNIX中派生进程的唯一方法,定义在<unistd.h>头文件中:

pid_t fork(void);
这个函数的奇特之处在于一次调用,两次返回。在父进程中返回子进程的ID,在子进程中返回0。所以,我们可以通过返回值判断当前进程是父进程还是子进程。

fork函数在子进程中返回0而不是父进程ID的原因是:任何子进程只有一个父进程,而这个父进程的ID可以通过函数getppid获得;但是对于父进程,可以有多个子进程,所以父进程无法通过函数获得子进程ID,如果父进程想跟踪所有子进程的ID,那么父进程必须在每次调用fork时记住子进程ID。

2、并发服务器

上面介绍了fork函数,可以通过调用fork函数达到并发的效果。下面是典型的并发服务器框架:

pid_t pid;
int listenfd,connfd;
listenfd=socket(...);
bind(listenfd,...);
listen(listenfd,...);
for(;;)
{
connfd=accept(listenfd,...);
if((pid=fork)==0)
{
close(listenfd);//关闭listenfd
doit(connfd);//处理请求
close(connfd);//关闭子进程connfd
exit(0);//子进程退出
}
close(onnfd);//关闭父进程connfd
}
这个框架中究竟发生了什么?可以用下面的图示展示出来。

(1)服务器调用socket、bind和listen函数后,处于监听状态,等待客户发送连接请求,这个时候服务器还没有调用accept:



(2)这时服务器只有一个套接字描述符listenfd,即监听描述符。当服务器中accept函数返回时,就有了两个套接字描述符listenfd和connfd,套接字描述符connfd是一个连接描述符,可以进行读写操作:



(3)这个时候客户与服务器建立了连接。随后服务器调用fork函数,自己变成父进程,复制自己形成一个子进程。由于两个进程相同,所以客户与两个进程的两个connfd都处于连接状态:



(4)然后,父进程中关闭连接套接字connfd,子进程关闭监听套接字listenfd:



这就达到了我们希望的状态,由子进程处理客户请求,父进程继续监听。

需要注意的是,close函数会导致发送一个FIN,随后TCP连接应该终止。那为什么父进程中close(connfd)后没有终止与客户的连接呢?每个文件描述符都有一个引用计数,是当前打开着的引用该文件的描述符的个数,只有引用计数为0时文件才会关闭。而fork函数执行后,对connfd套接字的引用计数是2,父进程关闭connfd后引用计数变为1,所以不会关闭连接。然而在子进程中,关闭connfd后引用计数变为0,会关闭与客户的连接。

3、时间获取程序的改进

这里我们只需要改进服务器程序,将它变成一个并发服务器。代码如下:

#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#define MAXLINE 1024

int main(int argc,char *argv[])
{
int listenfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("socket error\n");
return 0;
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5000);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
printf("bind error\n");
return 0;
}
if(listen(listenfd,5)<0)
{
printf("listen error\n");
return 0;
}
int connfd;
socklen_t len;
struct sockaddr_in cliaddr;
pid_t pid;
for(;;)
{
len=sizeof(cliaddr);
if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0)
{
printf("accept error\n");
return 0;
}
if((pid=fork())==0)
{
if(close(listenfd)<0)
{
printf("close listenfd error\n");
return 0;
}
printf("[PID]%ld Receive a connection from:%s.%d\n",(long)getpid(),inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port));
ticks=time(NULL);
snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks));
if(write(connfd,buff,strlen(buff))<0)
{
printf("write error\n");
return 0;
}
printf("[PID]%ld sleep 5s.\n",(long)getpid());
sleep(5);
printf("[PID]%ld sleep done.\n",(long)getpid());
if(close(connfd)<0)
{
printf("close child connfd error\n");
return 0;
}
return 0;
}
if(close(connfd)<0)
{
printf("close parent connfd error\n");
return 0;
}
}
}
这里我们为了增加服务器处理每个请求的时间,加了sleep(5),会看到,服务器在处理一个客户的请求时,也会与另一个客户建立连接处理请求。运行结果如下:

(1)打开服务器:



(2)客户1连接(进程ID是3594):



(3)客户2连接(进程ID是3596):



(4)下图是服务器处理情况:



可以看到,在客户1(进程ID是3594)仍在sleep的时候,服务器接受了客户2(进程ID是3596)的连接。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: