CGI - 通用网关不通用

2020-07-03  本文已影响0人  401

CGI 起源

从 Web 服务器说起

在万维网初期,Web 服务器接受并解析客户端发来的 HTTP 请求,返回请求所需的静态资源如 HTML 和图片。

随着技术的发展,Web 服务器实现了对动态网页的支持 (早期):根据请求调用外部扩展应用程序进行处理,这些程序动态处理完成后将结果返回给 Web 服务器和客户端。如下图所示:

下文将把 外部扩展应用程序 这类程序称为 Web 应用程序

web_server.png

但是不同的 HTTP 服务器使用各自不同的方式与 Web 应用程序做交互,这导致即使对于同样的请求进行同样的处理,也要做不同的实现。正是在这样的背景下,CGI 应运而生。

何为 CGI

CGI(Common Gateway Interface)即通用网关接口最早于 1993 年由 NCSA(美国国家超级电脑应用中心)设计与开发,之后被其他开发者接纳从而成为一种标准。1997 年由 Ken Coar 领导制定了更详细的规范,并最终输出为 RFC 3875

它规定了 HTTP 服务器与 Web 应用程序之间该如何交互、如何传递数据、传递哪些数据等,其本质是一种 HTTP 服务器与 Web 应用程序之间的交互协议。CGI 制定的接口规范主要涉及三部分:环境变量、标准输入、标准输出

其中环境变量作为 HTTP 服务器与 Web 应用程序之间传递数据的一种方式,例如客户端通过 GET 请求传递而来的参数将被 HTTP 服务器放置于 QUERY_STRING 变量,请求方法放置在 REQUEST_METHOD ,HTTP服务器 拉起「Web 应用程序」的进程之后,Web 应用程序从环境变量中读取到请求方法、请求参数等必要信息。 Web 应用程序处理完成后将结果通过标准输出返回给 HTTP 服务器和客户端( Web 应用程序的输出将被 HTTP 服务器重定向至「HTTP 服务器与客户端之间建立的 socket」)。

所以何为 CGI?

CGI 是一种 HTTP 服务器与 Web 应用程序之间交互的协议。

使用了这套协议与 HTTP 服务器进行交互的程序被称为 CGI 程序。

由于早期这类程序通常采用轻量的脚本编写,所以也经常被称为 CGI 脚本

通用网关不通用

CGI 通用网关接口这个名字不难理解,其中网关接口表示 CGI 作为 HTTP 服务器与真正的 Web 应用程序之间的「中介」,起到接收请求和协议转换的「网关」作用。再结合当时不同 HTTP 服务器与 Web 程序之间具有不同的交互规则这一历史背景,因此 CGI 协议的目的并是提供一个通用交互协议。

但在企鹅厂工作期间,发现公司内部大量使用 CGI 技术,在部分团队中甚至是唯一的网关技术,久而久之,导致不少同事对 CGI 的理解产生了偏差。不少人将 CGI 直接理解为字面含义,即:通用网关接口。

在这种理解下,任何面向 Web 客户端实现了 HTTP 请求接收和协议转换的程序都被当作 CGI。在公司内部的日常讨论中,经常听到将 Java Servlet、Nodejs、Python 编写的 Web 程序称做为 CGI。这显然是错误的,CGI 仅仅是众多网关技术中的一种,并且作为一项古老的技术甚至不是主流的技术选择。

其他网关技术

CGI 在处理每个客户端请求时都会新建一个进程来运行 CGI 程序,如果是 CGI 脚本每次还需要初始化脚本解释器。这种 fork-and-execute 的处理模式会消耗大量的服务器资源,在面对大量客户端请求时,CGI 的技术将存在严重的性能问题。

所以后续又发展出了 Servlet、FastCGI 等更为高效的网关技术。

Apache HTTP Server

CGI 最早是为 NCSA 的 HTTPd 服务器设计和开发的,但之后 NCSA 的开发工作所有停滞。

而基于 HTTPd 的 Apache 服务器则于 1995 年初开始启动开发工作。从 1996 年开始成为最受欢迎的 Web 服务器之一。截止 2020 年 4 月,最为繁忙的百万个站点中有 29.12% 使用着 Apache,依然高于占比 25.54% 的 Nginx。

与之前的 HTTP 服务器相比,Apache 提供了 mod_* 模块的加载技术,从而能够将原先需独立运行的 Web 应用程序加载成 Apache 服务器的一个模块。

