【Unix 网络编程】服务器网络编程模型——多进程并发模型

在介绍多进程并发模型前,先看看之前的一个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,…);
}
上面程序大致存在这么一些弊端,基本上都在阻塞阶段:
  1. 我们知道如果客户端没有发来连接请求,那么服务器端进程将会阻塞在 accept 系统调用处,程序而不能执行其他任何操作,直到有客户端调用 connect 发送连接请求,accept 返回,程序才能进行后面的操作。
  2. 在与客户端建立连接之后,就可以进行正常通信了,这里服务器端通过 read 系统调用从客户端接收数据,对于read 函数,如果客户端迟迟不发送数据过来,那么程序同样也会阻塞在 read 调用,苦等 read 返回,这时,如果还有另外的客户端请求连接时,都会失败。
  3. 另外同样的道理,write 系统调用也会使得程序出现阻塞(常见的就是客户端写缓冲区满了),write函数将苦等到系统缓冲区有足够的空间把你要发送的数据拷进去才返回。

阻塞的结果就是一直在耗费系统资源,前后就一个进程在运行,什么事情都仰仗着它来完成,一旦阻塞就得等着它完成这件事才能干后面的事情,耗费系统资源。 另外更重要的是当一个客户请求传输花费较长时间时,服务器将被单个客户长时间占用,如前面的迭代模型(前面链接)设置的是单个用户一直占用,这样服务器不可能同时服务多个客户,必须等这个终止了才行。

这样我们可以考虑 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一个子进程与之数据通信,彼此不干扰,不会影响到新客户的连接。

注意到上面这个多进程并发模型,每次有客户请求连接时,父进程又会fork 一个子进程(阻塞在accept处,监听新客户连接请求的一直是最开始的那个父进程),换句话说,服务器有多少个客户与之连接,期间就要fork 多少个子进程来进行数据交互。虽然Linux 在创建进程中采用写时拷贝机制,大大降低了fork 一个子进程的消耗,但若客户端连接较大,一个服务器有大量的客户连接是很正常的事情,这时系统仍然将不堪重负。


我们可以通过多线程(线程池)并发模型,在一定程序上改善这个问题。






  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值