网络服务器的几种并发服务模型

网络服务器的几种并发服务模型,第1张

服务程序最为关键的设计是并发服务模型,当前有以下几种典型的模型:

- 单进程服务,使用非阻塞IO

使用一个进程服务多个客户,通常与客户通信的套接字设置为非阻塞的,阻塞只发生在select()、poll()、epoll_wait()等系统调用上面。这是一种行之有效的单进程状态机式服务方式,已被广泛采用。

缺点是它无法利用SMP(对称多处理器)的优势,除非启动多个进程。此外,它尝试就绪的IO文件描述符后,立即从系统调用返回,这会导致大量的系统调用发生,尤其是在较慢的字节传输时。

select()本身的实现也是有局限的:能打开的文件描述符最多不能超过FD_SETSIZE,很容易耗尽;每次从select()返回的描述符组中扫描就绪的描述符需要时间,如果就绪的描述符在末尾时更是如此(epoll特别彻底修复了这个问题)。

- 多进程服务,使用阻塞IO

也称作 accept/fork 模型,每当有客户连线时产生一个新的进程为之服务。这种方式有时是必要的,比如可以通过操作系统获得良好的内存保护,可以以不同的用户身份运行程序,可以让服务运行在不同的目录下面。但是它的缺点也很明显:进程比较占资源,进程切换开销太大,共享某些信息比较麻烦。Apache 1.3就使用了这种模型,MaxClients数很容易就可以达到。

- 多线程服务,使用阻塞IO

也称之 accept/pthread_create模型,有新客户来时创建一个服务线程而不是服务进程。这解决了多进程服务的一些问题,比如它占用资源少,信息共享方便。但是麻烦在于线程仍有可能消耗光,线程切换也需要开销。

- 混合服务方式

所谓的混合服务方式,以打破服务方和客户方之间严格的1:1关系。基本做法是:

新客户到来时创建新的工作线程,当该工作线程检测到网络IO会有延迟时停止处理过程,返回给Server一个延迟处理状态,同时告诉 Server被延迟的文件描述符,延迟超时时间。Server会在合适的时候返回工作线程继续处理。注意这里的工作线程不是通过 pthread_create()创建的,而是被包装在专门用于处理延迟工作的函数里。

这里还有一个问题,工作线程如何检测网络IO会有延迟?方法有很多,比如设置较短的超时时间调用poll(),或者甚至使用非阻塞IO。如果是套接字,可以设置SO_RCVTIMEO和SO_SNDTIMEO选项,这样更有效率。

除了延迟线程,Server还应提供了未完成线程的支持。

如有有特别耗费时间的操作,你可以在完成部分工作后停止处理,返回给Server一个未完成状态。这样Server会检查工作队列是否有别的线程,如果有则让它们运行,否则让该工作线程继续处理,这可以防止某些线程挨饿。

典型的一个混合服务模型开源实现ServerKit

Serverkit的这些线程支持功能可简化我们的服务程序设计,效率上应该也是有保证的。

2. 队列(queue)

ServerKit提供的队列是一个单向链表,队列的存取是原子操作,如果只有一个执行单元建议不要用,因为原子操作的开销较大。

3. 堆(heap)

malloc()分配内存有一定的局限,比如在多线程的环境里,需要序列化内存分配操作。ServerKit提供的堆管理函数,可快速分配内存,可有效减少分配内存的序列化操作,堆的大小可动态增长,堆有引用计数,这些特征比较适合多线程环境。目前ServerKit堆的最大局限是分配单元必须是固定大小。

4. 日志记录

日志被保存在队列,有一个专门的线程处理队列中的日志记录:它或者调用syslog()写进系统日志,或者通过UDP直接写到远程机器。后者更有效。

5. 读写锁

GNU libc也在pthreads库里实现了读写锁,如果定义了__USE_UNIX98就可以使用。不过ServerKit还提供了读写锁互相转换的函数,这使得锁的应用更为弹性。比如拥有读锁的若干个线程对同一个hash表进行检索,其中一个线程检索到了数据,此时需要修改它,一种办法是获取写锁,但这会导致释放读锁和获取写锁之间存在时间窗,另一种办法是使用ServerKit提供的函数把读锁转换成写锁,无疑这种方式更有效率。

除了以上这些功能,ServerKit还提供了数据库连接池的管理(当前只支持MySQL)和序列化(Sequences),如感兴趣可参见相关的API文档。

二、ServerKit服务模块编写

ServerKit由3部分组成:server程序,负责加载服务模块、解析配置文件、建立数据库连接池;libserver,动态链接库,提供所有功能的库支持,包括server本身也是调用这个库写的;API,编程接口,你编写的服务模块和ServerKit框架进行对话的接口。