例如 Apache 通过 mod_php 并能加载 php 编写的 Web 应用程序,在服务器启动时并初始化 PHP 解析器,无需每次请求都重新启动解析器。同时由于 Web 应用程序作为 Apache 的一个模块运行,所以也不需要通过 CGI 协议实现两者之间的交互,请求 Web 应用程序将能够更加方便的使用 Apache 的提供的各种接口,也能够更好的融入 Apache 的生命周期管理。

通过 mod_* 模块技术,可以说将原本分离的 Web 应用程序和 HTTP 服务器融合在一起,因此 Apache 也更像一个完整的 Web 应用服务器。

另一方面,模块化设计的好处是 Apache 可以为不同的业务需求或场景提供不同的请求处理模型。

如 Apache 在 Unix 系统环境下就提供了三种不同的处理模型:

1. prefork

与 CGI 协议下的处理模型相似,采用多进程的方式处理请求。但 Apache 的 prefork 模式将事先创建一些工作进程,并通过一种管理进程来维护这些工作进程。

每次请求不再实时新建进程,而是从已经创建的进程中分配一个空闲进程来处理请求。

apache_mpm_prefork.png

2. worker
worker 模型采用多进程 + 多线程的方式处理请求。相对于 prefork 多进程模型,worker 模型可以处理更多的请求,这是由于每个请求由线程处理将消耗更少的资源。

但同时其稳定性相比 prefork 模式将有所下降,因为一个进程包含多个线程,每个线程处理一个请求,如果一个进程中的某个线程失败可能会导致整个进程失败,从而影响其他请求的处理。

apache_mpm_worker.png

3. event
上述两种模型都存在 keep-alive 问题。即当请求为 keep-alive 类型的请求时(尤其是 HTTP/1.1 默认使用了 keep-alive),传统模式下的 apache 的处理进程和线程会一直维持状态以阻塞等待客户端的数据(除非超时被释放),针对 keep-alive 长连接请求,apache 存在进一步优化的空间。

为解决 keep-alive 请求问题,event 模型中的监听线程除了监听和接收请求之外,还需要承担起更多的处理任务,负责处理所有 keep-alive 的 socket 套接字(且这些 socket 套接字已经被各种 handler 、协议过滤器处理完毕,只剩下一件事:发送数据给客户端),这样工作线程就可以释放出来处理新的请求。

另一方面,监听线程将采用 epoll 的方式管理 socket(最初采用 select,现已支持 epoll),从而提高请求的处理能力。

关于五种 IO 模型(包括 IO 多路复用的 select\poll\epoll) 等知识后续会单独梳理出一篇博客。

apache_mpm_event.png

epoll 的优化主要在于:

FastCGI

为了解决传统 CGI 效率差,难以扩展等缺陷,同时也是为了和同期出现的嵌入式模块 mod_* 技术相竞争,Open Market 设计开发了 CGI 的改进版 FastCGI。

FastCGI 的核心改进就是取缔传统 CGI 每个请求都新建进程的处理方式,减少因此产生的资源开销。FastCGI 工作模式如下:

fastcgi_model.png

同时 FastCGI 还有缓存机制进一步提高 CGI 脚本的执行效率,即首次调用时会编译脚本并将编译结果保存到缓存中,而下次接受到新请求时会优先转向这个编译过的代码,而不用每次都调用解释器来重新解释脚本。当更改了脚本,加速器的临时缓存也会被清空以保证调用的是新版本的脚本。

Servlet

Servlet 狭义上只是一种 Java Web 程序中的一种组件,它提供了编写 Web 应用程序的一系列接口。

但这里提及 Servlet 更多的是指广义上所代表的 Web 程序技术。在上文介绍的 CGI 中,HTTP 服务器与 CGI 程序属于不同的程序,分别跑在不同的进程中,虽然 CGI 程序进程可被 HTTP 服务器管理,但毕竟不在同一个内存空间中,所以两者之间需要通过 CGI 协议进行交互。

而 Servlet 所采用的技术则是将 HTTP 服务器和 Web 应用程序(相当于 CGI)整合在一起。由于 Servlet 是一种 Java 组件,开发者编写的 Servlet 程序需要运行在相应的具有 JVM 环境的 Servlet 容器中。我们将这类容器称为 Web 容器

