Linux 系统应用网络编程三(服务器模型)

        在网络通信过程中,服务端通常需要处理多个客户端。由于多个客户端的请求可能会同时到来,服务器端可采用不同的方法来处理。总体上来说,服务器端可采用两种模型来实现:循环服务器模型和并发服务器模型

        循环服务器模型是指服务器端依次处理每个客户端,直到当前客户端的所有请求处理完毕,再处理下一个客户端。这类模型的优点是简单,缺点显而易见。特别是TCP循环服务器模型,由于必须先处理完当前客户端,容易造成其他客户端等待时间较长的情况。

        为了提高服务器的并发处理能力,又引入了并发服务器模型。其基本思想是在服务器端此阿勇多任务机制(多进程或多线程),分别为每个客户端创建一个任务来处理,极大地提高了服务器的并发处理能力。

        下面具体介绍循环服务器模型和并发服务器模型的流程及实现。为了更好的进行对比。本节均以TCP为例讨论相关模型。

一、循环服务器(TCP)

1.1、运行介绍

       TCP循环服务器是一种常用的模型,其工作流程如下:

1)服务器端从连接请求队列中提取请求,建立连接并返回新的已连接套接字;

2)服务器端通过已连接套接字循环接收数据,处理并发送给客户端,知道客户端关闭连接;

3)服务器端关闭已连接套接字,返回步骤1;

1.2、特点分析

       通过上面对服务器执行过程的介绍,可以得到以下结论。

1)服务器端采用循环嵌套来实现。外层循环依次提取每个客户端的连接请求,建立TCP连接。内层循环接受并处理当前客户端的所有数据,知道客户端关闭连接;

2)如果当前客户端没有处理结束,其他客户端必须一直等待。

注意:采用这种模型的服务器无法同时为多个客户端服务。

1.3、编程示例

下面实现 TCP ECHO 服务器端和客户端。服务器端接收到客户端数据后,原封不动发送回去(回射服务);客户端运行时,用户从键盘输入字符串,发送给服务器端并接受返回的数据,直到用户输入quit 后退出。

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 128
#define PORT 8888
 
int main()
{
	int listenfd, clientfd;
	int n;
	struct sockaddr_in serveraddr,clientaddr;
	socklen_t peerlen;
	char buffer[BUFFER_SIZE];
 
	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket error");
		exit(-1);
	}
	else
	{
		printf("listenfd:%d\n",listenfd);
	}
 
	memset(&serveraddr,0,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
 
	if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0) //绑定IP地址和端口号
	{
		perror("bind error");
		exit(-1);
	}
	else
	{
		printf("bind successfully!\n");
	}
 
	if(listen(listenfd,10) == -1)
	{
		perror("listen error");
		exit(-1);
	}
	else
	{
		printf("listening....\n");
	}
 
	peerlen = sizeof(clientaddr);
	while(1)
	{
		if((clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&peerlen)) < 0) //循环等待客户端的连接
		{
			perror("accept error");
			exit(-1);
		}
		else
		{
			printf("connection from [%s:%d]\n",inet_ntoa(clientaddr.sin_addr)
					,ntohs(clientaddr.sin_port));	
		}
 
		memset(buffer,0,sizeof(buffer));
		while(1)
		{
			if((n = recv(clientfd,buffer,BUFFER_SIZE,0)) == -1) //循环接收客户端发送的数据
			{
				perror("recv error");
				exit(-1);
			}
			else if(n == 0) //此时,客户端断开连接
			{
				break;
			}
			else
			{
				printf("Received message:%s\n",buffer);
				if(send(clientfd, buffer, n, 0) == -1)
				{
					perror("send error");
					exit(-1);
				}
				else
				{
					printf("sendmessage:%s\n",buffer);
				}
			}
		}
		close(clientfd); //客户端断开连接后,服务端也断开
	}
	close(listenfd);
 
	return 0;
}

client.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 128
#define PORT 8888
 
int main()
{
	int n;
	int serverfd, clientfd;
	struct sockaddr_in serveraddr;
	char buffer[BUFFER_SIZE];
 
	if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket error");
		exit(-1);
	}
	else
	{
		printf("clientfd:%d\n",clientfd);
	}
//设置服务端的IP地址和端口号
	memset(&serveraddr,0,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
 
	if(connect(clientfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
	{
		perror("connect error");
		exit(-1);
	}
	else
	{
		printf("connect successfully!\n");
	}
 
	while(1)
	{		
		printf("input > ");
		fgets(buffer,sizeof(buffer),stdin);
		if(strcmp(buffer,"quit\n") == 0) //遇到quit,退出
			break;
		buffer[strlen(buffer) - 1] = '\0'; //将'\n'去掉
		if(send(clientfd,buffer,sizeof(buffer),0) == -1)
		{
			perror("send error");
			exit(-1);
		}
		memset(buffer,0,sizeof(buffer)); //清空buffer
		if((n = recv(clientfd,buffer,sizeof(buffer),0)) == -1)
		{
			perror("recv error");
			exit(-1);
		}
		else if(n == 0)
		{
			break; //若服务端意外关闭
		}
		else
		{
			printf("echo:%s\n",buffer);
		}
	}
	close(clientfd);
 
	return 0;
}

二、并发服务器(TCP)

2.1、运行介绍

       TCP并发服务器模型在网络通信中被广泛使用,既可以采用多进程也可以采用多线程来实现。以多进程为例,其工作流程如下:

1)服务器端父进程从连接请求队列中提取请求,建立连接并返回新的已连接套接字。

2)服务器端父进程创建子进程为客户端服务。客户端关闭连接时,子进程结束。

