UNIX网络编程——常用服务器模型总结
下面有9种服务器模型分别是:
- 迭代服务器。
- 并发服务器,为每个客户fork一个进程。
- 预先派生子进程,每个子进程都调用accept,accept无上锁保护。
- 预先派生子进程,以文件锁的方式保护accept。
- 预先派生子进程,以线程互斥锁上锁的方式保护accept。
- 预先派生子进程,由父进程向子进程传递套接口描述字。
- 并发服务器,为每个客户请求创建一个线程。
- 预先创建线程,以互斥锁上锁方式保护accept。
- 预先创建线程,由主线程调用accept,并把每个客户连接传递给线程池中某个可用线程。
1、迭代服务器
典型代码:
socket bind listen for(;;) { connfd = accept(listenfd, (SA*)&cliaddr, &clilen); process_connection(connfd); close(connfd); }
优点:主要是编程简单。
缺点:处理完一个连接之后才能处理下一个连接,无并发可言,应用很少。
2.并发服务器,为每个客户fork一个进程
典型代码:
init_address(server_addr) listenfd = socket(AF_INET,SOCKET_STREAM,0); bind(listenfd, (SA*)server_addr, sizeof(serveraddr)); listen(listenfd,BACKLOG); for(;;) { connfd = accept(listenfd, (SA*)&cliaddr, &clilen); if(connfd < 0) { if(EINTR == errno) continue; else //error } if(fork() == 0) { close(c=listenfd); process_connection(connfd); //child process exit(0); } close(connfd);//parent, close connected socket }
早期的网络服务器每天处理几百或者几千个客户连接时,这种服务器模型还是可以应付的。
但是随着互联网业务的迅猛发展,繁忙的web服务器每天可能需要处理千万以上的连接,并发服务器的问题在于为每个客户现场fork一个子进程比较消耗cpu时间。
3.预先派生子进程,每个子进程都调用accept,accept无上锁保护
缺点:
- 服务器必须在启动的时候判断需要预先派生多少子进程
- 惊群现象(一个连接到来唤醒所有监听进程),不过较新版本的linux貌似修正了这个问题
优点:无须引入父进程执行fork的开销就能处理新到的客户
4.预先派生子进程,以文件锁的方式保护accept
本模型与3的区别仅仅是对accept(listenfd)使用了文件锁。
这种模型是为了解决以库函数的形式实现的accept不能在多个进程中引用同一个监听套接口的问题(源自BSD的unix,在内核中实现的accept可以引用)。使用文件锁保证了每个连接到来,只有一个进程阻塞在accept调用上。对于已经在内核中实现了accept的系统来说,这种模型至少增加了加锁解锁的开销,所以相对于第3种模型性能较低(特别是在消除了惊群问题的系统上)
5.预先派生子进程,以线程互斥锁上锁的方式保护accept
典型代码:
static pthread_mutex_t *mptr; void my_lock_init(char *pathname) { int fd; pthread_mutexattr_t mattr; //因为是相关进程所以可以使用/dev/zero设备创建共享内存 //优势是调用mmap创建共享存储区之前无需一个实际的文件 //映射/dev/zero自动创建一个指定长度的映射区 fd = open("/dev/zero", O_RDWR, 0,); // 将mptr映射到共享存储区 mptr = mmap(0, sizeof(pthread_mutex_t), PORT_READ | PORT_WRITE, MAP_SHARED, fd, 0); close(fd); pthread_mutexattr_init(&mattr); pthread_mutexattr_setpshared(&mptr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(mptr, &mattr); } void my_lock_wait() { pthread_mutex_lock(mptr); } void my_lock_release() { pthread_mutex_unlock(mptr); } int main(int argc, char **argv) { //init socket and address my_lock_init(pathname); for(i = 0; i < nchildren; ++i) { pids[i] = child_make(i, listenfd, addrlen); } for(;;) pause(); } pid_t child_make(int i, int listenfd, int addrlen) { pid_t pid; if((pid = fork) > 0) return pid; child_main(i, listenfd, addrlen); } void child_main() { for(;;) { my_lock_wait(); connfd = accept(listenfd, chiladdr, &clilen); my_lock_release(); web_child(connfd); close(connfd); } }
这种模型在模型4上做出了进一步的改进,由于以文件锁的方式实现保护会涉及文件系统,这样可能比较耗时,所以改进的办法是以pthread mutex互斥量代替文件锁。使用线程上锁保护accept不仅适用于同一进程内各线程上锁,也适用于不同进程间上锁在多进程环境下使用线程互斥锁实现同步有两点要求:
1.互斥锁必须放在由所有进程共享的内存区
2.必须告知线程库这是在不同的进程间共享的互斥锁
注意:目前很火的高性能web服务器的代表nginx就是采用的这种模型,网络上对nginx的研究很多。
6.预先派生子进程,由父进程向子进程传递套接口描述字
优势:不需要对accept上锁保护
劣势:
- 编码复杂—父进程必须跟踪子进程的忙闲状态,以便给空闲子进程传递新的套接口。在前述的预先派生子进程的例子中,父进程无需关心由哪一个子进程接收一个客户连接,操作系统会根据调度算法处理这些细节。采用这种模型的结果是这些进程无法均衡的处理连接。
- 父进程通过字节流管道把描述子传递到各个子进程,并且各个子进程通过字节流管道写回单个字节,比起无论是使用共享内存区中的互斥锁还是使用文件锁实施的上锁和解锁都更费时。
7.并发服务器,为每个客户请求创建一个线程
典型代码:
for(;;) { connfd = accept(listenfd, cliaddr, &clilen,); pthread_create(&tid, NULL, &doit, (void *)connfd); } void * doit(void *arg) { pthread_detach(pthread_self()); web_child((int)arg); close((int)arg) return (NULL); }
优点:编码简单。
缺点:现场为每个连接创建线程相对于预先派生线程池来说比较耗时。
8.预先创建线程,以互斥锁上锁方式保护accept
优势:
- 编程简介,易于理解
- 线程池的方式避免了现场创建线程的开销
- OS线程调度算法保证了线程负载的均衡性
这就是leader-follower模式一个线程等待连接到来,其他线程休眠;新连接到来后leader去处理连接,释放listenfd,其他线程竞抢监听套接口listenfd(可能有惊群的问题)。leader在处理完连接以后成为follower。
9.预先创建线程,由主线程调用accept,并把每个客户连接传递给线程池中某个可用线程
劣势:相对于模型8,该模型不仅需要使用pthread mutex额外还需要使用pthread cond。