并发服务器的实现(进程与线程)

本文探讨了并发服务器的实现,包括循环服务器与并发服务器的区别,并详细阐述了并发服务器通过创建子进程和使用线程的两种方法。重点讨论了使用子进程的方式,分析了父子进程间的资源独立性,以及在线程实现中如何处理共享资源的同步和调度,以实现高效且公平的缓冲区使用策略。

总体来说,服务器的运行模式大体有两类:循环服务器和并发服务器。所谓的循环服务器就是说他给客户端提供的服务时一个接着一个的,不能同时服务,也就是说当一个用户使用服务器的时候,其他用户不没能使用只能等待。这显然不符合实际服务器的要求,而并发服务器就可以很好地解决这个问题。并发服务器能够同时为多个客户端服务,同时并发服务的能力是服务器性能的一个重要指标。

并发服务器的实现总体有以下几种方法:

① 服务器和每个接收到的客户机进行连接,创建一个新的子进程处理这个客户机请求。

② 服务器预先创建多个子进程,由子进程处理客户机请求。这种方式叫做“预创建”服务器。 

③ 服务器用函数select实现对多个客户机连接的多路复用。

④ 超级服务器激活的服务器

这里我们主要讨论第一种实现,我们分为两种实现方式:利用子进程实现,利用线程的方式实现。这里服务器端的功能就是接受客户端的数据,并在服务器屏幕上打印出来,而且要求客户机可以主动断开链接,服务器能够为多个客户机服务。

利用子进程方式实现,服务器端代码:

/*server_fork.c*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/*定义通讯端口*/
#define MYPORT 4000
/*定义服务器端等待队列长度*/
#define BACKLOG 10
/*定义数据缓冲区大小*/
#define MAXDATASIZE 1024
/*定义客户端发给服务器端断开链接的信号*/
#define FINISH "close"

int main(void)
{
	int sock_fd,new_fd;
	int numbytes;
	int n;
	int sin_size;
	char buf[MAXDATASIZE];
/*IP地址数据结构*/
	struct sockaddr_in server_ip,client_ip;

/*创建套接字*/
	if( (sock_fd=socket(AF_INET,SOCK_STREAM,0)) == -1 )
	{
		perror("socket");
		exit(1);
	}

/*设置协议族为IPv4,端口为自定义端口,地址为本地任意可用IP地址*/
	server_ip.sin_family = AF_INET;
	server_ip.sin_port = htons(MYPORT);
	server_ip.sin_addr.s_addr = htonl(INADDR_ANY);
	n = 1;

/*设置套接字选项,使其可以重复使用端口*/
	setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
/*绑定地址和端口信息*/
	if( bind(sock_fd,(struct sockaddr *)&server_ip,sizeof(struct sockaddr)) == -1 )
	{
		perror("bind");
		exit(1);
	}
/*监听来自客户端的链接,队列长度为BACKLOG*/
	if( listen(sock_fd,BACKLOG) == -1 )
	{
		perror("listen");
		exit(1);
	}

/*主操作部分*/
	while(1)
	{
		sin_size = sizeof(struct sockaddr);
		memset(buf,0,sizeof(buf));
/*接受下一个客户端链接*/
		new_fd = accept(sock_fd,(struct sockaddr *)&client_ip,&sin_size);
		if( new_fd == -1 )
		{
			perror("accept");
			exit(1);
		}
/*创建子进程,并定义子进程中的操作*/
		if( !fork() )
		{
			printf("server get connection from %s\n",inet_ntoa(client_ip.sin_addr));
			while(1)
			{
/*接收来自客户端的信息*/
				if( (numbytes=recv(new_fd,(void *)buf,MAXDATASIZE,0)) == -1 )
				{
					printf("receive data error!\n");
					continue;
				}
/*判断是不是断开链接的信息,是则向客户端发送回应,并断开链接,否则打印信息*/
				if( strcmp(FINISH,buf) )
				{
					buf[numbytes] == '\0';
					printf("server: %s has been read!\n",buf);
				}
				else
				{
					printf("%s\n",buf);
/*发送回应*/
					if( (numbytes=send(new_fd,"Connection closed!\n",MAXDATASIZE,0)) == -1 )
					{
						printf("close error!\n");
						exit(1);
					}
					printf("Connection closed!\n");
					close(new_fd);
					exit(0);
				}
			}
		}
		close(new_fd);
	}
	close(sock_fd);

	return 0;
}

客户机端代码:

