在介绍多进程并发模型前,先看看之前的一个TCP socket 例子,一个同步阻塞迭代模型。其服务器端核心代码如下,完整代码参见前面链接,
同步阻塞迭代服务器模型:
ser_sockfd = socket(…);
bind(ser_sockfd,…);
listen(ser_sockfd,…);
while(1)
{
cli_sockfd = accept(ser_sockfd,…);
//recv(cli_sockfd,buf,…);
read(cli_sockfd,buf,…);
doit(buf);
//send(cli_sockfd,buf,…);
write(cli_sockfd,buf,…);
}
上面程序大致存在这么一些弊端,基本上都在阻塞阶段:
阻塞的结果就是一直在耗费系统资源,前后就一个进程在运行,什么事情都仰仗着它来完成,一旦阻塞就得等着它完成这件事才能干后面的事情,耗费系统资源。 另外更重要的是当一个客户请求传输花费较长时间时,服务器将被单个客户长时间占用,如前面的迭代模型(前面链接)设置的是单个用户一直占用,这样服务器不可能同时服务多个客户,必须等这个终止了才行。
这样我们可以考虑 fork 一个子进程来服务每个客户:当一个连接建立时,accept 返回,服务器接着调用 fork,然后子进程服务客户(通过已连接套接口 cli_sockfd),父进程则等待另一个连接(通过监听套接口 listenfd)。其中父进程关闭已连接套接口,负责监听外来客户连接请求,子进程则关闭监听套接口,负责与服务器端的数据通信。因为我们fork 子进程是在 accept 返回之后,此时连接已经建立,监听套接口和已连接套接口都在父进程与子进程之间共享。
看代码,这个程序是根据前面同步阻塞迭代模型修改而来,while循环内不断读取写入,服务器端会堵塞在recv处(客户端程序):
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 6666
int main(int argc,char **argv)
{
pid_t pid;
int ser_sockfd,cli_sockfd;
int err,n;
int addlen;
struct sockaddr_in ser_addr;
struct sockaddr_in cli_addr;
char recvline[200],sendline[200];
ser_sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字
if(ser_sockfd == -1)
{
printf("socket error:%s\n",strerror(errno));
return -1;
}
bzero(&ser_addr,sizeof(ser_addr));
/*在待捆绑到该TCP套接口(sockfd)的网际套接口地址结构中填入通配地址(INADDR_ANY)
和服务器的众所周知端口(PORT,这里为6666),这里捆绑通配地址是在告知系统:要是系统是
多宿主机(具有多个网络连接的主机),我们将接受宿地址为任何本地接口的地址*/
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(PORT);
//将网际套接口地址结构捆绑到该套接口
err = bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
if(err == -1)
{
printf("bind error:%s\n",strerror(errno));
return -1;
}
//将套接口转换为一个监听套接口,监听等待来自客户端的连接请求
err = listen(ser_sockfd,5);
if(err == -1)
{
printf("listen error\n");
return -1;
}
printf("listen the port:\n");
while(1)
{
addlen = sizeof(struct sockaddr);
//等待阻塞,等待客户端申请,并接受客户端的连接请求
//accept成功,将创建一个新的套接字,并为这个新的套接字分配一个套接字描述符
cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr,&addlen);
if(cli_sockfd == -1)
{
printf("accept error\n");
}
if((pid = fork()) == 0)
{
//数据传输
close(ser_sockfd);
while(1)
{
printf("waiting for client...\n");
n = recv(cli_sockfd,recvline,1024,0);
if(n == -1)
{
printf("recv error\n");
}
recvline[n] = '\0';
printf("recv data is:%s\n",recvline);
printf("Input your words:");
scanf("%s",sendline);
send(cli_sockfd,sendline,strlen(sendline),0);
}
}
close(cli_sockfd);
}
close(cli_sockfd);
return 0;
}
精练一下,看看核心代码部分(上面是每个客户一直与服务器端进行通信,数据传输采用recv和send,flag标志位置0,和read,write无异,下面修改一下采用执行一次数据交互,类似于客户端发过来数据,服务器接收到这个数据进行其余操作,再反馈给客户,通信终止)
多进程并发服务器模型:
ser_sockfd = socket(…);
bind(ser_sockfd,…);
listen(ser_sockfd,…);
while(1)
{
cli_sockfd = accept(ser_sockfd,…);
pid = fork();
if(-1 == pid)
do_err_handler();
if(0 == pid)//child
{
close(ser_sockfd);//监听套接口
//recv(cli_sockfd,buf,…);
read(cli_sockfd,buf,…);
doit(buf);
//send(cli_sockfd,buf,…);
write(cli_sockfd,buf,…);
close(cli_sockfd);
}
close(cli_sockfd);//parent
}
同前面同步迭代模型一样,也会有阻塞在 accept 的可能性,但是较为改进的是:一旦某个客户端连接建立起来,会立即 fork 一个新的进程来处理与这个客户的数据交互,避免程序阻塞在read调用处,而影响其他客户端的连接。看程序,父进程负责监听客户端的连接请求,子进程负责数据交互。分工合作,父进程一直阻塞在accept 处等待新客户的连接请求,已连接的与客户之间的数据交互则由子进程来完成。
为做对比,这里贴出迭代模型的服务器端代码,客户端代码点此
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 6666
int main(int argc,char **argv)
{
int ser_sockfd,cli_sockfd;
int err,n;
int addlen;
struct sockaddr_in ser_addr;
struct sockaddr_in cli_addr;
char recvline[200],sendline[200];
ser_sockfd = socket(AF_INET,SOCK_STREAM,0); //创建套接字
if(ser_sockfd == -1)
{
printf("socket error:%s\n",strerror(errno));
return -1;
}
bzero(&ser_addr,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(PORT);
//将网际套接口地址结构捆绑到该套接口
err = bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
if(err == -1)
{
printf("bind error:%s\n",strerror(errno));
return -1;
}
//将套接口转换为一个监听套接口,监听等待来自客户端的连接请求
err = listen(ser_sockfd,5);
if(err == -1)
{
printf("listen error\n");
return -1;
}
printf("listen the port:\n");
while(1)
{
addlen = sizeof(struct sockaddr);
//等待阻塞,等待客户端申请,并接受客户端的连接请求
//accept成功,将创建一个新的套接字,并为这个新的套接字分配一个套接字描述符
cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr,&addlen);
if(cli_sockfd == -1)
{
printf("accept error\n");
}
//数据传输
printf("waiting for client...\n");
n = recv(cli_sockfd,recvline,1024,0);
if(n == -1)
{
printf("recv error\n");
}
recvline[n] = '\0';
printf("recv data is:%s\n",recvline);
printf("Input your words:");
scanf("%s",sendline);
send(cli_sockfd,sendline,strlen(sendline),0);
close(cli_sockfd);
}
close(ser_sockfd);
return 0;
}
上面这个同步迭代服务器模型,如果一个客户正在与该服务器进行数据传输,或阻塞在某处(recv),此时当新客户试图与该服务器建立连接时,便不能得到及时响应,必须等服务器与老客户完成通信后,服务器才能与新客户连接进行数据通信。而这里讲的多进程服务器模型则不一样,与客户进行数据通信的都是同一个父进程fork出来的子进程,父进程则负责监听,有新客户再fork一个子进程与之数据通信,彼此不干扰,不会影响到新客户的连接。
我们可以通过多线程(线程池)并发模型,在一定程序上改善这个问题。