基于phpx的php 扩展调试

用c写php的扩展不是一件轻松的事情,调试起来更是麻烦。普通的php扩展调试网上可以找到资料。我这里因为要用到c++,因此用了phpx来降低php扩展开发的门槛。

phpx项目地址:https://github.com/swoole/phpx

准备环境

ubuntu16.04, gcc, git, wget

首先从php的官方网站下载需要的php版本的源代码: https://php.net/releases/

我下载的是php-7.0版本的

wget https://www.php.net/distributions/php-7.0.33.tar.gz

下载完成后,解压进入目录,安装一些必要的依赖库。

sudo apt-get install libxml2-dev
./configure --enable-debug
make -j 4
sudo make install

因为我的扩展使用了phpx这个库,所有phpx的编译也需要开启debug模式,编辑cmakelist.txt加入 SET(CMAKE_BUILD_TYPE "Debug")。然后

cmake .
make -j 4
sudo make install

注意:在编译php扩展的时候也要开启调试模式。

使用gdb调试

因为php+phpx这个组合编译出来的扩展没有办法像普通的gdb调试一样,通过行号来打断点调试。因此需要根据函数名来打断点。

比如我的代码中有一个关键的方法名叫做input, 因此可以

nm /usr/local/lib/php/extensions/debug-non-zts-20151012/php_dv.so | grep input

得到以下的查找结果

000000000003a99e T _Z8Dv_inputRN3php6ObjectERNS_4ArgsERNS_7VariantE
00000000000491f8 T _ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE

_ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE这个就是可以打断点的函数名啦。

gdb php
break _ZN11DataAdaptor5inputESt6vectorIP6PersonSaIS2_EE
run paintAll.php

就可以看到断点打在了

Breakpoint 1, DataAdaptor::input (this=0x1259de0, 
    allPerson=std::vector of length 2622, capacity 2622 = {...}) at ../DataAdaptor.cc:195