3)服务器端父进程关闭已连接套接字,返回步骤1;

2.2、特点分析

通过上面对服务器执行过程的介绍,可以得到以下结论。

1)服务器端父进程一旦接受到客户端的连接请求,建立好连接并创建新的子进程。这意味着每个客户端在服务器端有一个专门的子进程为其服务。

2)服务器端的多个子进程同时运行(宏观上),处理多个客户端。

3)服务器端的父进程不具体处理每个客户端的数据请求。

注意:采用这种模型的服务器端需要避免僵死进程。

2.3、编程示例

下面采用并发模型实现 TCP ECHO服务器端。

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#define BUFFER_SIZE 128
#define PORT 8888
#define IP "192.168.3.51"
 
void handler(int signo);
 
int main()
{
	int listenfd, clientfd;
	int n;
	pid_t pid;
	struct sockaddr_in serveraddr,clientaddr;
	socklen_t peerlen;
	char buffer[BUFFER_SIZE];
 
	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket error");
		exit(-1);
	}
	else
	{
		printf("Socket successfully!\nlistenfd:%d\n",listenfd);
	}
 
	memset(&serveraddr,0,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
 
	if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0)
	{
		perror("bind error");
		exit(-1);
	}
	else
	{
		printf("bind successfully!\n");
		printf("local IP:%s port:%d\n",IP,PORT);
	}
 
	if(listen(listenfd,10) == -1)
	{
		perror("listen error");
		exit(-1);
	}
	else
	{
		printf("listening....\n");
	}
 
	signal(SIGCHLD,handler);//捕捉SIGCHLD信号,子进程死亡后,调用handler回收	
	peerlen = sizeof(clientaddr);
	while(1)
	{
		if((clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&peerlen)) < 0)
		{
			perror("accept error");
			exit(-1);
		}
		else
		{
			printf("connection from [%s:%d]\n",inet_ntoa(clientaddr.sin_addr)
					,ntohs(clientaddr.sin_port));	
		}
 
		memset(buffer,0,sizeof(buffer));
		if((pid = fork()) < 0)
		{
			perror("fork error");
			exit(-1);
		}
		else if(pid == 0)
		{
			close(listenfd);
			while(1)
			{
				if((n = recv(clientfd,buffer,BUFFER_SIZE,0)) == -1)
				{
					perror("recv error");
					exit(-1);
				}
				else if(n == 0)
				{
					break;
				}
				else
				{
					printf("Received message:%s\n",buffer);
				}
			
				if(send(clientfd, buffer, n, 0) == -1)
				{
					perror("send error");
					exit(-1);
				}
				else
				{
					printf("sendmessage:%s\n",buffer);
				}
			}
			printf("client is closed\n");
			exit(0);
		}
		else
		{
			close(clientfd);
		}
	}
	close(listenfd);
 
	return 0;
}
//接收到SIFCHLD信号后,回收子进程
void handler(int signo)
{
	pid_t pid;
	while((pid = waitpid(-1, NULL, WNOHANG)) > 0)
	{
		printf("child(%d) is over!\n",pid);
	}
}

client.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 128
#define PORT 8888
 