/*client.c*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>

/*定义通讯端口*/
#define MYPORT 4000
/*定义数据缓冲区大小*/
#define MAXDATASIZE 1024
/*定义客户端发给服务器端断开链接的信号*/
#define FINISH "close"

int main(int argc,char *argv[])
{
	int sock_fd;
	int numbytes;
	char buf[MAXDATASIZE];
	struct sockaddr_in server_ip;
/*主机信息结构体*/
	struct hostent *h;
	
	
	if( argc != 2)
	{
		printf("Usage: %s hostip\n",argv[0]);
		exit(0);
	}
/*将第二个参数转换为网络字节序的IP地址*/
	if( (h=gethostbyname(argv[1])) == NULL )
	{
		herror("gethostbyname");
		exit(1);
	}
	if( (sock_fd=socket(AF_INET,SOCK_STREAM,0)) == -1 )
	{
		perror("socket");
		exit(1);
	}
	server_ip.sin_family = AF_INET;
	server_ip.sin_port = htons(MYPORT);
/*将刚才得到的IP地址填入*/
	server_ip.sin_addr = *((struct in_addr *)h->h_addr);

/*与服务器建立链接*/
	if( connect(sock_fd,(struct sockaddr *)&server_ip,sizeof(struct sockaddr)) == -1 )
	{
		printf("connection error!\n");
		exit(1);
	}
	while(1)
	{
		memset(buf,0,sizeof(buf));
		printf("Please input a string:");
		scanf("%s",buf);
/*发送数据*/
		if( (numbytes=send(sock_fd,buf,MAXDATASIZE,0)) == -1 )
		{
			printf("data send error!\n");
			exit(1);
		}
/*如果输入的时断开链接的信号,则要监听网络上来自服务器的回应*/
		if( !strcmp(FINISH,buf) )
		{
			if( (numbytes=recv(sock_fd,(void *)buf,MAXDATASIZE,0)) == -1 )
			{
				printf("close error!\n");
				exit(1);
			}
			printf("%s\n",buf);
			close(sock_fd);
			printf("Byebye!\n");
			exit(0);
		}
	}

	return 0;
}

当多个客户机链接到服务器的时候,服务器会创建多个子进程分别为其服务。随着计算机硬件的发展,具有多核心,快处理频率的CPU越来越普及,因此在应用程序中也因该加以使用。在计算机系统中有一个比进程个更小的调度单位,那就是线程,一个进程可以分为多个线程,多个线程共享一个进程的资源,同时线程也是CPU调度的最小单位,也可以将线程看作轻量级的进程。

在本例中客户端不变,要将服务器端修改。由于调用fork函数创建子进程的时候,会将父进程的数据COW(copy on write),基本上就是复制了一份,所以父子进程的资源使用基本独立。但是线程则不一样,线程之间的资源是共享的,所以这时候互斥同步的线程调度就很重要。在本程序中,定了一个二维数组作为缓冲区,存放接受的字符串,在有限的数量内,每个线程有自己的缓冲区,当缓冲区占满时就要阻塞后来的线程并列队。这个时候就需要一种方式调度好缓冲区的使用。我们要实现的缓冲区调度功能是:当缓冲区空闲时可以使用,当一个满时使用另一个,所有都满时从第一个开始一个一个往后排列阻塞,均衡负载,线程和缓冲区没有固定的对应关系,完全随机使用,那个空闲就使用那个,没有空闲就从第一个依次等待。同时还要定义信号量来实现缓冲区的互斥使用。服务器代码如下:

/*server_fork.c*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <semaphore.h>

/*定义通讯端口*/
#define MYPORT 4000
/*定义服务器端等待队列长度*/
#define BACKLOG 10
/*定义数据缓冲区大小*/
#define MAXDATASIZE 1024
/*定义服务器同时可以服务的客户端数,为了方便测试,这里设置为2*/
#define MAXTHREAD 2
/*定义客户端发给服务器端断开链接的信号*/
#define FINISH "close"

/*服务器套接字和线程套接字*/
int sock_fd,new_fd[MAXTHREAD];
int numbytes;
int n;
int sin_size;
/*定义标志缓冲区是否可用的标志*/
int flag[MAXTHREAD];
/*定义缓冲区,一共MAXTHREAD个,每个MAXDATASIZE字节*/
char buf[MAXTHREAD][MAXDATASIZE];
/*定义每个缓冲区的信号量,实现互斥操作*/
sem_t sem[MAXTHREAD];
/*线程号*/
pthread_t thread[MAXTHREAD];
/*IP地址数据结构*/
struct sockaddr_in server_ip,client_ip;

