其实常见的几种服务器/客服端模型都可以,用餐厅,服务员,客人来解释。
- 餐厅:代表服务器
- 顾客:代表客服端
- 服务员:代表处理客人请求的事件
- 菜单:菜单方便顾客了解菜品和价格,客人读了菜单,然后点菜,相当于服务器发给客服端的数据
- 记账本:服务员记录顾客点了那些菜,方便后厨做菜和服务员收钱,相当于客服端给服务器发送数据
大致流程:
- 顾客进入餐厅(前提是餐厅门开了且正在营业∩﹏∩),
- 服务员就前来招待,
- 顾客会先看一下菜单,然后点餐,服务员在记账本记下菜品然后交给后厨做菜(现实中顾客对服务员的请求不光是点菜,有时候可能会问厕所在哪∩︿∩)
- 菜做好了,服务员把菜端上来,然后顾客开吃。
- 顾客吃完了,招呼一声服务员结账,顾客结完账没什么事就离开餐厅。
下面就以具体的模型分情况介绍。
======================================================================================
单线程循环模型
======================================================================================
这个餐厅只有一个服务员,这个服务要处理所有顾客的点菜,结账等请求,如果当前顾客的请求事件一直没有处理完,其他需要处理请求的顾客就会一直等待,有些顾客不愿等,就会到别家餐厅吃饭。
简单代码:
listen(serverfd, listen_num);
while(1)
{
connfd = accept(serverfd);
read(connfd);
write(connfd);
close(connfd);
}
- 优点:由于只请了一个服务员,所以节省了人工费
- 缺点:当顾客少时,服务员能即使处理请求,当顾客多时,服务员就忙不过来了,也会损失一部分顾客。
实际的服务器/客服端
优点:
- 简单、易于实现
- 没有同步、加锁这些麻烦事,也没有这些开销
缺点:
- 不能同时处理过多的客服端请求
- 没有利用多核cpu的优势
餐厅老板发现,顾客一多,服务员就忙不过来,干脆就每来一个顾客,就给他分配一个服务员,这个服务员就专门处理一个顾客的请求。
然后就变成下面的情况:
一个人来就餐,一个服务员去服务,然后客人会看菜单,点菜。 服务员将菜单给后厨。
二个人来就餐,二个服务员去服务……
五个人来就餐,五个服务员去服务……
而这种情况有分多进程和多线程
======================================================================================
多进程模型
======================================================================================
由于每个菜单和记账本都属于餐厅的资源(服务器的资源),而多进程就是给每一个服务员都分配一个菜单和记账本,服务员之间各用各的,没有竞争关系。
简单代码:
listen(serverfd, listen_num);
while(1)
{
connfd = accept(serverfd);
if ((child_pid = frok()) == 0) {
close(serverfd);
read(connfd);
write(connfd);
close(connfd);
}
close(connfd);
}
优点:可以同时满足多个顾客的请求。
缺点:成本高(每个服务员都有菜单和记账本),一个服务员还好,如果一百个服务员,那餐厅直接亏本。
======================================================================================
多线程模型
======================================================================================
老板发现给每个服务员都分配一个菜单和记账本成本太高,于是只准备一个菜单和记账本,如果一个服务员把菜单和记账本拿走了,只能等她把菜单和记账本归还后才能使用(这里相当于互斥锁),这样既可以解决同时服务多个顾客的问题,又可以不让成本太高。
简单代码:
listen(serverfd, listen_num);
while(1)
{
connfd = accept(serverfd);
pthread_create(&tid, NULL, &doit, (void *)connfd);
}
void *doit(void *arg)
{
pthread_detach(pthread_self());
read(connfd);
write(connfd);
close(connfd);
}
- 优点:可以解决同时服务多个顾客的问题,又可以不让成本太高。
- 缺点:成本还是很高,而且还进行互斥,增加了管理的麻烦。
具体代码可参考我的博客:https://blog.csdn.net/qq_40732350/article/details/89043038
======================================================================================
进程池和线程池模型
======================================================================================
老板又发现,多线程和多进程分配的服务员太多了,成本太高了,而且每次都是顾客进店后才让一个服务员准备
服务员准备的过程:穿上工作服,拿好菜单和记账本等等。
这样再时间上太慢,不能及时的服务顾客。
于是老板在开张时一次性安排10服务员,这10个服务员把所有准备工作都好然后迎接顾客,如果顾客太多,10个服务员不够,那就要靠餐厅的协调能力,而且得让顾客等一会儿。
代码就不写了,参考下面的图
- 优点:成本低,响应时间快
- 缺点:成本比单线程高,需要系统来协调。
具体代码可参考我的博客:https://blog.csdn.net/qq_36221862/article/details/73694504
======================================================================================
I/O多路复用模型
======================================================================================
老板又发现(这个老板老是发现∩▂∩)多线程和多进程的成本都很高,而且有的顾客有特别磨叽,半天也没点好菜,耽误了太多时间。
于是就用下面的办法。
餐厅只分配一个服务员,在顾客点菜时,服务员把菜单放在桌子上,马上离开去服务其他顾客,等顾客点完了通知一下服务员,然后服务员再来处理。
由于Linux中多了复用有两种方式:select 和 epoll
select
while(1)
{
select(fd_max+1, &cpy_reads, 0, 0, &timeout))== -1)
for(i=0; i<fd_max+1; i++) {
if(FD_ISSET(i, &cpy_reads)) {
if(i==serv_sock) { //有一个新顾客来了
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max<clnt_sock) fd_max=clnt_sock;
}else{ //顾客发送请求了
str_len=read(i, buf, BUF_SIZE);
if(str_len==0) { //顾客走了
FD_CLR(i, &reads);
close(i);
}else{
write(i , buf, str_len); //给顾客反馈
}
}
}
}
}
epoll
while(1)
{
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
for(i=0; i<event_cnt; i++ ) {
if(ep_events[i].data.fd==serv_sock) { //有一个新顾客来了
dr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
}else{
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);//顾客发送请求了
if (str_len==0) { //顾客走了
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
}else{
write(ep_events[i].data.fd, buf, str_len); //给顾客反馈
}
}
}
}
epoll又分 边缘触发 和水平触发
- 水平触发方式:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知。允许在任意时候重复检测IO的状态,没有必要每次描述符就绪后尽可能多的执行IO,select,poll就属于水平触发事件。只要满足要求就触发一个事件。
- 边缘触发方式:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知。在收到一个IO事件通知尽可能多的执行IO操作,因为如果再一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取就绪的描述符。信号驱动式IO就属于边缘触发。每当状态改变就触发一个事件。
- 优点:成本低,不用加互斥条件,可以最大限度的利用CPU
- 缺点:不能同时服务多个请求
可以参考我的博客:https://blog.csdn.net/QQ2558030393/article/details/90896317
小结:每种模型都有它使用的场景,适合的才是最好的。
参考:
http://server.51cto.com/sOS-589117.htm
https://cloud.tencent.com/developer/article/1376352
https://blog.csdn.net/yexiangCSDN/article/details/85988776
https://blog.csdn.net/qq_36221862/article/details/73694504
https://blog.csdn.net/QQ2558030393/article/details/91360750