int main()
{
	int n;
	int serverfd, clientfd;
	struct sockaddr_in serveraddr;
	char buffer[BUFFER_SIZE];
 
	if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket error");
		exit(-1);
	}
	else
	{
		printf("clientfd:%d\n",clientfd);
	}
 
	memset(&serveraddr,0,sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
 
	if(connect(clientfd,(struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
	{
		perror("connect error");
		exit(-1);
	}
	else
	{
		printf("connect successfully!\n");
	}
 
	while(1)
	{		
		printf("input > ");
		fgets(buffer,sizeof(buffer),stdin);
		if(strcmp(buffer,"quit\n") == 0)
			break;
		buffer[strlen(buffer) - 1] = '\0';
		if(send(clientfd,buffer,sizeof(buffer),0) == -1)
		{
			perror("send error");
			exit(-1);
		}
		memset(buffer,0,sizeof(buffer));
		if((n = recv(clientfd,buffer,sizeof(buffer),0)) == -1)
		{
			perror("recv error");
			exit(-1);
		}
		else if(n == 0)
		{
			break;
		}
		else
		{
			printf("echo:%s\n",buffer);
		}
	}
	close(clientfd);
 
	return 0;
}

三、I/O多路复用并发服务器

        I/O多路复用模型可以解决资源限制的问题。此模型实际上时将UDP循环模型用在了TCP上面。服务器用单进程循环处理请求(客户端有限的情况下)。

但是,其存在同样的问题:由于服务器是依次处理客户的请求,所以可能会导致有的客户等待时间过长。

server:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define MAXSIZE 128
 
int main()
{
	int i,nbyte;
	int listenfd, confd, maxfd;
	char buffer[MAXSIZE];
	fd_set global_rdfs, current_rdfs;
	struct sockaddr_in addr,clientaddr;
	int addrlen = sizeof(struct sockaddr_in);
	int caddrlen = sizeof(struct sockaddr_in);
 
	if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket error");
		exit(-1);
	}
	else
	{
		printf("socket successfully!\n");
		printf("listenfd : %d\n",listenfd);
	}
 
	memset(&addr, 0 ,addrlen);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(PORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	if(bind(listenfd,(struct sockaddr *)&addr,addrlen) == -1)
	{
		perror("bind error");
		exit(-1);
	}
	else
	{
		printf("bind successfully!\n");
		printf("listen port:%d\n",PORT);
	}
 
	if(listen(listenfd,5) == -1)
	{
		perror("listen error");
		exit(-1);
	}
	else
	{
		printf("listening...\n");
	}
 
	maxfd = listenfd;
	FD_ZERO(&global_rdfs);
	FD_SET(listenfd,&global_rdfs);
 
	while(1)
	{
		current_rdfs = global_rdfs;
		if(select(maxfd + 1,&current_rdfs, NULL, NULL,0) < 0)
		{
			perror("select error");
			exit(-1);
		}
		
		for(i = 0; i <= listenfd + 1; i++)
		{
			if(FD_ISSET(i, &current_rdfs)) //fd 就绪
			{
				if(i == listenfd) //有新的连接
				{
					if((confd = accept(listenfd,(struct sockaddr *)&clientaddr,&caddrlen)) == -1)
					{
						perror("accept error");
						exit(-1);
					}
					else
					{
						printf("Connect from [IP:%s PORT:%d]\n",
								inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
						FD_SET(confd,&global_rdfs);
						maxfd = (maxfd > confd ? maxfd : confd);
					}
				}
				else
				{
					if((nbyte = recv(i, buffer, sizeof(buffer),0)) < 0)
					{
						perror("recv error");
						exit(-1);
					}
					else if(nbyte == 0) //客户端close
					{
						close(i);
						FD_CLR(i,&global_rdfs); //将其清出
					}
					else
					{
						printf("recv:%s\n",buffer);
						send(i, buffer, sizeof(buffer),0);
					}
				}
			}
		}
	}
 
	return 0;
}

四、对比

模型类型流程实现优缺点及适用场景
循环服务器模型1. 服务器等待接收客户端请求。1. 使用recv()或recvfrom()接收客户端请求。优点:实现简单,逻辑清晰,适用于低负载场景。
2. 处理请求。2. 对接收到的请求进行处理。缺点:无法处理并发请求,响应时间不稳定,在高负载情况下性能下降。
3. 返回响应并等待下一个请求。3. 使用send()或sendto()返回响应。适用场景:简单的文件服务器或小型的Web服务器,其中并发量不是主要考虑因素。
并发服务器模型1. 服务器监听客户端连接请求。1. 使用socket()创建套接字,bind()绑定地址和端口,listen()监听连接请求。优点:能够同时处理多个请求,提高了系统的吞吐量和响应速度,响应时间相对稳定。
2. 接受连接请求,为每个客户端创建一个新的进程或线程处理请求。2. 使用accept()接受连接请求,创建子进程或线程处理每个客户端的请求。缺点:实现复杂,需要考虑线程管理、同步、资源竞争等问题,资源消耗较大。
3. 在进程或线程中处理请求并返回响应。3. 在子进程或线程中使用recv()或recvfrom()接收请求,处理请求后使用send()或sendto()返回响应。适用场景:大规模的Web服务器(如Apache)、数据库服务器、在线游戏服务器等,需要处理大量并发连接的应用。
I/O多路复用并发服务器模型1. 服务器初始化文件描述符集合,将监听套接字加入集合。1. 使用socket()创建套接字,bind()绑定地址和端口,listen()监听连接请求。使用FD_ZERO()、FD_SET()等宏初始化文件描述符集合。优点:资源消耗少,能同时高效处理多个IO行为,适用于高并发场景。
2. 使用select()函数监听文件描述符集合中的套接字。2. 使用select()函数等待文件描述符集合中的套接字变为可读或可写状态。缺点:只能处理并发产生的IO事件,无法处理CPU计算密集型任务。
3. 若监听套接字可读,则接受新的连接请求,将新连接套接字加入集合。3. 若监听套接字可读,使用accept()接受新的连接请求,并将新连接的套接字加入文件描述符集合。适用场景:HTTP请求、网络传输等IO行为密集的场景。
4. 若已连接套接字可读,则读取数据并处理请求,返回响应。4. 若已连接套接字可读,使用recv()或recvfrom()读取数据,处理请求后使用send()或sendto()返回响应。
5. 重复步骤2-4,直到服务器关闭。5. 使用while循环不断重复步骤2-4,直到服务器关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值