并发服务器模型引入
在循环服务器模型文章中讲解了循环服务器的实现方式和循环服务器模型存在的问题,那么针对这些问题应该如何改进那?
循环服务器模型的处理方式是串行,也就说必须需要前一步做完,后一步才能继续做,这样就会极大的浪费资源。那么要如何改进循环服务器那?我们可以改变思路:如果这些事情之间互不影响,各自执行各自的事情,这就是并行的思想。
与串行服务器不同,并发服务器对客户端的服务请求进行并发处理。例如:多个客户端同时发送请求时,服务器可以同时进行处理,而不是向循环服务器那样处理完毕一个客户端的请求之后再处理另一个请求。
并发服务器模型
1、多进程并发服务器模型
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include "sockErrHand.h"
#define PORT 5500
#define MAX 32
#define SIZE 512
void clientHandle(int cliSock)
{
char dataBuf[SIZE];
time_t t;
int rLen=-1;
printf("进程%d:正在接收客户发送的消息\n",getpid());
do
{
memset((void*)dataBuf,0,SIZE);
rLen=read(cliSock,(void*)dataBuf,SIZE);
if(strcmp("TIME\n",dataBuf)==0)
{
memset((void*)dataBuf,0,SIZE);
sprintf(dataBuf,"%d:",getpid());
time(&t);
ctime_r(&t,dataBuf+strlen(dataBuf));
Write(cliSock,(void*)dataBuf,strlen(dataBuf));
}
else
{
memset((void*)dataBuf,0,SIZE);
strcpy(dataBuf,"无效命令\n");
Write(cliSock,(void*)dataBuf,strlen(dataBuf));
}
}while(1);
Close(cliSock);
//没有回收子进程
}
int main()
{
int serSock=Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serAddr,cliAddr;
socklen_t cliAddrLen=sizeof(cliAddr);
bzero(&serAddr,sizeof(serAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serAddr.sin_port=htons(PORT);
Bind(serSock,(struct sockaddr*)(&serAddr),sizeof(serAddr));
Listen(serSock,MAX);
printf("SERVERLISTENING...\n");
while(1)
{
int cliSock=Accept(serSock,(struct sockaddr*)(&cliAddr),&cliAddrLen);
int pid=fork();
switch(pid)
{
case -1:
perror("fork");
break;
case 0:
//子进程处理函数
Close(serSock);
clientHandle(cliSock);
break;
default:
//关闭客户端连接
Close(cliSock);
}
}
close(serSock);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include "sockErrHand.h"
#define PORT 5500
#define IPSIZE 32
#define SIZE 512
int flag=1;
int main()
{
int cliSock=Socket(AF_INET,SOCK_STREAM,0);
char strIP[IPSIZE];
scanf("%s",strIP);
struct sockaddr_in serAddr;
bzero(&serAddr,sizeof(serAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_port=htons(PORT);
inet_pton(AF_INET,strIP,(void*)(&serAddr.sin_addr));
Connect(cliSock,(struct sockaddr*)(&serAddr),sizeof(serAddr));
printf("客户端已经成功连接服务器\n");
char dataBuf[SIZE];
int readLen=-1;
while(1)
{
memset((void*)dataBuf,0,SIZE);
readLen=Read(STDIN_FILENO,(void*)dataBuf,SIZE);
Write(cliSock,(void*)dataBuf,readLen);//阻塞。
memset((void*)dataBuf,0,SIZE);
readLen=Read(cliSock,(void*)dataBuf,SIZE);
Write(STDOUT_FILENO,(void*)dataBuf,readLen);
}
Close(cliSock);
return 0;
}
多进程服务器模型的特点:
(1)稳定。进程之间互不影响。
(2)内存消耗大。
2、多线程并发服务器模型
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include "sockErrHand.h"
#define PORT 5500
#define MAXCLI 32
#define SIZE 512
void* clientHandle(void* pcliSock)
{
printf("线程号:%x正在接收数据\n",(unsigned int)pthread_self());
pthread_detach(pthread_self());
int cliSock=*((int*)pcliSock);
int readLen=-1;
char dataBuf[SIZE];
time_t t;
while(1)
{
memset((void*)dataBuf,0,SIZE);
readLen=Read(cliSock,dataBuf,SIZE);
if(strcmp("TIME\n",dataBuf)==0)
{
time(&t);
memset((void*)dataBuf,0,SIZE);
ctime_r(&t,dataBuf);
}
else
{
memset((void*)dataBuf,0,SIZE);
strcpy(dataBuf,"无效命令\n");
}
Write(cliSock,(void*)dataBuf,strlen(dataBuf));
}
pthread_exit(NULL);
}
int main()
{
int serSock=Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serAddr;
bzero(&serAddr,sizeof(serAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serAddr.sin_port=htons(PORT);
Bind(serSock,(struct sockaddr*)(&serAddr),sizeof(serAddr));
Listen(serSock,MAXCLI);
printf("SERVERLISTEN\n");
while(1)
{
struct sockaddr_in cliAddr;
socklen_t cliAddrLen=sizeof(cliAddr);
int cliSock=Accept(serSock,(struct sockaddr*)(&cliAddr),&cliAddrLen);
pthread_t pthID;
pthread_create(&pthID,NULL,clientHandle,(void*)(&cliSock));
}
Close(serSock);
pthread_exit(NULL);
}
多线程服务器模型的特点:
(1)稳定性相对较差。一个线程异常结束就会导致整个程序结束。
(2)线程本身相比较于进程是减少了系统的开销,但在引用线程后会自然而然的带来“线程同步”问题,如果采用锁机制就会严重的降低程序的性能。
(3)线程在创建时最多只能创建大约380个,如果像创建更多线程就需要修改线程的属性。
进程池和线程池
在上面的两个服务器程序中,都是通过动态创建进程或线程的方式来处理客户端的连接,即每来一个客户端连接,就创建一个线程或进程去处理,等到客户端断开连接,在销毁对应的线程或进程。动态创建的进程或线程通常是为专门的客户端服务的,这将导致系统中产生大量的进程或线程。而无论是这些进程或线程的创建还是销毁都是需要内核完成的,这就需要系统从用户空间转到内核空间,这样就会浪费大量的时间。使用上面的服务器程序,假设一种情况:如果所有的客户端都只是连接上后只发送一条“TIME”然后就断开连接,然后重新连接,这样就会导致服务器将大量的时间和资源都消耗在了创建和销毁进程或线程上了。基于这种情况就产生了“池”的概念。
“池”的概念
我们在编程时有时为了提高程序运行的效率,会使用“空间换时间”的方法,“池”其实就是这种方法的应用。由于服务器的硬件资源比较“充裕”,那么提高服务器性能就会很自然的想到“空间换时间”的方法,即浪费服务器的硬件资源,以换取其运行资源,这就是“池”的概念。“池”是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配。当服务器进入正式运行阶段时,如果需要相关的资源,就可以直接从“池”中获取,无需分配。这样就会比原来动态分配的方式的快一些。当客户端关闭连接时,只需要将所使用的资源再放回“池”中即可,这样也会减少释放资源的开销。这样就避免了服务器对内核频繁访问。常见的池有:内存池,进程池和线程池。
内存池
内存池是一种内存分配方式,通常我们都是使用new或malloc等函数申请分配内存,这样做的缺点在于:由于所申请的内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池则时真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存,这样可以使内存的分配效率提高。
进程池和线程池(类似)
进程池是由服务器预先创建一组子进程,这些子进程的数目再3-10个之间(一般情况)。线程池中的线程数量应该和CPU数量差不多。
进程池中的所有子进程都运行着相同的代码,并具有相同的属性,比如优先级和组ID等。
当有新任务来临时,主进程将通过某种方式选择进程池中的某一个子进程来为之服务。相比于动态创建子进程,选择一个已经存在的子进程显得代价小的多。至于主进程选择哪一个子进程来为新任务服务,则有两种方法:
(1)主进程使用某种算法来主动选择子进程。常见的算法有:轮流算法和随机算法。
(2)主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠再该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取得任务并执行,而其他子进程将继续睡眠再工作队列中。
当选好子进程后,主进程还需要使用某种通知机制来告诉子进程有新任务处理,并传递必要的数据。最简单的方式是,再父进程和子进程之间先建立好一条管道,然后通过管道来实现所有的进程之间的通信。在父线程和子线程之间传递数据可以通过贡献实现。
线程池主要用于:
(1)需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大。但对于长时间的任务,线程池的优点就不明显了。
(2)对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
(2)接受突发性的大量请求,但不至于是服务器因此产生大量的线程应用。