前言:
这本UNIX网络编程到这里也就结束了,回想一下,确实自己后一段的学习有放松了,昨天和同学交流,他说他每天十二点睡,早上4.30起床,学习到8.30再去实验室做老师的活,想来我这一部书第一遍就学习了两个月,是在是自愧不如,他是专硕,今年九月就要找工作了,相信他的前途一定是一片光明的。我自己也绝对不能再犹豫,一定做出成绩来!
1.基本设计模式分类:
a.迭代服务器(iterative server),这种服务器一次只能服务一个客户,服务完了之后才能服务下一个,实际运用中使用很少。
b.并发服务器(concurrent server),这种服务器为每一个客户调用一个fork,或者创建一个新的线程,每个客户都有一个对应的线程或者进程服务,大多数UNIX服务器都属于这种。
并发服务器变体:
a.预先派生子进程:这种方式如果同时处理的客户数超过了进程数,新的客户会在完成了三次握手之后在监听队列里等待。父进程最好检查可用子进程,在必须的时候开辟新的子进程
b.预先派生子线程
c.子进程/线程调用accept获取客户套接字
d.主进程/线程调用accept获取客户套接字再传递给子进程/线程
2.各种设计实现方式:
迭代服务器:
只需要在无限的for循环中执行处理程序就行了,完成了一个客户之后重新调用accept获取新的客户。
并发服务器:
a.每个客户派生一个子进程:
1.主进程调用accept,获取新的客户套接字。
2.调用fork();子进程关闭监听套接字,处理新接收的客户;主进程关闭新客户的套接字。
3.主进程再次调用accept,阻塞直到下一个客户到达。
4.子进程结束之后,会发送SIGCHLD信号,主进程捕获该信号,并且在信号处理函数中调用waitpid。
b.预先派生子进程
1.主进程创建监听套接字;
2.主进程创立一定数量的子进程;
3.主进程调用pause();其他处理都由子进程完成;
4.子进程处理方式与迭代服务器相同,各自调用accept,处理完成之后再次调用accept。
在子进程调用accept之上又有各种不同的方式:
a.各个子进程同时调用accept,无上锁保护
这种方式会引起惊群问题,所有的子进程会同时唤醒,单只有一个可以接收到客户套接字,会带来额外的系统开销,当子进程变多的时候,这个问题会非常明显。每个子进程处理的客户数是差不多的,系统会自动分配给子进程。
b.各个子进程同时使用select函数,有消息来时,再调用非阻塞的accept。
同样会引起惊群,而且由于使用了select和accept两个函数处理,系统开销比直接调用accept还要大,不如直接调用accept。
c.accept使用文件锁保护
前面的方式相同,在调用accept之前调用fcntl上锁,阻塞在此,上锁成功后再调用accept,再解锁,再进行客户处理。每个子进程处理的客户数是差不多的,系统会自动分配给子进程
d.accept使用线程锁保护
要使用线程锁,1,互斥量必须放在所有进程共享的内存区中,2,必要告诉线程函数库,这是在不同进程之间共享的互斥锁。需要线程库支持PTHREAD_PROCESS_SHARED属性。
<span style="font-size:18px;">pthread_mutex_t * mptr;</span>
<span style="font-size:18px;">pthread_mutexattr_t mattr; int fd; fd = open("/dev/zero", O_RDWR, 0); mptr = mmap(0, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); close(fd); pthread_mutexattr_init(&mattr); pthread_mutexattr_setshared(&mattr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(mptr, &mattr);</span>
在父进程中调用accept:
在父进程中调用accept可以避免之前的惊群现象,但是在父进程中获取的套接字描述符必须要传递给子进程,而且必须要维护一个子进程的信息结构,=其中有:
a.子进程进程号
b.子进程管道描述符
c.子进程状态
运行过程:
1.父进程建立好监听套接字,然后建立字节流管道,调用fork()父进程存下进程ID,字节流管道0,关闭管道1.
2.子进程将字节流管道1复制到STDERR上,关闭不用的描述符,等待父进程发送客户的套接字描述符过来。然后调用处理函数,处理完毕之后给父进程发送一个字节的消息,表示处理完毕,父进程将该子进程的状态改为空闲,子进程继续阻塞在等待父进程发送套接字上。
3.父进程accept收到新的客户套接字之后将,查询是否有空闲的子进程,如果有,将它 的状态改成繁忙,然后发送套接字描述符给子进程。如果没有空闲子进程,则可以选择出错退出,或者选择派生新的子进程。收到子进程的通知的话,就将子进程状态改成空闲。
使用线程的并发服务器:
a.每个客户创建一个线程:新线程调用
<span style="font-size:14px;">pthread_detach(pthread_self());</span>脱离主线程,完毕后可以自行退出,然后执行处理程序。
b.预先创立子线程
子进程自行调用accept,,此时子线程使用线程锁,可以避免惊群问题。
主线程统一调用accept:
需要处理描述符通知的问题,解决方法为:子线程测试全局变量,符合要求时即是有客户套接字,获取之后修改。主线程收到客户时修改全局变量,通过条件变量通知自线程。
具体方法如下:
1.主线程建立一个存放客户套接字的数组,和iput与iget两个索引。最开始都置0。到收到客户时,iput++,存放进相应的数组位置上。
2.增加之前,主线程需要锁定互斥锁,增加完毕后,给条件变量发送信号,然后解开互斥锁。
3.子线程锁住互斥锁,测试iget是否与iput相等,相等的时候就调用条件变量wait函数,阻塞在此,直到收到条件变量信号,返回并锁住互斥锁。
当条件wait返回,或者iget等于iput时,获取客户套接字,增加iget,然后释放互斥锁,调用处理函数。
3.各种设计模式的性能:
这里只说控制进程所用的cpu时间,因为迭代处理器只有一个进程一个线程,所以基准为0。
统计结论:
1.使用线程比进程的开销小,cpu控制时间更短
2.使用加锁的方式,子进程/线程调用accept,比起不加锁的冲突方式更加快。cpu控制时间更短,其中线程锁又优于文件锁
3.预先创建线程/进程的方式,优于收到客户后创建线程的方式。
4.主线程/进程调用accept,然后发送套接字给子线程/进程的方式比子线程直接调用慢。
总结:
1.在没有高并发需求的时候可以使用迭代服务器,优点是编写简单。
2.在高并发的情况下使用多线程的处理模式,最好是预先创建线程,并且能实时监控线程忙碌程度,需要的时候开辟新线程。
服务器设计范式基本就在这里面,还有多进程,进程又多线程的方式在此不表。这些方式需要好好熟悉。以后可以自己尝试设计处理的服务器。