ServerKit需要libConfuse解析配置文件,所以出了安装ServerKit,还需要安装libConfuse。关于libConfuse可参考 http://www.nongnu.org/confuse/ 。

下面我们看一个简单的服务模块FOO:

#include <confuse.h>

#include <server.h>

static long int sleep_duration

static int FOO_construct()

{

fprintf(stderr, "FOO_construct\n")

return 1

}

static int FOO_prestart(cfg_t *configuration)

{

fprintf(stderr, "FOO_prestart\n")

return 1

}

static void * FOO_operator(void *foobar)

{

fprintf(stderr, "FOO_operator\n")

for() sleep(sleep_duration)

return NULL

}

static void FOO_report(void)

{

fprintf(stderr, "FOO_report\n")

}

static cfg_opt_t FOO_config[] = {

CFG_SIMPLE_INT("sleep_duration", &sleep_duration),

CFG_END()

}

static char *FOO_authors[] = {"Vito Caputo <vcaputo@pengaru.com>", NULL}

SERVER_MODULE(FOO,0,0,1,"Example module that does nothing but sleep")

按以下方法编译:

$ gcc -c -fPIC -pthread -D_REENTRANT -g FOO.c

$ gcc -shared -lserver -lconfuse -lpthread -g -e __server_module_main -o FOO.so FOO.o

-e选项指定程序运行入口,这使得你可以直接在命令行敲 ./FOO.so 运行模块。

server程序根据环境变量SERVER_PERSONALITY_PATH定位主目录,并查找主目录下的c11n作为配置文件,动态加载的模块需放在主目录下的modules目录。

$ export SERVER_PERSONALITY_PATH=`pwd`

$ mkdir modules

$ cp FOO.so modules

$ vi c11n

c11n的内容:

identity = "any_id"

FOO {

sleep_duration = 1

}

identity标识server实例,用ps可看到程序名称形如server.identity,本例为server.any_id。

执行server启动服务程序。

三、ServerKit其他功能缺陷

缺乏daemon模式;

只能运行在Linux box;

DB pool只支持MySQL;

Heap管理内存的功力有限

服务器创建并绑定套接字后fork出几个子进程,子进程中分别进行accept(该函数为阻塞函数)、recv、处理数据然后再次acept,这样循环下去。所有客户端发来的信息都是直接由子进程处理。

例程

代码如下,在处理客户端请求之前,服务器先fork了3个子进程,然后将客户端的请求直接交由子进程处理。

该例程中,服务器fork子进程后,子进程监听并接收客户端的信息,然后打印客户端发来的信息和自己的id(id代表自己是第几个子进程)

服务器端代码:

/**************************************

author:arvik

purpose:test the server simultaneity

email:1216601195@qq.com

csdn: http://blog.csdn.net/u012819339

**************************************/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <signal.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#define BUFFLEN 1024

#define SERVER_PORT 8887

#define BACKLOG 5

#define PIDNUMB 3

static void handle_connect(int s_s, int id)

{

int s_c

struct sockaddr_in from //client addr

socklen_t len = sizeof(from)

while(1)

{

s_c = accept(s_s, (struct sockaddr*)&from, &len)

char buff[BUFFLEN]

memset(buff, 0, BUFFLEN)

int n = recv(s_c, buff, BUFFLEN, 0) //non block

if(n >0)

{

printf("This process id is: %d \nreveive from client: %s\n", id, buff)

}

close(s_c)

}

}

void sig_int(int num)

{

exit(1)

}

int main(int argc, char **argv)

{

int s_s

struct sockaddr_in local

signal(SIGINT, sig_int)

s_s = socket(AF_INET, SOCK_STREAM, 0)

memset(&local, 0, sizeof(local))

local.sin_family = AF_INET

local.sin_addr.s_addr = htonl(INADDR_ANY)

local.sin_port = htons(SERVER_PORT)

bind(s_s, (struct sockaddr*)&local, sizeof(local))

listen(s_s, BACKLOG)

pid_t pid[PIDNUMB]

for(int i = 0i<PIDNUMBi++)

{

pid[i] = fork()

if(pid[i] == 0)

{

handle_connect(s_s, i)

}

}

sleep(100)

close(s_s)

return 0

}

客户端代码:

/**************************************

author:arvik

purpose:test the server simultaneity

email:1216601195@qq.com

csdn: http://blog.csdn.net/u012819339

**************************************/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <unistd.h>

#include <signal.h>

#define BUFFLEN 24

#define SERVER_PORT 8887

int main()

{

int s_c

struct sockaddr_in server

char buff[] = "hello"

s_c = socket(AF_INET, SOCK_STREAM, 0)

memset(&server, 0, sizeof(server))

server.sin_family = AF_INET

server.sin_addr.s_addr = htonl(INADDR_ANY)//any local address

server.sin_port = htons(SERVER_PORT)

connect(s_c, (struct sockaddr*)&server, sizeof(server))

send(s_c, buff, strlen(buff), 0)

sleep(1)

close(s_c)

return 0

}

