用C写一个web服务器(四) CGI协议

* { margin: 0; padding: 0 }
body { font: 13.34px helvetica, arial, freesans, clean, sans-serif; color: black; line-height: 1.4em; background-color: #F8F8F8; padding: 0.7em }
p { margin: 1em 0; line-height: 1.5em }
table { font-size: inherit; font: 100%; margin: 1em }
table th { border-bottom: 1px solid #bbb; padding: .2em 1em }
table td { border-bottom: 1px solid #ddd; padding: .2em 1em }
input[type="text"],input[type="password"],input[type="image"],textarea { font: 99% helvetica, arial, freesans, sans-serif }
select,option { padding: 0 .25em }
optgroup { margin-top: .5em }
pre,code { font: 12px Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace }
pre { margin: 1em 0; font-size: 12px; background-color: #eee; border: 1px solid #ddd; padding: 5px; line-height: 1.5em; color: #444; overflow: auto }
pre code { padding: 0; font-size: 12px; background-color: #eee; border: none }
code { font-size: 12px; background-color: #f8f8ff; color: #444; padding: 0 .2em; border: 1px solid #dedede }
img { border: 0; max-width: 100% }
abbr { border-bottom: none }
a { color: #4183c4; text-decoration: none }
a:hover { text-decoration: underline }
a code,a:link code,a:visited code { color: #4183c4 }
h2,h3 { margin: 1em 0 }
h1,h2,h3,h4,h5,h6 { border: 0 }
h1 { font-size: 170%; border-top: 4px solid #aaa; padding-top: .5em; margin-top: 1.5em }
h1:first-child { margin-top: 0; padding-top: .25em; border-top: none }
h2 { font-size: 150%; margin-top: 1.5em; border-top: 4px solid #e0e0e0; padding-top: .5em }
h3 { margin-top: 1em }
hr { border: 1px solid #ddd }
ul { margin: 1em 0 1em 2em }
ol { margin: 1em 0 1em 2em }
ul li,ol li { margin-top: .5em; margin-bottom: .5em }
ul ul,ul ol,ol ol,ol ul { margin-top: 0; margin-bottom: 0 }
blockquote { margin: 1em 0; border-left: 5px solid #ddd; padding-left: .6em; color: #555 }
dt { font-weight: bold; margin-left: 1em }
dd { margin-left: 2em; margin-bottom: 1em }
sup { font-size: 0.83em; vertical-align: super; line-height: 0 }
kbd { display: inline-block; padding: 3px 5px; font-size: 11px; line-height: 10px; color: #555; vertical-align: middle; background-color: #fcfcfc; border: solid 1px #ccc; border-bottom-color: #bbb }
* { }
code[class*="language-"],pre[class*="language-"] { color: black; background: none; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; text-align: left; white-space: pre; word-spacing: normal; line-height: 1.5 }
pre[class*="language-"] { position: relative; margin: .5em 0; border-left: 10px solid #358ccb; background-color: #fdfdfd; background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); overflow: visible; padding: 0 }
code[class*="language"] { max-height: inherit; height: 100%; padding: 0 1em; display: block; overflow: auto }
:not(pre)>code[class*="language-"],pre[class*="language-"] { background-color: #fdfdfd; margin-bottom: 1em }
:not(pre)>code[class*="language-"] { position: relative; padding: .2em; color: #c92c2c; border: 1px solid rgba(0, 0, 0, 0.1); display: inline; white-space: normal }
pre[class*="language-"]::before,pre[class*="language-"]::after { content: ""; z-index: -2; display: block; position: absolute; bottom: 0.75em; left: 0.18em; width: 40%; height: 20%; max-height: 13em }
:not(pre)>code[class*="language-"]::after,pre[class*="language-"]::after { right: 0.75em; left: auto }
.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata { color: #7D8B99 }
.token.punctuation { color: #5F6364 }
.token.property,.token.tag,.token.boolean,.token.number,.token.function-name,.token.constant,.token.symbol,.token.deleted { color: #c92c2c }
.token.selector,.token.attr-name,.token.string,.token.char,.token.function,.token.builtin,.token.inserted { color: #2f9c0a }
.token.operator,.token.entity,.token.url,.token.variable { color: #a67f59; background: rgba(255, 255, 255, 0.5) }
.token.atrule,.token.attr-value,.token.keyword,.token.class-name { color: #1990b8 }
.token.regex,.token.important { color: #e90 }
.language-css .token.string,.style .token.string { color: #a67f59; background: rgba(255, 255, 255, 0.5) }
.token.important { font-weight: normal }
.token.bold { font-weight: bold }
.token.italic { font-style: italic }
.token.entity { cursor: help }
.namespace { opacity: .7 }
.token.tab:not(:empty)::before,.token.cr::before,.token.lf::before { color: #e0d7d1 }
pre[class*="language-"].line-numbers { padding-left: 0 }
pre[class*="language-"].line-numbers code { padding-left: 3.8em }
pre[class*="language-"].line-numbers .line-numbers-rows { left: 0 }
pre[class*="language-"][data-line] { padding-top: 0; padding-bottom: 0; padding-left: 0 }
pre[data-line] code { position: relative; padding-left: 4em }
pre .line-highlight { margin-top: 0 }
pre.line-numbers { position: relative; padding-left: 3.8em; counter-reset: linenumber }
pre.line-numbers>code { position: relative }
.line-numbers .line-numbers-rows { position: absolute; top: 0; font-size: 100%; left: -3.8em; width: 3em; letter-spacing: -1px; border-right: 1px solid #999 }
.line-numbers-rows>span { display: block; counter-increment: linenumber }
.line-numbers-rows>span::before { content: counter(linenumber); color: #999; display: block; padding-right: 0.8em; text-align: right }

前言

时隔一个多月,终于又有时间来更新我的服务器了,这次更新主要实现一下 CGI 协议。

先放上GitHub链接 tinyServer-GitHub-枕边书

作为一个服务器,基本要求是能受理请求,提取信息并将消息分发给 CGI 解释器,再将解释器响应的消息包装后返回客户端。在这个过程中,除了和客户端 socket 之间的交互,还要牵扯到第三个实体 - 请求解释器。

客户端负责封装请求和解析响应,服务器的主要职责是管理连接、数据转换、传输和分发客户端请求,而真正进行数据文档处理与数据库操作的就是请求解释器,这个解释器,在 PHP 中一般是 PHP-FPM,JAVA 中是 Servlet。

我们之前进行的处理多在客户端和服务器之间的通信,以及服务器的内部调整,这次更新的内容主要是后面两个实体之间的进程间通信。

进程间通信牵涉到三个方面,即方式形式内容

方式指的是进程间通信的传输媒介,如 Nginx 中实现的 TCP 方式和 Unix Domain Socket,它们分别有跨机器和高效率的优点,还有我实现的服务器用了很 low 的popen方式。

而形式就是数据格式了,我认为它并无定式,只要服务器容易组织数据,解释器能方便地接收并解析,最好也能节约传输资源,提高传输效率。目前的解决方案有经典的 xml,轻巧易理解的 json 和谷歌高效率的 protobuf。它们各有优点,我选择了 json,主要是因为有CJson库的存在,数据在 C 中方便组织,而在PHP中,一个json_decode()方法就完成了数据解析。

至于应该传输哪些内容呢?CGI 描述了一套协议:

CGI

通用网关接口(Common Gateway Interface/CGI)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。

CGI 是服务器与解释器交互的接口,服务器负责受理请求,并将请求信息解释为一条条基本的请求信息(在文档中被称为“元数据”),传送给解释器来解释执行,而解释器响应文档和数据库操作信息。

之前看了一下 CGI 的 RFC 文档,总结了几个重要点,有兴趣的可以看下底部参考文献。常见规范(信息太多,只考虑 MUST 的情况)如下:

CGI请求

  • 服务器根据 以 / 分隔的路径选择解释器;
  • 如果有 AUTH 字段,需要先执行 AUTH,再执行解释器;
  • 服务器确认 CONTENT-LENGTH 表示的是数据解析出来的长度,如果附带信息体,则必须将长度字段传送到解释器;
  • 如果有 CONTENT-TYPE 字段,服务器必须将其传给解释器;若无此字段,但有信息体,则服务器判断此类型或抛弃信息体;
  • 服务器必须设置 QUERY_STRING 字段,如果客户端没有设置,服务端要传一个空字符串“”
  • 服务器必须设置 REMOTE_ADDR,即客户端请求IP;
  • REQUEST_METHOD 字段必须设置, GET POST 等,大小写敏感;
  • SCRIPT_NAME 表示执行的解释器脚本名,必须设置;
  • SERVER_NAMESERVER_PORT 代表着大小写敏感的服务器名和服务器受理时的TCP/IP端口;
  • SERVER_PROTOCOL 字段指示着服务器与解释器协商的协议类型,不一定与客户端请求的SCHEMA 相同,如‘https://‘可能为HTTP;
  • CONTENT-LENGTH 不为 NULL 时,服务器要提供信息体,此信息体要严格与长度相符,即使有更多的可读信息也不能多传;
  • 服务器必须将数据压缩等编码解析出来;

CGI响应

  • CGI解释器必须响应 至少一行头 + 换行 + 响应内容;
  • 解释器在响应文档时,必须要有 CONTENT-TYPE 头;
  • 在客户端重定向时,解释器除了 client-redir-response=绝对url地址,不能再有其他返回,然后服务器返回一个 302 状态码;
  • 解释器响应 三位数字状态码,具体配置可自行搜索;
  • 服务器必须将所有解释器返回的数据响应给客户端,除非需要压缩等编码,服务器不能修改响应数据;

Nginx和PHP的CGI实现

介绍完了 CGI,我们来参考一下当前服务器 CGI 协议实现的成熟方案,这里挑选我熟悉的 Nginx 和 PHP。

在 Nginx 和 PHP 的配合中,Nginx 自然是服务器,而解释器是 PHP 的 SAPI。

SAPI

SAPI: Server abstraction API,指的是 PHP 具体应用的编程接口,它使得 PHP 可以和其他应用进行交互数据。

PHP 脚本要执行可以通过很多种方式,通过 Web 服务器,或者直接在命令行下,也可以嵌入在其他程序中。常见的 sapi 有apache2handler、fpm-fcgi、cli、cgi-fcgi,可以通过 PHP 函数php_sapi_name()来查看当前 PHP 执行所使用的 sapi。

PHP5.3 之前使用的与服务器交互的 sapi 是cgi,它实现基本的 CGI 协议,由于它每次处理请求都要创建一个进程、初始化进程、处理请求、销毁进程,消耗过大,使得系统性能大大下降。

这时候便出现了 CGI 协议的升级版本 Fast-CGI。

PHP-FPM

快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。

Fast-CGI 提升效率主要靠将 CGI 解释器长驻内存重现,避免了进程反复加载的损耗。PHP 的 sapi cgi-fcgi实现了 Fast-CGI 协议,提升了 PHP 处理 Web 请求的效率。

那么我们常见的 php-fpm 是什么呢?它是一种进程管理器(PHP-FastCGI Process Manager),它负责管理实现 Fast-CGI 的那些进程(worker进程),它加载php.ini信息,初始化 worker 进程,并实现平滑重启和其他高级功能。

Nginx 将请求都交给 php-fpm,fpm 选择一个空闲工作进程来处理请求。

纠偏

这里总结一下几个名字,以防混淆:

  • sapi,是 PHP 与外部进程交互的接口;
  • CGI/Fast-CGI(大写)是一种协议;
  • 本节中出现的 cgi(小写),是指 PHP 的 sapi,即实现 CGI 协议的一种接口。
  • php-fpm 是管理实现了Fast-CGI协议的进程的一个进程。

代码实现

介绍完了高端的Nginx服务器,说一下我的实现:

服务器解析 http 报文,实现 CGI 协议,将数据包装成 json 格式,通过 PHP 的cli sapi 发送至 PHP 进程,PHP 进程解析后响应 json 格式数据,服务器解析响应数据后包装成 http 响应报文发送给客户端。

http_parser

首要任务是解析 http 报文,C 中没有很丰富字符串函数,我也没有封装过常用的函数库,所以只好临时自己实现了一个util_http.c,这里介绍几个处理 http 报文时好用的字符串函数。

strtok(char str[], const *delimeter),将 delimeter 设置为 "\n",分行处理 http 报文头正好适合。

sscanf(const *str, format, dest1[,dest...]),它从字符串中以特定格式读取字符串,读取时的分隔符是空格,用它来处理 http 请求行十分方便。

至于解析 http 报文头的键值对应,没想到好方法,只好使用字符遍历来判断。

cJSON

cJSON 是一个 C 实现的用以生成和解析 json 格式数据的函数库,在 GitHub 上可以轻松搜到,只用两个文件 cJSON.ccJSON.h即可。

需要注意:C 作为强类型语言,往 json 内添加不同类型的数据要使用不同的方法,cJSON 支持 string, bool, number, cJSON object等类型。

这里简单地介绍一下生成和解析的一般方法;

生成:

cJSON *root; // 声明cJSON格式数据
root = cJSON_CreateObject(); // 创建一个cJSON对象
cJSON_AddStringToObject(root, "key", "value") // 往cJSON对象内添加键值对
char *output = cJSON_PrintUnformatted(root); // 生成json字符串
cJSON_Delete(root); // 别忘记释放内存

解析:

cJSON *json = cJSON_Parse(response_json);
value = cJSON_GetObjectItem(cJSON, "key");

当然,也可以声明 cJSON 类型的数据进行嵌套;

总结

说实话,用最基本的 C 写业务逻辑类的代码真的能折磨死人,仅一个字符串的操作就能让人欲仙欲死了。常用 C 开发的应该有各种函数库吧,就算没有自己的库也要去找开源库,自己造不了所有的轮子。

感觉服务器又被自己写残了,留了很多业务类型的坑也不知道什么时候会填,希望能有时间写一个工业级的东西。。。

如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我。博客一直在更新,欢迎 关注

参考: The Common Gateway Interface (CGI) Version 1.1

深入理解PHP内核 ? 生命周期和Zend引擎

搞不清FastCgi与PHP-fpm之间是个什么样的关系

时间: 2024-11-08 21:14:11

用C写一个web服务器(四) CGI协议的相关文章

用C写一个web服务器(二) I/O多路复用之epoll

.container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .container::before,.container::after { content: " "; display: table } .container::after { clear: both } .container::before,.container::after { content:

一起写一个 Web 服务器

导读: 本系列深入浅出的讲述了如何用 Python 从 0 开始,写一个 web 服务器,并让其与业界流行的 web 框架协同工作,最后还进一步完善了开头的 web 服务器 demo,让其可以支持多并发请求的处理,并解决了过程当中遇到的"僵尸进程"等一系列 socket/网络编程 中的常见问题,图文并茂.循序渐进,是篇非常不错的教程,对了解整个 Web 编程理论相当有帮助,推荐一看. 作者:伯乐在线 - 高世界 翻译 1.什么是 Web 服务器,以及怎样工作的? 一起写一个 Web 服

使用node.js 文档里的方法写一个web服务器

刚刚看了node.js文档里的一个小例子,就是用 node.js 写一个web服务器的小例子 上代码 (*^▽^*) //helloworld.js// 使用node.js写一个服务器 const http=require('http'); const hostname='127.0.0.1' const port=3000; const server = http.createServer((req,res)=>{ res.statusCode=200; res.setHeader('Cont

如何写一个Web服务器

最近两个月的业余时间在写一个私人项目,目的是在Linux下写一个高性能Web服务器,名字叫Zaver.主体框架和基本功能已完成,还有一些高级功能日后会逐渐增加,代码放在了github.Zaver的框架会在代码量尽量少的情况下接近工业水平,而不像一些教科书上的toy server为了教原理而舍弃了很多原本server应该有的东西.在本篇文章中,我将一步步地阐明Zaver的设计方案和开发过程中遇到的困难以及相应的解决方法. 为什么要重复造轮子 几乎每个人每天都要或多或少和Web服务器打交道,比较著名

转:C#写的WEB服务器

转:http://www.cnblogs.com/x369/articles/79245.html 这只是一个简单的用C#写的WEB服务器,只实现了get方式的对html文件的请求,有兴趣的朋友可以在此基础之上继续开发更多功能,小弟学c#不久,如有错漏,望请见凉!! 摘要: WWW的工作基于客户机/服务器计算模型,由Web 浏览器(客户机)和Web服务器(服务器)构成,两者之间采用超文本传送协议(HTTP)进行 通信,HTTP协议的作用原理包括四个步骤:连接,请求,应答.根据上述HTTP协议的作

Hello Node.js之搭建一个web服务器

Node.js简述 Nodejs 是JavaScript运行时,解释器是C/C++写的,基于ChromeV8引擎, 事件驱动,非阻塞I/O模型.本系列目前参考了Node.js官网,慕课网Scott的Node.js基础,<Node即学即用>图灵系列,以及可能的网上公开资源. Nodejs包管理器是npm 包就是别人写好的库. Nodejs提供了fs,http等内置对象,操作磁盘文件.搭建服务器. 特征:单线程.事件驱动.异步非阻塞I/O模型. Node.js安装 官网安装Node.js,自带np

徒手用Java来写个Web服务器和框架吧&lt;第二章:Request和Response&gt;

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 接上一篇,说到接受了请求,接下来就是解析请求构建Request对象,以及创建Response对象返回. 多有纰漏还请指出.省略了很多生产用的服务器需要处理的过程,仅供参考.可能在不断的完整中修改文章内容. 先上图 项目地址: https://github.com/csdbianhua/Telemarketer 首先看看如何解析请求 解析请求 构建Request对象 这部分对应代码在这里,可以对照查看 一个HTTP的GET请求大概如下

徒手用Java来写个Web服务器和框架吧&lt;第三章:Service的实现和注册&gt;

徒手用Java来写个Web服务器和框架吧<第一章:NIO篇> 徒手用Java来写个Web服务器和框架吧<第二章:Request和Response> 这一章先把Web框架的功能说一些,有个雏形. 先是制作一个Service,并绑定到一个正则地址.用到了注解和反射. 项目地址: Telemarketer Service的定义 Telemarketer的Service是一个服务,请求了跟它关联的地址,那就由它来为你服务. 它对外只需一个方法.并且对这个方法的要求大概只有输入一个Reque

如何给ss bash 写一个 WEB 端查看流量的页面

由于刚毕业的穷大学生,和朋友合租了一台服务器开了多个端口提供 ss 服务,懒得配置 ss-panel,就使用了 ss-bash 来监控不同端口的流量,但每次都要等上服务器才能看到流量使用情况,很麻烦,于是就写了个简单的页面来提供 WEB 访问,具体内容一起通过本文学习吧 由于刚毕业的穷大学生,和朋友合租了一台服务器开了多个端口提供 ss 服务,懒得配置 ss-panel,就使用了 ss-bash 来监控不同端口的流量,但每次都要等上服务器才能看到流量使用情况,很麻烦,于是就写了个简单的页面来提供