在介绍多进程并发模型前,先看看之前的一个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<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/socket.h> #include<sys/types.h> #include<unistd.h> #include<netinet/in.h> #include <errno.h> #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<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/socket.h> #include<sys/types.h> #include<unistd.h> #include<netinet/in.h> #include <errno.h> #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一个子进程与之数据通信,彼此不干扰,不会影响到新客户的连接。
我们可以通过多线程(线程池)并发模型,在一定程序上改善这个问题。