【UNIX网络编程】客户/服务器程序设计范式

前言:

这本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.在高并发的情况下使用多线程的处理模式,最好是预先创建线程,并且能实时监控线程忙碌程度,需要的时候开辟新线程。

服务器设计范式基本就在这里面,还有多进程,进程又多线程的方式在此不表。这些方式需要好好熟悉。以后可以自己尝试设计处理的服务器。











你可能感兴趣的:(【UNIX网络编程】客户/服务器程序设计范式)