195    void DataAdaptor::input(vector allPerson) {

可以使用layout分割窗口,大致结果如下:

gdb调试窗口

coredump信息的查看

php扩展出错后,也可以生成coredump文件。需要修改limits.conf中的core文件大小,比如设置成unlimited。生成core文件后,执行gdb php core, 然后使用bt即可打印出方法的调用栈,从而分析问题出现的原因。

用c写php扩展的笔记

编写php扩展的步骤:

1.使用php-src中ext文件夹中的ext_skel生成项目框架
2.编辑config.m4,将其中三句话前面的dnl删除,改成下面这样。

PHP_ARG_WITH(md2pic, for md2pic support,
Make sure that the comment is aligned:
[  --with-md2pic             Include md2pic support])

3.执行phpize
4.执行./configure
5.使用make编译
6.使用make install安装扩展
7.将扩展加入php.ini中
8.使用php -m检查扩展是否正常加载

关于config.m4

config.m4相当于一个构建系统,在php扩展的开发中,我的理解就是它可以用来配置lib,include,flags等编译时的属性以及其他的一些功能。这里给出一个配置了其他的lib和include信息的config.m4文件

dnl Id
dnl config.m4 for extension md2pic

dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

PHP_ARG_WITH(md2pic, for md2pic support,
Make sure that the comment is aligned:
[  --with-md2pic             Include md2pic support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(md2pic, whether to enable md2pic support,
dnl Make sure that the comment is aligned:
dnl [  --enable-md2pic           Enable md2pic support])

if test "PHP_MD2PIC" != "no"; then
  dnl Write more examples of tests here...

  dnl # --with-md2pic -> check with-path
  dnl SEARCH_PATH="/usr/local /usr"     # you might want to change this
  dnl SEARCH_FOR="/include/md2pic.h"  # you most likely want to change this
  dnl if test -rPHP_MD2PIC/SEARCH_FOR; then # path given as parameter
  dnl   MD2PIC_DIR=PHP_MD2PIC
  dnl else # search default path list
  dnl   AC_MSG_CHECKING([for md2pic files in default path])
  dnl   for i in SEARCH_PATH ; do
  dnl     if test -ri/SEARCH_FOR; then
  dnl       MD2PIC_DIR=i
  dnl       AC_MSG_RESULT(found in i)
  dnl     fi
  dnl   done
  dnl fi
  dnl
  dnl if test -z "MD2PIC_DIR"; then
  dnl   AC_MSG_RESULT([not found])
  dnl   AC_MSG_ERROR([Please reinstall the md2pic distribution])
  dnl fi

  dnl # --with-md2pic -> add include path

  PHP_ADD_INCLUDE(src/libMultiMarkdown/include)

  LIBNAME=gd # you may want to change this
  LIBSYMBOL=gdImageCreate # you most likely want to change this 

  PHP_CHECK_LIBRARY(LIBNAME,LIBSYMBOL,
  [
    PHP_ADD_LIBRARY_WITH_PATH(gd,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(curl,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(png,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(z,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(jpeg,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(freetype,"/usr/lib", MD2PIC_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(m,"/usr/lib", MD2PIC_SHARED_LIBADD)
    AC_DEFINE(HAVE_MD2PICLIB,1,[ ])
  ],[
    AC_MSG_ERROR([wrong md2pic lib version or lib not found])
  ],[

  ])


  dnl
  PHP_SUBST(MD2PIC_SHARED_LIBADD)
  PHP_NEW_EXTENSION(md2pic, [md2pic.c \
  src/libMultiMarkdown/aho-corasick.c \
  src/libMultiMarkdown/beamer.c \
  src/libMultiMarkdown/char.c \
  src/libMultiMarkdown/critic_markup.c \
  src/libMultiMarkdown/d_string.c \
  src/libMultiMarkdown/epub.c \
  src/libMultiMarkdown/file.c \
  src/libMultiMarkdown/html.c \
  src/libMultiMarkdown/latex.c \
  src/libMultiMarkdown/lexer.c \
  src/libMultiMarkdown/memoir.c \
  src/libMultiMarkdown/miniz.c \
  src/libMultiMarkdown/mmd.c \
  src/libMultiMarkdown/object_pool.c \
  src/libMultiMarkdown/opendocument-content.c \
  src/libMultiMarkdown/opendocument.c \
  src/libMultiMarkdown/scanners.c \
  src/libMultiMarkdown/stack.c \
  src/libMultiMarkdown/textbundle.c \
  src/libMultiMarkdown/token_pairs.c \
  src/libMultiMarkdown/token.c \
  src/libMultiMarkdown/transclude.c \
  src/libMultiMarkdown/rng.c \
  src/libMultiMarkdown/uuid.c \
  src/libMultiMarkdown/writer.c \
  src/libMultiMarkdown/zip.c \
  src/libMultiMarkdown/parser.c \
  src/libMultiMarkdown/pic.c], $ext_shared,, [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 ] )
fi

编写php扩展的资料

我这里主要参考的是 php内核剖析这本书。

php的扩展其实也可以用c++开发。这里有一个很好的项目php-x,并且开发扩展也要容易很多。

从php-fpm解析FastCGI协议

这是一篇类似于开发笔记的文章,从php-fpm与nginx的tcp请求中,去理解FastCGI协议,因此不会详细的阐述FastCGI协议到底是什么样的。

从一段抓包说起

"No.","Time","Source","Destination","Protocol","Length","Info"
"431","22.975896289","127.0.0.1","127.0.0.1","TCP","76","55928  >  9000 [SYN] Seq=0 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=0 WS=128"
"432","22.975910047","127.0.0.1","127.0.0.1","TCP","76","9000  >  55928 [SYN, ACK] Seq=0 Ack=1 Win=43690 Len=0 MSS=65495 SACK_PERM=1 TSval=1732184618 TSecr=1732184618 WS=128"
"433","22.975920352","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [ACK] Seq=1 Ack=1 Win=43776 Len=0 TSval=1732184618 TSecr=1732184618"
"434","22.975948796","127.0.0.1","127.0.0.1","TCP","1356","55928  >  9000 [PSH, ACK] Seq=1 Ack=1 Win=43776 Len=1288 TSval=1732184618 TSecr=1732184618"
"435","22.975953739","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [ACK] Seq=1 Ack=1289 Win=174720 Len=0 TSval=1732184618 TSecr=1732184618"
"452","23.068068706","127.0.0.1","127.0.0.1","TCP","660","9000  >  55928 [PSH, ACK] Seq=1 Ack=1289 Win=174720 Len=592 TSval=1732184710 TSecr=1732184618"
"453","23.068076923","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [ACK] Seq=1289 Ack=593 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710"
"454","23.068097717","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [FIN, ACK] Seq=593 Ack=1289 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710"
"455","23.068153021","127.0.0.1","127.0.0.1","TCP","68","55928  >  9000 [FIN, ACK] Seq=1289 Ack=594 Win=44928 Len=0 TSval=1732184710 TSecr=1732184710"
"456","23.068163150","127.0.0.1","127.0.0.1","TCP","68","9000  >  55928 [ACK] Seq=594 Ack=1290 Win=174720 Len=0 TSval=1732184710 TSecr=1732184710"

为了抓这段包,需要将php-fpm中的监听地址改成tcp socket。注意:tcp socket的性能远远低于unix socket。

可以看到,这里面除了tcp的握手和断开以及应答部分,有PSH标志的是FastCGI的具体协议内容。可以看到nginx给php-fpm发送了一段数据,之后php-fpm进行响应。FastCGI协议就是这样简单的使用tcp协议,使得Web Server可以转发HTTP请求到FastCGI应用程序上,具体的协议内容可以参考FastCGI规范中文翻译

php-fpm在单次请求结束后,会主动断开连接,而在FastCGI协议中,明确说明单次连接是可以复用的。

https://stackoverflow.com/questions/43280573/whether-the-connection-between-php-fpm-and-nginx-by-fast-cgi-are-persistent-kee

这个链接中有关于nginx和php-fpm连接释放的相关说明。

web server 可以将关闭权限委托给php-fpm,这样php-fpm在每次请求结束后就会关闭。

将关闭权限委托给php-fpm的好处就是不会因为连接的占用导致子进程不释放。但是不断的建立和断开连接也会影响性能。

当前php-fpm和nginx的主动断开连接是否会影响性能

会影响性能,但是并不推荐保持连接。

如果希望php-fpm不主动关闭连接,可以使用以下设置:

Syntax: fastcgi_keep_conn on | off;
Default:    
fastcgi_keep_conn off;
Context:    http, server, location
This directive appeared in version 1.1.4.

记得在upstream中使用keepalive选项

upstream backend {
    server 127.0.0.1:9000
    keepalive 20
}

但是缺点也很明显,如果用户请求一直和nginx保持连接,那么nginx也不会释放该与php-fpm的连接。这样会一直占用php-fpm的子进程不释放。当达到php-fpm的最大子进程时,就会拒绝其他的请求。

同时需要注意的是,如果nginx和php-fpm都在本地,不断的重新建立连接的影响是很小的。因此并不推荐将fastcgi_keep_conn选项打开。

这里是一些压测数据(pm.max_children 设置为20,这里只使用20个并发):

测试命令

ab -k -n 100000 -c 20 http://localhost/php/index.php

在主动断开FastCGI连接的情况下:

Server Software:        nginx/1.13.3
Server Hostname:        localhost
Server Port:            80

Document Path:          /php/index.php
Document Length:        60 bytes

Concurrency Level:      20
Time taken for tests:   28.334 seconds
Complete requests:      100000
Failed requests:        0
Keep-Alive requests:    0
Total transferred:      22900000 bytes
HTML transferred:       6000000 bytes
Requests per second:    3529.39 [#/sec] (mean)
Time per request:       5.667 [ms] (mean)
Time per request:       0.283 [ms] (mean, across all concurrent requests)
Transfer rate:          789.29 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0      14
Processing:     1    5   2.4      5     212
Waiting:        0    5   2.4      5     212
Total:          1    6   2.5      5     212

在不断开连接的情况下:

测试一直没法正常完成,部分请求会超时。

因此FastCGI是没有必要保持连接的,这会大大降低并发度。

benchmark 压测,请求直接超时退出

ab -k -c 100 -n 10000 "http://localhost/php/index.php"

php-fpm有一个子进程数量的限制,在并发过高时,没有办法为每一个请求分配一个子进程,导致请求一直在等待,直至超时退出。

unix socket和tcp socket的区别

unix socket相对于tcp socket来说,性能会提升很多。

unix socket虽然也有个socket,但是和网络一点关系都没有。unix socket是进程间的通信。

unix socket不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

实现FastCGI协议时,tcp连接中读到EOF代表了什么

在写代码过程中,tcp连接读到了EOF。从表面上来看,是读到了流的结束。但也意味着对端至少关闭了写通道。这是因为php-fpm读到了它无法理解的请求,因此直接关闭了连接。

在开发过程中,遇到了php-fpm进程不释放的问题

在BeginRequestRecord中,将flags置为1,这样与fastcgi的连接会一直保持。但是我在tcp连接中读到EOF时,却没有释放这个连接。因此这个连接会占用一个php-fpm子进程不会释放。只要手动释放这个连接即可,或者将flags设为0。

参考

FastCGI协议分析

Nginx支持PHP的PATHINFO模式配置分析

Linux下的IPC-UNIX Domain Socket

FastCGI 规范中文翻译

原文地址:https://fastcgi-archives.github.io/FastCGI_Specification.html

1.简介

FastCGI 是一种对 CGI 的开放扩展,在不改变 Web 服务的前提下,为所有的网络应用程序提供了很高的性能。

这个规范的目的很小:从应用程序角度来看,指定了一个 FastCGI 应用程序和一个支持 FaseCGI的 Web 服务之间的接口。许多 Web 服务的特性和 FastCGI相关,例如,应用程序管理工具,与Web服务器接口的应用程序无关,此处不再赘述。

这个规范适用于Unix(更确切的说,适用于支持 Berkeley Sockets 的 POSIX 系统)。规范的大部分是一个简单的通信协议,它独立于字节序,并将扩展到其他系统。

我们将通过比较 FastCGI 和常规的 CGI/1.1 的 Unix 实现来介绍它。 FastCGI 是被设计用于支持常驻内存的应用程序进程,例如,应用程序服务。常规的 CGI/1.1 的 Unix 实现的主要不同之处在于,CGI 会创建一个应用程序进程,响应一个请求之后就会退出。

FastCGI进程的初始状态比CGI / 1.1进程的初始状态更简洁,因为 FastCGI 进程在初始化时没有开始与任何事物连接。它没有常规的打开标准输入(stdin)、输出(stdout)和错误(stderr)流,并且它不会通过环境变量接受大量信息。在一个 FastCGI 进程中,关键的初始状态是监听一个 socket,这个socket会接收来自 Web 服务器的连接。

一个 FastCGI 进程在它监听的 socket 上接收一个连接时,进程会执行一个简单的协议去接收和发送数据。这个协议主要有两个目的。第一,在多个独立的 FastCGI 请求中,这个协议复用一个传输连接。这支持那些使用了事件驱动或多线程编程技术来处理并发请求的应用程序。第二,对于每一个请求,这个协议在每个传输方向上都提供了多个独立的数据流。这样,例如,stdout 和 stderr 数据都通过单个传输连接从应用程序传递到Web服务器,而不是像 CGI/1.1 那样需要单独的管道。

一个 FastCGI 应用程序扮演了明确定义的角色之一。我们最熟悉的是响应器角色,应用程序从一个 HTTP 请求中接收所有的信息,之后生成一个 HTTP 响应;这正是 CGI/1.1 程序所扮演的角色。第二个角色是认证器,应用程序从一个 HTTP 请求中接收所有的信息,之后生成一个认证通过/不通过的决定。第三个角色是过滤器,应用程序从一个 HTTP 请求中接收所有的信息,加上一个 Web 服务器中存储的额外的文件数据流,然后生成一个“过滤的”版本的数据流作为 HTTP 响应。这个框架是可扩展的,因此更多的 FastCGI 角色可以在以后定义。

在本说明书的其余部分中,术语“ FastCGI 应用程序”,“应用程序进程”或“应用程序服务器”在不会引起混淆的情况下缩写为“应用程序”

2.初始处理状态

2.1 参数列表

默认情况下,Web 服务器创建一个包含单个元素的参数列表,应用程序的名字会被当作可执行文件路径名的最后一部分。Web 服务器可能提供了一种方法来指明一个不同的应用程序名称,或者一个更详细的参数列表。

注意,由 Web 服务器执行的文件可能是一个解释性脚本(一个文本文件,以#!开头),这种情况下,应用程序参数的构建如在execve联机帮助页中所述那样。

2.2 文件描述符

Web 服务器在应用程序开始执行时打开单个文件描述符FCGI_LISTENSOCK_FILENO。这个描述符指向由 Web 服务器创建的监听的socket。

FCGI_LISTENSOCK_FILENO 等价于 STDIN_FILENO 。标准的描述符 STDOUT_FILENO 和 STDERR_FILENO 在应用程序开始执行时被关闭。判断一个应用程序是被 CGI 还是 FastCGI 调用的可靠方法是:调用 getpeername(FCGI_LISTENSOCK_FILENO),返回 -1 并将errno设置为ENOTCONN的就是 FastCGI 程序。

Web服务器选择可靠的传输,Unix流管道(AF_UNIX)或TCP/IP(AF_INET),隐含在FCGI_LISTENSOCK_FILENO套接字的内部状态中。

2.3 环境变量

Web 服务器可以使用环境变量去传递参数给应用程序。这个规范定义了一个这样的变量:FCGI_WEB_SERVER_ADDRS。我们期待随着规范的演变,会有更多的变量会被传递。Web 服务器可以提供一种方法去绑定其他的环境变量,比如 PATH 变量。

2.4 其他状态

Web 服务器可以提供一种方法去指明一个应用程序的初始处理状态的其他部分,比如优先级,用户 ID,用户组 ID,根目录,以及进程的工作目录。

3.协议基础

3.1 符号

我们使用 C 语言符号去定义协议信息的格式。所有的结构体元素都使用 unsigned char 类型定义,并安排使ISO C编译器以常规方式将它们排列,没有填充。在结构体中,第一个字节会被第一个传输,第二个会被第二个传输,以此类推。

我们使用两个公约来概括我们的定义。

第一,当两个相邻的结构体组件名称相同时,除了后缀”B1″和”B0″,这意味着这两个组件可以被视为单个数字,计算为B1<<8 + B0。

第二,我们扩展 C 的结构体,允许以下的形式

struct {
    unsigned char mumbleLengthB1;
    unsigned char mumbleLengthB0;
    ... /* other stuff */
    unsigned char mumbleData[mumbleLength];
};

这代表着一个变长的结构体,它的长度是由前面的组件的值决定的。

3.2 接受传输连接

一个 FastCGI 应用程序在由文件描述符 FCGI_LISTENSOCK_FILENO 引用的 socket 上调用 accept() 去接收一个新的传输连接。如果 accept() 成功了,FCGI_WEB_SERVER_ADDRS 环境变量被绑定,应用程序立即执行以下的特殊操作:

  • FCGI_WEB_SERVER_ADDRS: 这个值是 Web 服务器的有效的 ip 地址列表。
  • 如果 FCGI_WEB_SERVER_ADDRS 绑定了,应用程序检查新连接的对等 IP 地址是否在列表中。如果检查失败了(包括连接没有使用 TCP/IP 这种可能性),应用程序关掉连接来响应。
    • FCGI_WEB_SERVER_ADDRS 是由英文逗号分割的 IP 地址列表。每一个 IP 地址是由点号分割的4个0~255内的数字组成。例如:FCGI_WEB_SERVER_ADDRS=199.170.183.28,199.170.183.71 。

应用程序可以接收多个并发传输连接,但是它不一定需要这样做。

3.3 记录

应用程序使用一个简单的协议从 Web 服务器获取请求并执行。协议的具体内容视应用程序的角色而定,但是一般来说,Web 服务器首先发送参数和其他数据到应用程序,之后应用程序发送结果数据给 Web 服务器,最终应用程序告诉 Web 服务器请求处理已经结束。

所有通过传输连接的数据都是在 FastCGI 记录(records)里的。FastCGI 记录完成两件事。第一,记录在多个独立的请求之间复用传输连接。这种复用支持使用事件驱动模型或多线程技术来处理并发请求的应用程序。第二,在同一个请求中,记录提供了在不同方向上多个独立的数据流。这样,stdout 和 stderr 可以使用同一个传输连接来传输,而不是需要不同的连接。

        typedef struct {
            unsigned char version;
            unsigned char type;
            unsigned char requestIdB1;
            unsigned char requestIdB0;
            unsigned char contentLengthB1;
            unsigned char contentLengthB0;
            unsigned char paddingLength;
            unsigned char reserved;
            unsigned char contentData[contentLength];
            unsigned char paddingData[paddingLength];
        } FCGI_Record;

一个 FastCGI 记录包含一个定长的前缀,以及变长的内容和填充字节。一条记录包含7个部分:

  • 版本号(version):指定 FastCGI 协议的版本号。这个规范文档的版本号是 FCGI_VERSION_1。
  • 类型(type):指定这条记录的类型。例如,记录的功能函数。具体的记录类型和功能函数在之后的章节有详细介绍。
  • 请求ID(requestId):指定这条记录属于哪个 FastCGI 请求。
  • 内容长度(contentLength):在contentData部分存储的字节数。
  • 填充长度(paddingLength):在paddingData部分存储的字节数。
  • 内容数据(contentData):在0到65535字节之间的数据,根据记录类型进行解释。
  • 填充数据(paddingData):0到255个字节的数据,被忽略。

我们使用宽松的C struct初始化语法来指定常量FastCGI记录。我们省略了版本号部分,忽略填充部分,并将requestId视为一个数字。因此 {FCGI_END_REQUEST, 1, {FCGI_REQUEST_COMPLETE,0} 是一个 type == FCGI_END_REQUEST, requestId == 1, and contentData == {FCGI_REQUEST_COMPLETE,0} 的记录。

Padding

协议允许发送者填充发送的记录,然后要求接收者解释 paddingLength,跳过 paddingData。Padding 允许发送者保持数据对齐,达到更高效的数据处理。使用X窗口系统协议的经验显示了这种对齐的性能优势。

我们推荐记录的长度是8字节的整数倍。一个 FastCGI 的固定长度部分正好是8个字节。

处理请求ID

Web服务器重用 FastCGI 的请求ID;在一个给定的传输连接上,应用程序追踪每个请求 ID 的当前状态。当应用程序收到一条记录{FCGI_BEGIN_REQUEST, R, …},一个请求 ID R 置为活跃状态。当应用程序发送一条记录 {FCGI_END_REQUEST, R, …} 给 Web 服务器时,请求 ID R置为非活跃状态。

当请求 ID R 是非活跃的,应用程序会忽略所有的 requestId R 的记录,除了如上所述的 FCGI_BEGIN_REQUEST 记录。

Web 服务器试图保持 FastCGI 请求 ID 是一个很小的数字。这样应用程序就可以使用一个很短的数组来追踪请求 ID 的状态,而不是一个长的数组或是一个哈希表。应用程序可以选择在同一时间仅仅接收一条请求。这样应用程序可以简单的根据当前连接请求 ID 来检查 requestId。

记录类型

有两种阐述 FastCGI 记录类型的方法。

第一个区别是管理记录和应用程序记录。管理记录包含非特定于任何 Web 服务器请求的信息,例如有关应用程序的协议功能的信息。应用程序记录包含有关requestId组件标识的特定请求的信息。

第二个区别是离散记录和流记录。离散记录本身包含有意义的数据单元。流记录是流的一部分,例如,一系列的0或更多的非空记录(length != 0),之后紧跟着一个空记录(length 0)。流记录的 contentData 部分是一连串的字节组成。这个字节序列就是流的值。因此流的值是独立于它包含多少条记录,以及它的字节在非空记录中如何划分。

这两点解释是不相关的。在当前版本的 FastCGI 协议定义的记录类型中,所有的管理记录类型都是离散的记录类型,几乎所有的应用程序记录类型都是流记录类型。但是有三个应用程序记录类型是离散的,也不能保证在之后的版本中,一个管理记录类型是流式的。

3.4 键值对

在这些角色中,FastCGI 应用程序需要读写边长值的不同数字。因此采用一个标准格式去编码一个键值对是有用的。

FastCGI 发送的键值对格式:键长,值长,键,值。小于等于127字节可以用一个字节编码,大于127字节的用4个字节编码:

typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair11;

typedef struct {
    unsigned char nameLengthB0;  /* nameLengthB0  >> 7 == 0 */
    unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength];
    unsigned char valueData[valueLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair14;

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
    unsigned char nameData[nameLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3  >> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB0;
    unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
    unsigned char valueLengthB2;
    unsigned char valueLengthB1;
    unsigned char valueLengthB0;
    unsigned char nameData[nameLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength
                    ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0];
} FCGI_NameValuePair44;

第一个字节的高位表示长度的编码。高位是0表示一个字节编码,高位是1表示4个字节编码。

这样的键值对格式允许发送方发送二进制数据,使得接受者可以立即分配正确大小的存储空间,即使是很大的值。

3.5 关闭传输连接

Web 服务器控制传输连接的生命周期。Web 服务器可以在没有活跃请求时关闭连接。或者 Web 服务器可以将关闭权限委托给应用程序(请参阅FCGI_BEGIN_REQUEST)。在这种情况下,应用程序在指定的请求之后关闭连接。

这种灵活设计可以包容不同的应用程序风格。简单的应用程序一次只处理一个请求,每个请求都会建立一个连接。更复杂的应用将会处理并发请求,一个和多个传输连接,会长时间保持传输连接。

通过在完成写入响应时关闭传输连接,简单的应用程序可以显着提升性能。Web服务器需要控制长期连接的连接生存期。

当应用程序关闭连接或发现连接已关闭时,应用程序将启动新连接。

4.管理记录类型

4.1 FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT

Web 服务器可以查询应用程序中的特定变量。服务器通常会在应用程序启动时执行查询,以便自动化系统配置的某些方面。

应用程序接受一个查询,比如{FCGI_GET_VALUES, 0, …}。FCGI_GET_VALUES 记录的 contentData 部分包含一系列的具有空值的键值对。

应用程序通过发送一个带有值的记录{FCGI_GET_VALUES_RESULT, 0, …}来响应。如果应用程序不理解在查询中的某个变量名,它会从响应中忽略该名称。FCGI_GET_VALUES 被设计成允许一个开放结束集合的变量。初始集合变量提供信息去帮助服务器操作应用,以及连接管理:

  • FCGI_MAX_CONNS:应用程序接收的并发传输连接的最大值。比如,1或10。
  • FCGI_MAX_REQS:应用程序接收的并发请求的最大值。比如1或50。
  • FCGI_MPXS_CONNS:如果应用程序不复用连接,这个值是0(例如,一个请求一个连接)。否则是1。

4.2 FCGI_UNKNOWN_TYPE

管理记录类型集可能会在此协议的未来版本中增长。为了提供这种演变,该协议包括 FCGI_UNKNOWN_TYPE 管理记录。当应用程序收到其类型T不理解的管理记录时,应用程序将使用{FCGI_UNKNOWN_TYPE,0,{T}}进行响应。

FCGI_UNKNOWN_TYPE记录的contentData部分具有以下形式:

typedef struct {
    unsigned char type;    
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

类型组件是无法识别的管理记录的类型。

5.应用的记录类型

5.1 FCGI_BEGIN_REQUEST, FCGI_GET_VALUES_RESULT

Web服务发送一个 FCGI_BEGIN_REQUEST 记录来开始一个请求。

一个 FCGI_BEGIN_REQUEST 记录的 contentData 部分有以下形式:

typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

角色组件设置Web服务器期望应用程序扮演的角色。当前定义的角色是:

  • FCGI_RESPONDER
  • FCGI_AUTHORIZER
  • FCGI_FILTER

角色定义具体在第六章描述。

flags部分包含一个控制连接关闭的位:

  • flags & FCGI_KEEP_CONN: 如果是0,应用程序在响应请求后关闭连接。如果不是0,应用程序在响应请求后不关闭连接;Web 服务器保持对连接的管理权限。

5.2 键值对流:FCGI_PARAMS, FCGI_RESULTS

FCGI_PARAMS

是一种流记录类型,用于从Web服务器向应用程序发送键值对。名称 – 值对一个接一个地沿着流向下发送,没有指定的顺序。

5.3 字节流:FCGI_STDIN, FCGI_DATA, FCGI_STDOUT, FCGI_STDERR

FCGI_STDIN

是一种流记录类型,用于从Web服务器向应用程序发送任意数据。 FCGI_DATA是第二个流记录类型,用于向应用程序发送其他数据。

FCGI_STDOUT和FCGI_STDERR是流记录类型,用于分别从应用程序向Web服务器发送任意数据和错误数据。

5.4 FCGI_ABORT_REQUEST

Web服务器发送 FCGI_ABORT_REQUEST 记录以中止请求。收到{FCGI_ABORT_REQUEST,R}后,应用程序会尽快响应{FCGI_END_REQUEST,R,{FCGI_REQUEST_COMPLETE,appStatus}}。这确实是来自应用程序的响应,而不是来自FastCGI库的低级别确认。

当HTTP客户端关闭其传输连接而来自客户端FastCGI请求正运行到一半时,Web服务器将中止FastCGI请求。这种情况似乎不太可能,大多数FastCGI请求的响应时间都很短,如果客户端速度很慢,Web服务器会提供输出缓冲。但FastCGI应用程序可能与其他系统通信有延迟或正执行服务器推送。

当Web服务器未通过传输连接复用请求时,Web服务器可以通过关闭请求的传输连接来中止请求。但是对于多路复用的请求,关闭传输连接会导致中止连接上的所有请求,这是一种令人遗憾的结果。

5.5 FCGI_END_REQUEST

应用程序发送FCGI_END_REQUEST记录以终止请求,既可能因为应用程序已处理请求,也可能应用程序已拒绝该请求。

FCGI_END_REQUEST记录的contentData部分具有以下形式:

typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;

appStatus组件是应用程序级状态代码。每个角色都在文档上记录了它对appStatus的使用。

protocolStatus组件是协议级状态代码;可能的protocolStatus值是:

  • FCGI_REQUEST_COMPLETE:正常的请求结束。
  • FCGI_CANT_MPX_CONN:拒绝新请求。当Web服务器通过一个连接将并发请求发送到旨在每个连接一次处理一个请求的应用程序时,就会发生这种情况。
  • FCGI_OVERLOADED:拒绝新请求。当应用程序耗尽某些资源时会发生这种情况,例如:数据库连接。
  • FCGI_UNKNOWN_ROLE:拒绝新请求。当Web服务器指定了应用程序未知的角色时,会发生这种情况。

6.角色

6.1 角色协议

角色协议仅包括具有应用程序记录类型的记录。它们都使用流传输几乎所有的数据。

为了使协议可靠并简化应用程序编程,角色协议被设计使用几乎连续的编组(nearly sequential marshalling.)。具有严格连续编组(strictly sequential marshalling)的协议中,应用程序接收其第一个输入,然后是第二个输入,等等。直接所有数据接受完成。类似地,应用程序发送它的第一个输出,然后发送它的第二个输出,直到它发送它们全部。输入不相互交错,输出不相互交错。

连续编组规则对某些FastCGI角色限制太多。因为 CGI 程序没有时间上的限制,可以同时使用 stdout和stderr。因此角色协议使用FCGI_STDOUT 和FCGI_STDERR来允许这两个流交错。

所有角色协议都使用FCGI_STDERR流,就像在传统应用程序编程中使用stderr一样:以可理解的方式报告应用程序级错误。使用FCGI_STDERR流始终是可选的。如果应用程序没有要报告的错误,它将不发送FCGI_STDERR记录或一个零长度FCGI_STDERR记录。

当角色协议要求传输FCGI_STDERR以外的流时,即使流是空的,也总是传输至少一个流类型的记录

再次为了可靠的协议和简化的应用程序编程,角色协议被设计成几乎连续的编组(nearly sequential marshalling.)。在真正的请求-响应协议中,应用程序在发送其第一个输出记录之前接收其所有输入记录。请求-响应协议不允许流水线操作。

请求-响应规则对某些FastCGI角色限制太多;毕竟,在开始写stdout之前,CGI程序不限制读取所有stdin。因此一些角色协议允许这种特定的可能性。首先,应用程序接收除最终流输入之外的所有输入。当应用程序开始接收最终流输入时,它可以开始写入其输出。

当角色协议使用FCGI_PARAMS传输文本值时,例如CGI程序从环境变量中获取的值,值的长度不包括终止空字节,且值本身不包含空字节。需要提供environ(7)格式键值对的应用程序必须在键和值之间插入等号,并在值后附加空字节。

角色协议不支持CGI的非解析头功能。FastCGI应用程序使用 Status 和Location CGI头设置响应状态。

6.2 响应器

一个响应器角色的FastCGI应用程序与CGI / 1.1程序具有相同的目的:它接收与HTTP请求关联的所有信息并生成HTTP响应。

下面将解释响应器如何模拟CGI/1.1的每个元素:

  • 响应器应用程序通过FCGI_PARAMS从Web服务器接收CGI/1.1环境变量。
  • 接下来,响应器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前,应用程序从该流接收最多CONTENT_LENGTH个字节。 (仅当HTTP客户端无法提供它们时,应用程序才会收到少于CONTENT_LENGTH个字节,例如因为客户端崩溃了。)
  • 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器,通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些,而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前,完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前,结束读取FCGI_STDIN。
  • 发送所有stdout和stderr数据后,响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE,将appStatus组件设置状态代码后,CGI程序通过exit系统调用返回。

响应者执行更新,例如实现POST方法时,应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH进行比较,如果两个数字不相等则中止更新。

6.3 授权器

授权器FastCGI应用程序接收与HTTP请求相关的所有信息,并生成授权/未授权的决策。在授权决策的情况下,授权者还可以将键值对与HTTP请求相关联;在做出未经授权的决定时,授权器会向HTTP客户端发送完整的响应。

由于CGI / 1.1定义了一种表示与HTTP请求相关的信息的完美方法,因此授权器使用相同的表示:

  • 授权器应用程序通过FCGI_PARAMS流从Web服务器接收HTTP请求信息,与响应器的格式相同。Web服务器不发送CONTENT_LENGTH,PATH_INFO,PATH_TRANSLATED和SCRIPT_NAME头。
  • 授权器应用程序以与Responder相同的方式发送stdout和stderr数据。CGI/1.1响应状态指明了请求的 处置方式。如果应用程序发送状态200(OK),则Web服务器允许访问。根据其配置,Web服务器可以继续进行其他访问检查,包括对其他授权器的请求。

授权器应用程序的200响应可能包括名称以Variable-为前缀的标头。这些头将应用程序中的键值对传递给Web服务器。例如,响应头:

Variable-AUTH_METHOD: database lookup

使用名称AUTH-METHOD传输值“database lookup”。服务器将这些键值对与HTTP请求相关联,并将它们包含在处理HTTP请求时执行的后续CGI或FastCGI请求中。当应用程序提供200响应时,服务器会忽略名称不带Variable-前缀的响应头,并忽略任何响应内容。

对于除“200”(OK)以外的授权器响应状态值,Web服务器拒绝访问并将响应状态,标头和内容发送回HTTP客户端。

6.4 过滤器

过滤器FastCGI应用程序接收与HTTP请求相关的所有信息,以及来自存储在Web服务器上的文件的额外数据流,并生成数据流的“过滤”版本作为HTTP响应。

过滤器的功能类似于将数据文件作为参数的响应器程序。区别在于使用过滤器,数据文件和过滤器本身都可以使用Web服务器的访问控制机制进行访问控制,将数据文件名称作为参数的响应程序必须对数据文件执行自己的访问控制检查。

过滤器采取的步骤类似于响应者的步骤。服务器首先向Filter提供环境变量,然后是标准输入(通常是POST数据),最后是数据文件输入:

- 与响应器一样,过滤器应用程序通过FCGI_PARAMS从Web服务器接收键值对。过滤器应用程序接收两个专属的变量:FCGI_DATA_LAST_MOD和FCGI_DATA_LENGTH。
- 接下来,过滤器应用程序通过FCGI_STDIN从Web服务器接收CGI/1.1 stdin数据。在接收流结束指示之前,应用程序从该流接收最多CONTENT_LENGTH个字节。(仅当HTTP客户端无法提供它们时,应用程序才会收到少于CONTENT_LENGTH个字节,例如因为客户端崩溃了。)

– 接下来,过滤器应用程序通过FCGI_DATA从Web服务器接收文件数据。该文件的最后修改时间(表示为1970年1月1日UTC以来的整数秒)为FCGI_DATA_LAST_MOD;应用程序可以查阅此变量并从缓存中进行响应而无需读取文件数据。在接收流结束指示之前,应用程序从该流中读取最多FCGI_DATA_LENGTH个字节。
– 响应器应用程序通过FCGI_STDOUT将CGI/1.1 stdout数据发送到Web服务器,通过FCGI_STDERR将CGI/1.1 stderr数据发送到Web服务器。应用程序同时发送这些,而不是一个接一个地发送。应用程序必须在开始写入FCGI_STDOUT和FCGI_STDERR之前,完成读取FCGI_PARAMS。但它无需在开始写入这两个流之前,结束读取FCGI_DATA。
– 发送所有stdout和stderr数据后,响应器应用程序发送FCGI_END_REQUEST记录。应用程序将protocolStatus部分设置为FCGI_REQUEST_COMPLETE,将appStatus组件设置状态代码后,CGI程序通过exit系统调用返回。

过滤器应将FCGI_STDIN上接收的字节数与CONTENT_LENGTH和FCGI_DATA上的FCGI_DATA_LENGTH进行比较。如果数字不匹配且过滤器是一次查询,过滤器响应应提供数据丢失的指示。如果数字不匹配且过滤器是一次更新,则过滤器应中止更新。

7.错误

FastCGI应用程序以零状态退出,表示它是故意终止的,例如为了执行原始形式的垃圾收集。FastCGI应用程序以非零状态退出,表示它崩溃了。Web服务器或其他应用程序管理器如何响应以零或非零状态退出的应用程序超出了本规范的范围。

Web服务器可以通过发送SIGTERM来请求FastCGI应用程序退出。如果应用程序忽略SIGTERM,则Web服务器可以使用SIGKILL。

astCGI应用程序使用FCGI_STDERR流和FCGI_END_REQUEST记录的appStatus部分报告应用程序级错误。在许多情况下,将通过FCGI_STDOUT流直接向用户报告错误。

Unix上,应用程序向syslog报告较低级别的错误,包括FastCGI协议错误和FastCGI环境变量中的语法错误。根据错误的严重程度,应用程序可以继续或以非零状态退出。

8.类型和常量

/*
 * Listening socket file number
 */
#define FCGI_LISTENSOCK_FILENO 0

typedef struct {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} FCGI_Header;

/*
 * Number of bytes in a FCGI_Header.  Future versions of the protocol
 * will not reduce this number.
 */
#define FCGI_HEADER_LEN  8

/*
 * Value for version component of FCGI_Header
 */
#define FCGI_VERSION_1           1

/*
 * Values for type component of FCGI_Header
 */
#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)

/*
 * Value for requestId component of FCGI_Header
 */
#define FCGI_NULL_REQUEST_ID     0

typedef struct {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} FCGI_BeginRequestBody;

typedef struct {
    FCGI_Header header;
    FCGI_BeginRequestBody body;
} FCGI_BeginRequestRecord;

/*
 * Mask for flags component of FCGI_BeginRequestBody
 */
#define FCGI_KEEP_CONN  1

/*
 * Values for role component of FCGI_BeginRequestBody
 */
#define FCGI_RESPONDER  1
#define FCGI_AUTHORIZER 2
#define FCGI_FILTER     3

typedef struct {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
} FCGI_EndRequestBody;

typedef struct {
    FCGI_Header header;
    FCGI_EndRequestBody body;
} FCGI_EndRequestRecord;

/*
 * Values for protocolStatus component of FCGI_EndRequestBody
 */
#define FCGI_REQUEST_COMPLETE 0
#define FCGI_CANT_MPX_CONN    1
#define FCGI_OVERLOADED       2
#define FCGI_UNKNOWN_ROLE     3

/*
 * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records
 */
#define FCGI_MAX_CONNS  "FCGI_MAX_CONNS"
#define FCGI_MAX_REQS   "FCGI_MAX_REQS"
#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS"

typedef struct {
    unsigned char type;    
    unsigned char reserved[7];
} FCGI_UnknownTypeBody;

typedef struct {
    FCGI_Header header;
    FCGI_UnknownTypeBody body;
} FCGI_UnknownTypeRecord;

9.参考文献

The WWW Common Gateway Interface at W3C

A.表:记录类型的属性

下表列出了所有记录类型,并指出了每种记录的属性:

  • WS->App: 此类记录只能由Web服务器发送到应用程序。其他类型的记录只能由应用程序发送到Web服务器。
  • management: 此类型的记录包含不是特定于Web服务器请求的信息,并使用的的请求ID。其他类型的记录包含特定于请求的信息,不能使用空的请求ID。
  • stream: 此类型的记录形成一个流,由具有空contentData的记录终止。其他类型的记录是离散的;每个都带有一个有意义的数据单元。
                               WS->App   management  stream

        FCGI_GET_VALUES           x          x
        FCGI_GET_VALUES_RESULT               x
        FCGI_UNKNOWN_TYPE                    x

        FCGI_BEGIN_REQUEST        x
        FCGI_ABORT_REQUEST        x
        FCGI_END_REQUEST
        FCGI_PARAMS               x                    x
        FCGI_STDIN                x                    x
        FCGI_DATA                 x                    x
        FCGI_STDOUT                                    x 
        FCGI_STDERR                                    x     

B. 典型的协议消息流

示例的其他符号约定:

  • 流记录的contentData(FCGI_PARAMS,FCGI_STDIN,FCGI_STDOUT和FCGI_STDERR)表示为字符串。以“…”结尾的字符串太长而无法显示,因此仅显示前缀。
  • 发送到Web服务器的消息相对于从Web服务器接收的消息缩进。
  • 消息按应用程序所经历的时间顺序显示。
  1. 一个没有stdin数据的简单请求,以及一个成功的响应:

(注:\013\016这里是8进制编码。这里所有的记录都省略了Version和PaddingData,因此类似于FCGI_BEGIN_REQUEST代表Type,1代表RequestId)

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

2.与示例1类似,但这次使用stdin上的数据。 Web服务器选择使用比以前更多的FCGI_PARAMS记录发送参数:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SER"}
{FCGI_PARAMS,          1, "VER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, "quantity=100&item=3047936"}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

3.与示例1类似,但这次应用程序检测到错误。应用程序将消息记录到stderr,将页面返回给客户端,并将非零退出状态返回给Web服务器。应用程序选择使用更多FCGI_STDOUT记录发送页面:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n<ht"}
    {FCGI_STDERR,      1, "config error: missing SI_UID\n"}
    {FCGI_STDOUT,      1, "ml>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_STDERR,      1, ""}
    {FCGI_END_REQUEST, 1, {938, FCGI_REQUEST_COMPLETE}}

4.示例1的两个实例,复用到单个连接上。第一个请求比第二个请求更难,因此应用程序不按顺序完成请求:

{FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          1, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_PARAMS,          1, ""}
{FCGI_BEGIN_REQUEST,   2, {FCGI_RESPONDER, FCGI_KEEP_CONN}}
{FCGI_PARAMS,          2, "\013\002SERVER_PORT80\013\016SERVER_ADDR199.170.183.42 ... "}
{FCGI_STDIN,           1, ""}

    {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n"}

{FCGI_PARAMS,          2, ""}
{FCGI_STDIN,           2, ""}

    {FCGI_STDOUT,      2, "Content-type: text/html\r\n\r\n<html>\n<head> ... "}
    {FCGI_STDOUT,      2, ""}
    {FCGI_END_REQUEST, 2, {0, FCGI_REQUEST_COMPLETE}}
    {FCGI_STDOUT,      1, "<html>\n<head> ... "}
    {FCGI_STDOUT,      1, ""}
    {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

redis使用总结

1.redis的使用场景

项目中存在多种redis的使用场景

1.1 场景一:缓存(key,value)

缓存是一个非常常见的场景,在项目中,可以将mysql中的一部分数据查询的结果缓存到redis中,以此来获取更快的查询速度。

以用户信息缓存为例,我的做法如下:

  • 1.设定用户信息缓存的规则,比如userinfo128代表用户id为128的用户信息缓存。
  • 2.在query语句执行时,先检查userinfo128是否存在,如果存在,直接取出结果返回,否则执行sql语句进行查询,并将查询结果缓存。
  • 3.在update和delete语句执行时,删除userinfo128的缓存,这样下一次query时就会自动更新缓存了。

1.2 timeline(sorted set)

timeline非常经典的场景就是微博,朋友圈这种,每个用户都能在自己的timeline上获取到按时间排序的其他用户发布的动态。这里有以前总结过的一篇文章:朋友圈式的TIMELINE设计方案

其实这个 timeline 我一开始的实现是用 list 去做的,但是用list会存在一个问题:因为推送动态可能同时发生,导致不是严格的按照时间排序。

1.3 推送用户集合(set)

这个功能其实就相当于维持一个用户的粉丝列表。这个列表是一个会经常发生变化的集合,并且在项目中是根据用户的关系链计算出来的,单次的查询会消耗很多的时间,因此做成一个集合,在需要推送动态之类的内容时,直接从redis的集合中查询,会节约很多的时间。

1.4 任务队列(list)

整个项目中很多的操作都是异步的,比如发短信,发邮件,推送用户动态等等,使用redis作为任务队列是很简单的,使用它的list结构,然后使用lpush/rpop对,一边push进任务,另一边有一个单独的后台进程pop出任务进行执行。

当然lpush/rpop并不是很好的一个选择,更好的选择是lpush/brpop,使用阻塞版本的pop指令,可以减少很多不必要的轮询。

在使用这样的任务队列时,还需要考虑到一个问题,如果在取出一个任务时进程崩溃,那么这个任务就彻底的丢失了。因此还可以使用 rpoplpush 或者阻塞版本的 brpoplpush ,取出一个任务的同时备份到另一个队列。如果执行成功的话就再lrem掉这个备份即可。关于队列的更详细的使用在第7大点有更详细的说明。

当然, Redis其实并不推荐作为任务队列的实现,如果需要的话,可以尝试使用Redis作者的另一个项目:disque,或者是kafka。

 

1.5 计数器(hash)

计数器我认为也算是 redis 一个常用的功能了,我认为原因有以下:

  • 1.很多场景下的计数功能都是一个非常高频的操作,使用 redis 会拥有极高的性能。
  • 2.redis支持原子性的自增(incre)操作,不用担心CAS(check and set)的问题。
  • 3.传统数据库,如mysql,如果是MyISAM,单次的更新会带来表锁,如果是InnoDB,则带来行锁,影响并发度。

计数器的使用很简单,直接对某个key做 incr 操作,或者对某个 hash 的 key 做 hincrby 操作即可。

 

2.php在使用redis时,多个数据库切换的困扰

项目中使用的是phpredis这个扩展,在使用pconnect保持redis长连接时,所有对redis的操作会共用同一个redis连接。这就导致:多个进程同时使用一个redis连接,并且多个进程使用的数据库不同时导致错误。比如下方的操作:

// 进程1做以下操作
redis->select(0);redis->set("key1", "val1");

// 进程2做以下操作
redis->select(1);redis-set("key2", "val2");

但是在redis的server端,所做的操作可能如下:

select(0)
select(1)
set("key2", "val2")
set("key1", "val1")

这样就导致key1的存储错误

所以我必须在所有这样的操作中,使用MULTI/EXEC对去解决这个问题。以上代码变成:

// 进程1做以下操作
redis->multi();redis->select(0);
redis->set("key1", "val1");result->exec();

// 进程2做以下操作
redis->multi();redis->select(1);
redis-set("key2", "val2");result->exec();

事实上,使用 redis 时,同时使用多个数据库并不推荐。因为在 redis 集群中是不支持 select 命令的。

3.redis多个数据库之间的切换,对性能有影响吗?

在探讨这个问题之前,摘录官网上对 select 命令的说明:

Since the currently selected database is a property of the connection, clients should track the currently selected database and re-select it on reconnection. While there is no command in order to query the selected database in the current connection, the CLIENT LIST output shows, for each client, the currently selected database.

大致意思可以翻译为:

因为当前选中的数据库是连接的一个属性,每个客户端连接都跟踪记录了当前选中的数据库,在重新连接时会重新选择数据库。虽然没有命令是为了查询当前连接选中的数据库,但是 CLIENT LIST 的输出会显示,每个客户端当前选中的是哪个数据库。

因此 select 操作只是修改了当前连接的属性。

4.redis 有 16 个数据库,目的是什么,最佳的使用方式是什么?为什么 redis 集群不支持 select?

同样的摘录一段官网的介绍

Redis different selectable databases are a form of namespacing: all the databases are anyway persisted together in the same RDB / AOF file. However different databases can have keys having the same name, and there are commands available like FLUSHDB, SWAPDB or RANDOMKEY that work on specific databases.

  In practical terms, Redis databases should mainly used in order to, if needed, separate different keys belonging to the same application, and not in order to use a single Redis instance for multiple unrelated applications.

When using Redis Cluster, the SELECT command cannot be used, since Redis Cluster only supports database zero. In the case of Redis Cluster, having multiple databases would be useless, and a worthless source of complexity, because anyway commands operating atomically on a single database would not be possible with the Redis Cluster design and goals.

大意如下:

Redis 多个不同的可选择的数据库是命名空间的一个表现形式:所有的数据库都会在同一个 RDB/AOF 文件中进行持久化。当然不同的数据库可以拥有同样的名字的键,同样的也有一些类似 FLUSHDB, SWAPDB 或 RANDOMKEY 这样的命名专门在数据库上工作的。

实际上,Redis 数据库应该主要用来分离属于一个应用的不同的键,而不是为了使用一个单独的 Redis 实例服务于多个不相关的应用。

当使用 Redis 集群时,SELECT 命名就不能使用了,因为 Redis 集群仅仅支持数据库0。在 Redis 集群的案例中,拥有多个数据库是无用的,是一种毫无价值的复杂性的来源,因为 Redis 集群的设计和目标是不可能支持 SELECT 命令的。

这里解释了为什么 Redis 被设计为有多个数据库,是为了分离同一个应用中不同的键而设计的,但是不能在多个不相关的应用中使用同一个 Redis 实例。并且还要注意在 Redis 集群中无法使用 SELECT ,因此在项目中还是不用为好。

5.redis是单进程的,如何理解?

我在第一次看到这句话时,是很不理解的。对于这种应用,不可能只有一个进程在工作啊。但是在深入了解之后,明白了这里的单进程指的是:处理 Redis 命令是单进程的。

也就是说,同一时间,在并发和并行的层面上来说,都只有一个 Redis 命令被执行。这样设计的理由我的理解有以下:

  • Redis是内存数据库,所有的操作耗时都视CPU的运行速度而定,IO不可能是瓶颈,并行/并发处理带来的意义不大。
  • 并行/并发会增加应用的复杂度

6.redis中使用队列的问题

在1.4小节中我提到了使用Redis作为任务队列的场景。在使用时遇到了程序运行一段时间之后,无法使用brpop获取数据的问题,并且这个程序的连接依然是存活的。

于是我用 CLIENT LIST 查看当前的连接客户端。发现服务器中有大量连接,但是很多连接的 idle 特别长,明显是很久以前的连接,这些连接我可以肯定是已经断开的。经过检查之后,发现 Redis.conf 中的 tcp-keepalive 项我设置为0了,设置为0就不会检查连接是否存活,从而导致连接一直存在。以前将 tcp-keepalive 设置为60,

那这跟 brpop 无法从 Redis 中获取数据有什么关系呢?以下是个人的猜想时间。

这要从Redis的block模型说起。Redis的网络连接是epoll模型的,所以是一个异步的io,肯定不会block一个连接。那么Redis server为了实现这样的block操作,会维持一个内部的哈希表,这个哈希表保存了哪个key上阻塞了哪些客户端。如下图所示:
此处输入图片的描述

如果此时list key1中被push进了一个值,key1就被置为ready状态,然后从链表头部取出client2,将值传给它。可能在我自己的测试中,有大量的已经断开连接客户端阻塞在key1上,但是因为tcp-keepalive为0,没有被及时清除。导致以上的结果。(目前的水平只能这么解释了,虽然还有很多地方说不通)

redis中的事务与锁

这篇文章是我在查找如何对redis中的值做原子操作时的一系列笔记,虽然最初的目的只是研究有哪些方式可以实现事务(transaction)操作,但是后来的延伸很多,所以我认为有必要做一些笔记防止忘记。

1.redis中的事务

redis中的事务其实并不满足数据库事务的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离型(Isolation)、持久性(Durability),简称ACID。redis只能在一致性和隔离性上提供保证,而原子性和持久性是无法保证的。因为redis的事务操作如果中断,无法回滚,满足不了原子性,并且redis的持久化策略有很多中,比如在纯内存模式下是无法持久化数据库更改的,满足不了持久性。

所以下面针对与redis事务的讨论都是有局限性的。仅仅是指对redis数据进行操作时,不会受到其他客户端的干扰。举例来说:客户端A读取keyA,修改keyA中间,不会受到客户端B的影响。

    //客户端A执行以下代码
    //获取user1的性别,如果性别是男,头像设置为男性角色
    //否则头像设置为女性角色
    gender =redis->get('user1_gender');
    if(gender === 'male'){redis->set('user1_photo','male.png');
    }else{
        $redis->set('user1_photo','female.png');
    }
    //客户端B执行以下代码
    //更改user1的性别,如果是男则改为女,如果是女则改为男
    //并根据性别设置头像
    gender =redis->get('user1_gender');
    if(gender === 'male'){redis->set('user1_gender','female');
        redis->set('user1_photo','female.png');
    }else{redis->set('user1_gender','male');
        $redis->set('user1_photo','male.png');
    }

如果客户端A、B同时只有一个执行,那么性别和头像一定是对应的。但是A、B同时执行,并且执行顺序如下:
时序图

那么执行结果就是user1性别为女,但头像为男性角色。

所以针对于这种情况,我们需要一定的措施去预防。

redis的事务实现方式有多种。据我所知有三种方式:

  • 1.MULTI/EXEC
  • 2.WATCH/UNWATCH
  • 3.lua脚本

1.1 MULTI/EXEC

MULTI/EXEC是成对出现的指令。大家都知道,redis执行指令是单线程的,也就是说所有指令都处于一个队列,一个一个执行。MULTI/EXEC可以保证所有的指令都会被同时放松到redis中,中间不会掺杂来自其他客户端的指令。

但是这个针对于上述情况并不适用。因为我们需要在GET到数据之后,才能做下面的更新操作。

1.2 WATCH/UNWATCH

在redis中我们可以使用watch来监视一个值。

redis> WATCH name
OK

redis> MULTI
OK

redis> SET name peter
QUEUED

redis> EXEC
(nil)

比如这段代码,在我们WATCH name后,如果另外一个客户端修改了name的值,那么这个客户端再次修改name则无法成功。这其实就是乐观锁的一种实现。它保证了代码的执行结果不会错乱。用一开始的例子来说,就是不会出现性别和头像不对应的情况。

1.3 lua脚本
redis中可以执行lua脚本来完成事务操作。lua脚本和MULTI/EXEC很像,也就是说在lua脚本执行的过程中,redis是不执行其他客户端的指令的。

但是lua脚本的不同之处在于,你可以使用GET操作来获取数据并判断,再执行后来的操作。

2.在redis操作中使用锁

在多线程环境中,为了防止资源出现race condition,需要借助锁来互斥的访问资源。在这里也是一样的,user1_gender就是我们要互斥访问的资源。

还是上述那个例子,如果我们使用悲观锁,只有获得锁的客户端才能读取和修改user1的的值,也可以很好的解决这个问题。

伪代码如下:

$can_lock = lock('user1_gender'); //得到锁
if($can_lock){
    do_something();
    $release_lock('user1_gender');  //释放锁
}

不同于多线程环境下的是,我们这里的锁的范围是针对于不同的客户端。因此没法使用基于系统的、或者基于语言的锁,而是得使用分布式的锁。这样的分布式锁我们同样可以借助于redis来实现。

整个分布式锁的实现可以概括为以下几个步骤:

    1. 获得锁。得到要锁的资源的唯一hash:lockname,以及一个随机字符串:identifier,设置expire:20s,这个expire就是锁的有效期,在有效期后锁会自动释放,防止出现死锁。在redis中使用setnx(lockname,identifier,expire)。这句代码的意思是:如果redis中不存在lockname,则存入lockname,值为identifier,过期时间是expire。
    1. 第一步我们就得到了一个锁,这一步我们开始执行获得锁之后的代码
    1. 释放锁。我们根据lockname来从redis中查找。如果get(lockname) identifier,则表示我们仍然持有这把锁,使用delete(lockname)来释放锁。如果不等于,说明我们已经不持有这把锁了,则什么也不做。

那么lock函数可以用以下代码来描述:

function lock(lockname,identifier,expire = 20){acquire_timeout = 10;      //花费10秒去获得锁,否则就放弃
    end = time() +acquire_timeout
    while (time() < end){can_lock = redis->setnx(lockname,identifier,expire);
        if($can_lock){
            return true;
        }
        sleep(0.1);
    }

    return false;
}
    function release_lock(lockname,identifier){
        redis->watch(lockname);
        try{
            if(redis->get(lockname) === identifier){
                //锁仍然持有,释放锁redis->delete(lockname);
                return true;
            }redis->unwatch();
        }catch(Exception $e){

        }

        return false;
    }

这样我们就很轻松的得到了借助于redis实现的分布式锁。但是这样的实现方式依然是有问题的。

问题1:如果在某个客户端获得锁后,redis主服务器宕机了,那么即使我们使用了主从备份,从属服务器被提升为主服务器,因为redis备份是异步的原因,这里的锁是没法及时同步到从属服务器的。

问题2:如果一个客户端在获得锁后,执行的操作超过了锁的有效期,锁被自动释放了。那么后续的操作是没法受到锁的保护的。

问题2的解决方案可以是watch,在获取锁后,可以立刻watch资源,然后再执行余下操作。

问题1的解决方案则是接下来要介绍的redlock算法

3.redlock

redlock算法是Redis的作者antirez提出来的。
可以被概述为以下几个步骤:

  • 1.获取当前时间(毫秒数):start_time。

  • 2.按顺序获得N个Redis节点的锁(使用相同的key和identifier,并设置初始有效时间:init_validity_time)。在获得每个redis结点的锁的时候,都要设置一个timeout参数,这个timeout要远小于锁的自动释放时间。例如:如果锁的自动释放时间是10s,timeout应该为~5-55ms(还得视网络情况决定)。这样可以防止在获取锁时,节点宕机,导致耗时过长锁被释放了。如果获取锁失败则立刻获取下一个redis节点的锁

  • 3.client计算为了获取锁花了多长时间:used_time = current_time – start_time。当且仅当client可以获取大多数实例的时候(至少N / 2 + 1个),所花费的时间小于锁的有效时间,才认为获得了锁。

  • 4.如果获得了锁,重新计算锁的有效时间:validity_time = init_validity_time – used_time

  • 5.如果锁获取失败(无法获取N/2 + 1个节点的锁,或者有效时间validity_time是负数),则释放所有实例的锁(即使获得锁的时候失败了,这主要是考虑到有的时候锁获得成功了,但是告知客户端时网络异常)。

这很好的解决了上述的问题1,通过多个redis节点了来保证分布式锁服务的可靠性。

参考文章

Distributed locks with Redis

基于Redis的分布式锁到底安全吗(上)?

基于Redis的分布式锁到底安全吗(下)?

redis事务

6.2.3 Building a lock in Redis

xdebug的简易使用教程

1.ubuntu下的安装

通过 pecl 安装

pecl install xdebug

然后将xdebug.so加入php.ini中,注意如果使用的是fpm,则需要加入到fpm下的php.ini,同理cli环境下则需要向cli下的php.ini添加

zend_extension=”/usr/local/php/modules/xdebug.so”

注意:xdebug是zend的拓展,不需要添加extension=xdebug.so

通过编译安装

git clone git://github.com/xdebug/xdebug.git
cd xdebug
phpize
./configure –enable-xdebug
make
make install

2.配置php使用xdebug

xdebug有许多特性。

2.1 通过设置来影响var_dump()

影响var_dump()的属性有:

  • xdebug.var_display_max_children,
  • xdebug.var_display_max_data
  • xdebug.var_display_max_depth

这三个属性的值都是数字类型的。会影响var_dump()函数显示的变量的内容长度和深度。

可以在php.ini做以下设置:

xdebug.var_display_max_depth = 2
xdebug.var_display_max_data = 8
xdebug.var_display_max_children = 3

具体的值可以自己手动调整

另外还有xdebug.cli_colorxdebug.overload_var_dump会影响到显示的效果

2.2 堆栈跟踪

演示脚本如下:

<?php
//这个脚本会超时
function foo( a ) {
    for (i = 1; i<a['foo']; i++) {
        if (i == 500000) xdebug_break();
    }
}

set_time_limit(1);
c = new stdClass;c->bar = 100;
a = array(
    42 => false, 'foo' => 9121240000000000,c, new stdClass, fopen( '/etc/passwd', 'r' )
);
foo( $a );
?>

我们使用设置

xdebug.collect_params = 1

结果如下:
params为1

修改一下设置:

xdebug.collect_params = 3

params为3

可以看到浏览器中的报错信息更多了,体现在foo()函数中的参数数量

同样的,我们还可以设置

xdebug.dump_globals = On
xdebug.dump.SERVER = ‘REQUEST_URI’

这样则可以展示一些超全局变量。这里指定了请求的URI

超全局变量

还可以设置

xdebug.show_local_vars = On

来展示程序运行期间的本地变量
本地变量

2.3 函数跟踪

使用xdebug可以记录所有的函数调用。

测试脚本如下:

<?php

ini_set('xdebug.trace_format','0');

xdebug_start_trace();
str = "Xdebug";
function ret_ord(c )
{
    return ord( c );
}

foreach ( str_split(str ) as char )
{
    echochar, ": ", ret_ord( $char ), "\n";
}
xdebug_stop_trace();
?>

使用的设置如下:

;代码跟踪日志文件位置,注意要先新建这个/tmp/php_traces/fpm目录,并设置777
xdebug.auto_trace = Off
xdebug.trace_output_dir = /tmp/php_traces/fpm
;代码跟踪日志文件格式 
xdebug.trace_output_name = trace.%c.%p
;trace中显示函数的参数值,这个很有用,待会细说
xdebug.collect_params = 3
xdebug.collect_includes = On
xdebug.collect_return = On
xdebug.show_mem_delta = On
xdebug.var_display_max_depth = 2

结果如下:
此处输入图片的描述

通过调整xdebug.trace_format的值可以更改记录的格式。0是人类可读,1是机器可读,2是html

2.4远程调试

这里xdebug中非常好用的一个功能。通过设置,我们可以在IDE中单步调试。下面我会使用vscode来演示一遍。

2.4.1 环境准备

1.首先我们需要为vscode安装xdebug的插件。

2.配置好调试环境

此处输入图片的描述

这一步是在vscode左侧调试栏新增配置,然后选择php即可。

3.打上断点,启动调试

此处输入图片的描述

4.在浏览器中访问这个页面即可

此处输入图片的描述

参考链接

https://xdebug.org/docs/all#default

Swoole Server架构分析

一.简介

首页这里引用一下swoole的官方介绍:

swoole:面向生产环境的 PHP 异步网络通信引擎
使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领域。 使用 PHP + Swoole 作为网络通信框架,可以使企业 IT 研发团队的效率大大提升,更加专注于开发创新产品。

通过上述的介绍,我们是可以得出几点信息的

  • 1.swoole可以投入生产环境
  • 2.使用php编写
  • 3.异步网络通信引擎,支持大量的网络协议,并且具有很高的网络性能

swoole server与传统的php运行模式是完全不同的,它是常驻内存的,省去了大量的php脚本的初始化。

二.swoole server的是怎么运行的

swoole进程/线程模型

这是一张官方的swoole运行时进程/线程模型。

1.Master进程

Master进程是一个多线程模型,其中包括Master线程,Reactor线程组,心跳检测线程,UDP收包线程。

以http server为例,Master线程负责监听(listen)端口,然后接受(accept)新的连接,然后将这个连接分配给一个Reactor线程,由这个Reactor线程监听此连接,一旦此连接可读时,读取数据,解析协议,然后将请求投递到worker进程中去执行。

Master进程是使用select/poll进行IO事件循环的,这是因为Master进程中的文件描述符只有几个(listenfd等),Reactor线程使用的是epoll,因为Reactor线程中会监听大量连接的可读事件,使用epoll可以支持大量的文件描述符。

2.Manager进程

Manager进程是专门用来管理Worker进程组和Task进程组的。它会Fork出指定数量的Worker进程和Task进程,并且有以下职能:

  • 子进程结束运行时,manager进程负责回收此子进程,避免成为僵尸进程。并创建新的子进程
  • 服务器关闭时,manager进程将发送信号给所有子进程,通知子进程关闭服务
  • 服务器reload时,manager进程会逐个关闭/重启子进程

3.Worker进程和Task进程

Worker进程接收Reactor线程投递过来的数据,执行php代码,然后生成数据交给Reactor线程,由Reactor线程通过tcp将数据返回给客户端。(如果是UDP,Worker进程直接将数据发送给客户端)。

Worker进程中执行的php代码和我们平时写php是一样的,它等同于php-fpm。但是众所周知,php-fpm下,php在处理异步操作时是很无力的,swoole提供的Task进程可以很好的解决这个问题。Worker进程可以将一些异步任务投递给Task进程,然后直接返回,处理其他的由Reactor线程投递过来的事件。

Task进程以完全同步阻塞的方式运行,一个Task进程在执行任务期间,是不接受从Worker进程投递的任务的,当Task进程执行完任务后,会异步地通知worker进程告诉它此任务已经完成。

所以介绍完上述的一些概念后,再引用一张官方的swoole执行流程图。
swoole运行流程图

这里需要注意,在文档上说的是:Workder进程组和Task进程组是由Manager进程Fork出来的,但是流程图上画的是在启动服务器时Fork出主进程和Worker进程组以及Tasker进程组。

三、使用swoole和传统php开发的优缺点

在说这个话题之前,需要先了解一下CGI,FASTCGI。

1.CGI
CGI的全称是Common Gateway Interface,通用网关接口,它使得任何一个拥有标准输入输出的程序拥有提供web server的能力。假设我们写了一个Hello World的c++程序,这个程序接受输入{text},输出{text},Hello World。

以nginx作为接受http请求为例,nginx接受一个http请求,Fork出一个进程,将http请求带来的text参数作为输入,执行完hello world程序,将输出{text},Hello World作为输出,销毁这个Fork出来的进程,由nginx返回给客户端。

这种方式虽然简单,但是要不断的Fork进程,销毁进程。

2.FASTCGI
FASTCGI,顾名思义,它是CGI的改进版,是一个常驻型的CGI服务。我们常用的php-fpm就是这种模式运行的,php-fpm负责Forl多个进程,每个进程中都运行了php的解释器。可以在终端下看一下php-fpm的进程:
php-fpm进程

一个php-fpm主进程,pid是1263,Fork出了3个子进程。在nginx+php-fpm的组合中,nginx负责接受http请求,将请求封装好交给php-fpm,php-fpm将请求按照一定的规则交给一个子进程去执行,这个子进程中的php解释器加载php代码运行。也是因为这个原因,传统的php只能作为web server。

然后我们发现,nginx+php-fpm的组合和我们Reactor+Worker子进程的运行方式非常相似。

3.swoole的运行方式
这里以swoole作为http server为例(传统php几乎都是作为web服务)。

首先swoole是实现了http server的,也就是说不需要nginx作为http服务器了,当然swoole并不是为了取代nginx,实际上swoole当前实现的http server功能有限,比如说只支持Get和Post,所有往往swoole前面还要运行一个nginx来作为前端代理服务器。

其次,swoole是内存常驻的。和php-fpm的常驻服务不同,php-fpm中常驻的是php的解释器,这个解释器会重复加载php代码,初始化环境,而swoole只在启动的时候加载,这样一来,性能就自然而然的提高了。这一点可以在开发中很明显的体现出来,php-fpm下,修改的php代码会即时生效,而使用swoole则需要重启swoole的server才能使代码生效。

通过上面的一些说明,就可以很明显的得出swoole和传统php开发的优缺点了。

swoole server优点:
– swoole性能更高
– 可以做为tcp,udp服务器
– 在高io高并发的服务器要求下,swoole的运行模式是完全可以胜任的

swoole server缺点:
– 更难上手。这要求开发人员对于多进程的运行模式有更清晰的认识
– 更容易内存泄露。在处理全局变量,静态变量的时候一定要小心,这种不会被GC清理的变量会存在整个生命周期中,如果没有正确的处理,很容易消耗完所有的内存。而以往的php-fpm下,php代码执行完内存就会被完全释放。
– 无法做密集计算。当然这一点是php甚至是所有动态语言都存在的问题。写在这里是因为防止误导读者以为使用swoole后,php可以用来做密集计算。