前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解服务器设计的基本模式

理解服务器设计的基本模式

作者头像
theanarkh
发布2020-05-08 14:00:18
7350
发布2020-05-08 14:00:18
举报
文章被收录于专栏:原创分享

前言:服务器是现代软件中非常重要的一个组成。今天分享一下服务器设计的一些模式。因为现代的服务器软件中,常见的都是基于TCP的,所以本文的内容也是基于TCP的。

首先我们先来了解,什么是服务器。顾名思义,服务器,重点是提供服务。那么既然提供服务,那就要为众人所知。不然大家怎么能找到服务呢。就像我们想去吃麦当劳一样,那我们首先得知道他在哪里。所以,服务器很重要的一个属性就是发布服务信息,服务信息包括提供的服务和服务地址。这样大家才能知道需要什么服务的时候,去哪里找。对应到计算机中,服务地址就是ip+端口。所以一个如果你想成为一个服务器,那么你就要首先公布你的ip和端口,但是ip和端口不容易记,不利于使用,所以又设计出DNS协议。这样我们就可以使用域名来访问一个服务,DNS服务会根据域名解析出ip。 一个介基tcp协议的服务器,基本的流程如下。

代码语言:javascript
复制
// 拿到一个socket用于监听
var socketfd = socket();
// 监听本机的地址(ip+端口)
bind(socketfd, 监听地址)
// 标记该socket是监听型socket
listen(socketfd)
// 阻塞等待请求到来
var socketForCommunication = accept(socket);

执行完以上步骤,一个服务器正式开始服务。下面我们看一下基于上面的模型,分析各种各样的处理方法。

1 串行处理请求

代码语言:javascript
复制
while(1) {
    var socketForCommunication = accept(socket);
    var data = read(socketForCommunication );
    handle(data);
    write(socketForCommunication, data );
}

我们看看这种模式的处理过程。假设有n个请求到来。那么socket的结构是。

这时候进程从accept中被唤醒。然后拿到一个新的socket用于通信。

这种模式就是从已完成三次握手的队列里摘下一个节点,然后处理。再摘下一个节点,再处理。如果处理的过程中有文件io,可想而知,效率是有多低。而且大并发的时候,socket对应的队列很快就不被占满。这是最简单的模式,虽然服务器的设计中肯定不会使用这种模式,但是他让我们了解了一个服务器处理请求的过程。

2 多进程模式

多进程式下又分为几种。 2.1 一个请求一个进程

代码语言:javascript
复制
while(1) {
    var socketForCommunication = accept(socket);
    if (fork() > 0) {
        // 父进程负责accept
    } else {
        // 子进程
        handle(socketForCommunication);
    }
}

这种模式下,每次来一个请求,就会新建一个进程去处理他。这种模式比串行的稍微好了一点,每个请求独立处理,假设a请求阻塞在文件io,那么不会影响b请求的处理,尽可能地做到了并发。他的瓶颈就是系统的进程数有限,大量的请求,系统无法扛得住。再者,进程的开销很大。对于系统来说是一个沉重的负担。 2.2 多进程accept 这种模式不是等到请求来的时候再创建进程。而是在服务器启动的时候,就会创建一个多个进程。然后多个进程分别调用accept。这种模式的架构如下。

代码语言:javascript
复制
for (let i = 0 ; i < 进程个数; i++) {
    if (fork() > 0) {
        // 父进程负责监控子进程
    } else {
        // 子进程处理请求
        while(1) {
            var socketForCommunication = accept(socket);
            handle(socketForCommunication);
        }
    }
}

这种模式下多个子进程都阻塞在accept。如果这时候有一个请求到来,那么所有的子进程都会被唤醒,但是首先被调度的子进程会首先摘下这个请求节点。后续的进程被唤醒后发现并没有请求可以处理。又进入睡眠。这是著名的惊群现象。改进方式就是在accpet之前加锁,拿到锁之后才能进行accept。nginx就解决了这个问题。但是据说现代操作系统已经在内核层面解决了这个问题。