所谓并发服务器就是在同一个时刻可以处理来自多个客户端的请求循环服务器是指服务器在同一时刻只可以响应一个客户端的请求。而且对于TCP和UDP套接字,这两种服务器的实现方式也有不同的特点。

1、TCP循环服务器:

首先TCP服务器接受一个客户端的连接请求,处理连接请求,在完成这个客户端的所有请求后断开连接,然后再接受下一个客户端的请求。创建TCP循环服务器的算法如下:

复制代码 代码如下:

socket(……)//创建一个TCP套接字

bind(……)//邦定公认的端口号

listen(……)//倾听客户端连接

while(1) //开始循环接收客户端连接

{

accept(……)//接收当前客户端的连接

while(1)

{ //处理当前客户端的请求

read(……)

process(……)

write(……)

}

close(……)//关闭当前客户端的连接,准备接收下一个客户端连接

}

TCP循环服务器一次只处理一个客户端的请求,如果有一个客户端占用服务器不放时,其它的客户机连接请求都得不到及时的响应。因此,TCP服务器一般很少用循环服务器模型的。

2、TCP并发服务器:

并发服务器的思想是每一个客户端的请求并不由服务器的主进程直接处理,而是服务器主进程创建一个子进程来处理。创建TCP并发服务器的算法如下:

复制代码 代码如下:

socket(……)//创建一个TCP套接字

bind(……)//邦定公认的端口号

listen(……)//倾听客户端连接

while(1) //开始循环接收客户端的接收

{

accept(……)//接收一个客户端的连接

if(fork(……)==0) //创建子进程

{

while(1)

{ //子进程处理某个客户端的连接

read(……)

process(……)

write(……)

}

close(……)//关闭子进程处理的客户端连接

exit(……) //终止该子进程

}

close(……)//父进程关闭连接套接字描述符,准备接收下一个客户端连接

}

TCP并发服务器可以解决TCP循环服务器客户端独占服务器的情况。但同时也带来了一个不小的问题,即响应客户机的请求,服务器要创建子进程来处理,而创建子进程是一种非常消耗资源的操作。

3、UDP循环服务器:

UDP服务器每次从套接字上读取一个客户端的数据报请求,处理接收到的UDP数据报,然后将结果返回给客户机。创建UDP循环服务器的算法如下:

1 socket(……)//创建一个数据报类型的套接字 2 bind(……)//邦定公认的短口号 3 while(1) //开始接收客户端的连接 4 { //接收和处理客户端的UDP数据报 5 recvfrom(……)6 process(……)7 sendto(……)//准备接收下一个客户机的数据报 8 }

消除行号

因为UDP是非面向连接的,没有一个客户端可以独占服务器。只要处理过程不是死循环,服务器对于每一个客户机的请求总是能够处理的。

UDP循环服务器在数据报流量过大时由于处理任务繁重可能造成客户技数据报丢失,但是因为UDP协议本身不保证数据报可靠到达,所以UDP协议是允许丢失数据报的。

鉴于以上两点,一般的UDP服务器采用循环方式4、UDP并发服务器把并发的概念应用UDP就得到了并发UDP服务器,和并发TCP服务器模型一样是创建子进程来处理的。

创建UDP并发服务器的算法如下:

复制代码 代码如下:

socket(……)//创建一个数据报类型的套接字

bind(……)//邦定公认的短口号

while(1) //开始接收客户端的连接

{ //接收和处理客户端的UDP数据报

recvfrom(……)

if(fork(……)==0) //创建子进程

{

rocess(……)

sendto(……)

}

}

除非服务器在处理客户端的请求所用的时间比较长以外,人们实际上很少用这种UDP并发服务器模型的。

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

创建子进程会带来系统资源的大量消耗,为了解决这个问题,采用多路复用I/O模型的并发服务器。采用select函数创建多路复用I/O模型的并发服务器的算法如下:

初始化(socket,bind,listen)

复制代码 代码如下:

while(1)

{

设置监听读写文件描述符(FD_*)

调用select

如果是倾听套接字就绪,说明一个新的连接请求建立

{

建立连接(accept)

加入到监听文件描述符中去

}

否则说明是一个已经连接过的描述符

{

进行操作(read或者write)

}

多路复用I/O可以解决资源限制问题,此模型实际上是将UDP循环模型用在了TCP上面。这也会带了一些问题,如由于服务器依次处理客户的请求,所以可能导致友的客户会等待很久。


欢迎分享,转载请注明来源:夏雨云

原文地址:https://www.xiayuyun.com/zonghe/229894.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-09
下一篇2023-04-09

发表评论

登录后才能评论

评论列表(0条)

    保存