/*自定义函数,功能是找到合适的缓冲区来分配给线程使用,当缓冲区的标志为0时表示可用,则返回序号,否则不可用返回-1,进行其他处理*/
int find(void)
{
	int i = 0;
	
	for(i=0;i<MAXTHREAD;i++)
	{
		if( flag[i] == 0 )
			return i;
	}
	
	return -1;
}

/*线程处理函数*/
void *func(void *arg)
{
	int thrd_num = (int)arg;

/*执行P操作*/
	sem_wait(&sem[thrd_num]);
/*打印链接信息*/
	printf("server get connection from %s,thread %d.\n",inet_ntoa(client_ip.sin_addr),thrd_num);
	while(1)
	{
/*接收客户端信息*/
		if( (numbytes=recv(new_fd[thrd_num],(void *)buf[thrd_num],MAXDATASIZE,0)) == -1 )
		{
			perror("recv");
			pthread_exit(NULL);
		}
/*判断客户端的信息是不是断开信号,是则断开链接,否则打印收到的信息*/
		if( strcmp(FINISH,buf[thrd_num]) )
		{
			buf[thrd_num][numbytes] == '\0';
			printf("server: In the thread %d,%s has been read!\n",thrd_num,buf[thrd_num]);
		}
		else
		{
			printf("%s\n",buf[thrd_num]);
			if( (numbytes=send(new_fd[thrd_num],"Connection closed!\n",MAXDATASIZE,0)) == -1 )
			{
				printf("close error!\n");
				pthread_exit(NULL);
			}
			printf("In the thread %d,connection closed!\n",thrd_num);
/*关闭线程操作的套接字*/
			close(new_fd[thrd_num]);
/*执行V操作*/
			sem_post(&sem[thrd_num]);
/*将旗标设置为0,以便下一个线程使用*/
			flag[thrd_num] = 0;
			pthread_exit(NULL);
		}
	}
}
int main(void)
{
	int i,no = 0;
	int ret;

/*初始化缓冲区和信号量*/
	for(i=0;i<MAXTHREAD;i++)
	{
		memset(buf[i],0,MAXDATASIZE);
		sem_init(&sem[i],0,1);
	}
/*创建套接字,类型为流式,默认TCP*/
	if( (sock_fd=socket(AF_INET,SOCK_STREAM,0)) == -1 )
	{
		perror("socket");
		exit(1);
	}
/*设置协议族为IPv4,端口为自定义端口,地址为本地任意可用IP地址*/
	server_ip.sin_family = AF_INET;
	server_ip.sin_port = htons(MYPORT);
	server_ip.sin_addr.s_addr = htonl(INADDR_ANY);
	n = 1;
/*设置套接字选项,使其可以重复使用端口*/
	setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
/*绑定地址和端口信息*/
	if( bind(sock_fd,(struct sockaddr *)&server_ip,sizeof(struct sockaddr)) == -1 )
	{
		perror("bind");
		exit(1);
	}
/*监听来自客户端的链接,队列长度为BACKLOG*/
	if( listen(sock_fd,BACKLOG) == -1 )
	{
		perror("listen");
		exit(1);
	}

/*主操作部分*/
	while(1)
	{
		sin_size = sizeof(struct sockaddr);

/*利用find函数找到合适的缓冲区,如果全部被占用,则从头依次向后排列,均衡阻塞*/
		if( (ret=find()) == -1 )
		{
			no++;
			no = no % MAXTHREAD;
/*等待占用线程结束,如果有多个在等待则形成等待队列*/
			pthread_join(thread[no],NULL);
		}
		else
			no = ret;
/*接受下一个客户端链接*/
		if( (new_fd[no] = accept(sock_fd,(struct sockaddr *)&client_ip,&sin_size)) == -1 )
		{
			perror("accept");
			exit(1);
		}
/*创建线程*/		
		if( pthread_create(&thread[no],NULL,func,(void *)no) )
		{
			perror("pthread_create");
			continue;
		}
/*创建成功,设置旗标*/
		else
		{
			printf("Thread %d has created!\n",no);
			flag[no] = 1;
		}
	}
/*关闭服务器套接字*/
	close(sock_fd);

	return 0;
}

两个服务器端的效果一样,只是实现的方式不同。本人初学网络编程,文中必有瑕疵,如有高见,请赐教!!!!






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值