2.3 进程池模式 进程池模式就是服务器创建的时候,创建一定数量的进程,但是这些进程是worker进程。他不负责accept请求。他只负责处理请求。主进程负责accept,他把accept返回的socket放到一个任务队列中。worker进程互斥访问任务队列从中取出请求进行处理。主进程的模式如下

子进程的模式如下

逻辑如下

代码语言:javascript
复制
for (let i = 0 ; i < 进程个数; i++) {
    if (fork() > 0) {
        // 父进程
    } else {
        // 子进程处理请求
        while(1) {
            // 互斥从队列中获取任务节点
            var task = getTask(queue);
            handle(task);
        }
    }
}
for (;;) {
    var newSocket = accept(socket);
    insertTask(queue);
}

多进程的模式同样适合多线程。

3 事件驱动

现在很多服务器(nginx,nodejs)都开始使用事件驱动模式去设计。从2的设计模式中我们知道,为了应对大量的请求,服务器需要大量的进程/线程。这个是个非常大的开销。而事件驱动模式,一般是配合单进程(单线程),再多的请求,也是在一个进程里处理的。但是因为是单进程,所以不适合cpu密集型,因为一个任务一直在占据cpu的话,后续的任务就无法执行了。他更适合io密集的。而使用多进程/线程的时候,一个进程/线程是无法一直占据cpu的,执行一定时间后,操作系统会执行进程/线程调度。这样就不会出现饥饿情况。事件驱动在不同系统中实现不一样。所以一般都会有一层抽象层抹平这个差异。这里以linux的epoll为例子。

代码语言:javascript
复制
// 创建一个epoll
var epollFD = epoll_create();
/*
 在epoll给某个文件描述符注册感兴趣的事件,这里是监听的socket,注册可读事件,即连接到来
 event = {
    event: 可读
    fd: 监听socket
    // 一些上下文
 }
*/
epoll_ctl(epollFD , EPOLL_CTL_ADD , socket, event);
while(1) {
    // 阻塞等待事件就绪,events保存就绪事件的信息,total是个数
    var total= epoll_wait(epollFD , 保存就绪事件的结构events, 事件个数, timeout);
    for (let i = 0; i < total; i++) {
        if (events[i].fd === socket) {
            var newSocket = accpet(socket);
            // 把新的socket也注册到epoll,等待可读,即可读取客户端数据
            epoll_ctl(epollFD , EPOLL_CTL_ADD , newSocket, 可读事件);
        } else {
            // 从events[i]中拿到一些上下文,执行相应的回调
        }
    }
}

这就是事件驱动模式的大致过程。本质上是一个订阅/发布模式。服务器通过注册文件描述符和事件到epoll中。等待epoll的返回,epoll返回的时候会告诉服务器哪些事件就绪了。这时候服务器遍历就绪事件,然后执行对应的回调,在回调里可以再次注册新的事件。就是这样不断驱动着。epoll的原理其实也类似事件驱动。epoll底层维护用户注册的事件和文件描述符。epoll本身也会在文件描述符对应的文件/socket/管道处注册一个回调。然后自身进入阻塞。等到别人通知epoll有事件就绪的时候,epoll就会把就绪的事件返回给用户。

代码语言:javascript
复制
function epoll_wait() {
    for 事件个数
        // 调用文件系统的函数判断
        if (事件[i]中对应的文件描述符中有某个用户感兴趣的事件发生?) {
            插入就绪事件队列
        } else {
            在事件[i]中的文件描述符所对应的文件/socket/管道等indeo节点注册回调。即感兴趣的事件触发后回调epoll,回调epoll后,epoll把该event[i]插入就绪事件队列返回给用户
        }
}

以上就是服务器设计的一些基本介绍。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 串行处理请求
  • 2 多进程模式
  • 3 事件驱动
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档