对libmicrohttpd添加FastCGI协议支持

  在以前的《基于libmicrohttpd的HTTP服务器初探》中,libmicrohttpd是可以返回文件系统中的静态网页的,所以挂GitHub Pages的静态博客是没有问题的,但是对于PHP这类动态网页就无能为力了。今天逛V2ex就看见一个家伙把自己简短实现的PHP Web Server挂上来求Star。乍一看用C代码处理HTTP协议,不是很感冒,但是通过FastCGI支持PHP还是挺合寡人口味的。
FastCGI

  基本来说,PHP这类脚本语言需要解释器动态执行生成结果,传统的CGI需要不断启动解释器(fork进程)、加载配置、运行脚本返回结果(通过标准输入、输出、标准错误)、关闭进程,大量的资源用于不断创建销毁进程,效率很低。于是改良版的FastCGI协议出来了,主要是通过Master-Worker进程池的方式,再加上I/O线路多路复用模式,提高了效率。
  之前配置过Nginx Web Server,其实从其配置文件中也猜测出了一些端倪。Nginx的fastcgi_pass可以通过ip:port或者unix套接字方式同php-fpm(FastCGI的一个实现)来进行数据交互,所以Nginx的ngx_http_fastcgi_module.c模块也是一个FastCGI很好的参照模板(只是苦于现在不了解Nginx的数据流,所以看起来很晕乎)!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
location ~ \.php$ {
# Test for non-existent scripts or throw a 404 error
# Without this line, nginx will blindly send any request ending in .php to php-fpm
try_files $uri =404;
fastcgi_pass unix:/run/php-fpm.socket;

# include /etc/nginx/fastcgi.conf;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;

fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param HTTPS $https if_not_empty;

fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;

fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
}

  其实FastCGI是语言无关的,知识一个协议。下文用最常见的PHP及其FastCGI实现php-fpm为例子说明。

一、向FastCGI发送请求

  总体看来,FastCGI算是很简单的协议了,就是要求Web Server以规定的格式,向php-fpm指定的ip:port或者unix socket连接后发送数据,然后读取php-fpm的返回就可以了。由于socket可以配置成多路复用的模式,所以请求的时候用一个requestId来标示,在会话结束之后requestId失效,可以被后面再次利用。这样也可以感觉到,一个ip:port或者unix socket只能被一个程序使用,否则读到的数据可能是别的程序的回答,但是本程序又处理不了,所以/run/php-fpm。socket启动后都是属于nginx的。
  接着上次的设计,当libmicrohttpd接收请求的url,发现文件名以。php结尾的时候,就连接php-fpm,然后将数据发送给php-fpm程序,接下读取php-fpm的返回结果给客户端就可以了,这也就是Nginx的fastcgi_pass的意义。
  针对HTTP的GET和POST方式,发送给php-fpm有细微的差别,所以这里分开描述。统一说一下FastCGI的数据是要求pad到8字节对其的,在操作的过程中需要注意。
FastCGI flow

1.1 GET方式

  本身没有消息体,传递的参数位于url上面。发送的过程如下:
  (1) 创建socket或者unix socket,然后连接配置文件指定的ip:port或者unix socket path;
  (2) 创建requestId,发送FCGI_RESPONDER的FCGI_BEGIN_REQUEST请求(其他两类FCGI_AUTHORIZER和FCGI_FILTER暂不考虑);
  (3) 发送不定个数的FCGI_PARAMS,并以空FCGI_PARAMS为结束标记。这里可以按照Nginx配置文件的参数名传递相应的参数值,很多参数可以从Libmicrohttpd的MHD_Connection连接中提取,对于GET最重要的是传递SCRIPT_FILENAME,其值为请求的文件在服务器文件系统上的绝对路径;然后如果请求的url有参数,将参数部分通过QUERY_STRING传递。这里需要吐槽一下,libmicrohttpd自动将GET的数据都key-value化了,而且貌似还找不回原始表示的参数,难道要我自己手动再连接起来?这里不需要传递CONTENT_LENGTH参数,其它参数能提供最好提供,全的参数名可以查找RFC 3875 CGI规范。
  (4) GET请求类型不需要发送FCGI_STDIN数据,这里可以直接读取php-fpm的返回了。

1.2 POST方式

  url即是访问的路径/文件,要发送的数据放在内部传递过来。所以多了一步将POST数据传递给php-fpm。
  (1) 创建socket或者unix socket,然后连接配置文件指定的ip:port或者unix socket path;
  (2) 创建requestId,发送FCGI_RESPONDER的FCGI_BEGIN_REQUEST请求;
  (3) 发送不定个数的FCGI_PARAMS,并以空FCGI_PARAMS为结束标记。这里重点需要传递CONTENT_LENGTH参数,为POST数据的长度;
  (4) 创建FCGI_STDIN格式的包,将POST数据发送给php-fpm。每次发送的最大长度为FCGI_MAX_LENGTH,所以如果POST的数据太大,可以连续发多个FCGI_STDIN数据包。最后发送空FCGI_STDIN表示结束;
  (5) 读取php-fpm返回;

二、读取FastCGI的结果,并返回给客户

  通过上面发送完请求后,就可以直接读socket得到php-fpm的返回了。
  Web Server一般先读取FCGI_HEADER_LEN一个头部的长度,然后解析这个头部,看返回的requestId是不是正确。然后查看返回类型:
  (1) FCGI_STDOUT:正常的php-fpm处理结果。通常第一次返回会添加一个”X-Powered-By: PHP/5。6。20-pl0-gentoo\r\nContent-type: text/html; charset=UTF-8\r\n\r\n”这样的头部信息,Web Server可以选择将这个信息添加到对浏览器的回复头上面。这个头之后的数据就是实际的处理结果,如果返回太长,可能一次读取不玩,多次读取头,然后读取负载大小,直到读取到FCGI_END_REQUEST为止。
  (2) FCGI_STDERR:一般是错误\警告类别的数据。
  (3) FCGI_END_REQUEST:本次请求结束,Web Server可以在这里把收集到的数据汇总返回给浏览器。

  运行效果如下:
  跑phpinfo(),以及POST提交表单没有问题,但是带参数的GET没有测试,我用的Libmicrohttpd库接受请求的,原因上面说过了。
phpinfo

三、后言

  官方网站fastcgi的网站已经不能访问了,以前很多资料指向的官方实现也找不到了。虽然现在的Apache、Nginx、Lighttpd都有实现fastcgi的模块,只需配置文件几行代码就能正常使用,但是好在FastCGI的协议看似不太复杂,参照别人的修改验证一下,也是挺有意义的。

本文完!

参考