那么 Servlet 最终面向客户端的将是一个完整统一的 Web 服务器(HTTP 模块 + Servlet 容器模块),例如 Tomcat 就是这类服务器的典型。Servlet 处理模型如下图所示:

servlet_model.png

基于 Servlet 的 Web 应用服务器内置了 HTTP 模块,对于 HTTP 与 Servlet 程序之间的通信制定了一套自己的协议,并且对并发请求下的线程的管理、调度等均有完整的实现。

当然除此之外,基于 Servlet 的 Web 应用服务器也可以与其他 HTTP 服务器如 Apache 结合使用。将 HTTP 服务器单拎出来,可以更好的发挥其优秀的静态资源处理能力。

servlet_model_1.png

与 CGI 相比,Servlet 采用多线程处理请求可有效减少请求处理时所消耗的资源,采用线程池和有效的实例生命周期的管理,则进一步增加资源复用率,提高请求处理的性能。同时在编程层面也提供了强大又便利的接口,提供更为高效的开发能力。

WSGI

WSGI(Python Web Server Gateway Interface)即 Web 服务器网关接口。是专门为 Python 设计的网关协议,用来描述和规范 HTTP 服务器如何与 Web 应用程序交互。

在 CGI 和 FastCGI 协议之上编写异步 Web 程序存在诸多不便,因此 Python 社区设计开发了 WSGI 协议。实现 WSGI 协议的 WSGI 服务器从 HTTP 服务器那接收到请求后,将把环境变量(请求参数)以及一个 callback 回调函数传递给 Web 应用程序,Web 应用程序处理完成后通过调用 callback 回调函数将响应返回给 HTTP 服务器。

WSGI 协议包含两个部分,一个是 WSGI 服务器端(server),另一个则是应用程序端(application)。即 WSGI 对上文中的 Web 应用程序也有比较严格协议规定。固编写 Web 应用程序通常需要使用支持 WSGI 协议的应用框架,如 Django、Flask 等。

WSGI 对进程/线程没有明确的限制,所以实现时可以各自采用不同的处理模型。

Node.js

Node.js 不是一门语言,也不是应用层框架。

Node.js 是能够在服务器端运行 JS 的开源、跨平台 JS 运行时环境

nodejs_architecture.png

Node.js 需要包含:

由上述各部分一起构建出了一个能够在服务器端使用 JavaScript 来编写各种服务器端应用的运行时环境,我们称之为 Node.js。

Node.js 经常被用来实现 Web 服务器,但与上文涉及的诸多 Web 服务器相比,Node.js 有许多独具特色的设计。

由上文 Node.js 的架构图可以知晓,Node.js 内置了 HTTP 相关的 http 模块,即 Node.js 平台已经提供了对 HTTP 请求的解析能力。开发者可以在此基础能力之上很方便的开发出 Web 应用,结合上文的内容,甚至可以称其能够很方便的开发出 Web 服务器,实际上这也正是 Node.js 的设计初衷之一。

JavaScript 本身是一门单线程的语言,所以 Node.js 本身所采用的请求处理模型与上文提及的多进程/多线程都有所不同。

Node.js 采用单线程 + 事件驱动 + 非阻塞 IO的技术实现了对请求(主要针对 IO 密集型请求)的高并发处理。

事件驱动模型如下图所示:

nodejs_event.png

而每次事件循环中所做的处理如下:

nodejs_event_handler.png

参考资料

Common Gateway Interface Wikipedia
通用网关接口 Wikipedia
RFC 3875 - CGI 规范 v1.1
李勇.CGI在嵌入式WEB服务器中的应用和实现[J].微计算机信息,2008(30):110-111+184.
What is Common Gateway Interface (CGI)? StackOverFlow
万法归宗——CGI
Apache HTTP Server Wikipedia
mod_perl Wikipedia
What is mod_php? StackOverFlow
Apache HTTP 服务器官方文档
The C10K problem
httpd三种MPM的原理剖析
FastCGI Wikipedia
FastCGI_Specification
FastCGI 协议规范中文版
Java servlet Wikipedia
Web服务器网关接口
PEP 3333
Node.js Wikipedia
Node.js GitHub
Node.js 官方中文文档
NodeJS Architecture & Concurrency Model
深入理解Node.js 中的进程与线程

上一篇 下一篇

猜你喜欢

热点阅读