用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,并且开发扩展也要容易很多。

HTTP协议中的缓存控制

一、总览

HTTP协议中有以下的头部字段和缓存相关(很多内容都是复制的MDN的文档)

字段名 请求头包含 响应头包含 含义 出现的协议版本
Cache-Control 是否缓存、缓存时间、缓存验证等 HTTP/1.1
Pragma 只有一种用法:Pragma: no-cache。在响应头中没有规定 HTTP/1.0
Vary 它决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复 HTTP/1.1
If-Match 在请求方法为 GET 和 HEAD 的情况下,服务器仅在请求的资源满足此首部列出的 ETag 之一时才会返回资源。而对于 PUT 或其他非安全方法来说,只有在满足条件的情况下才可以将资源上传 HTTP/1.1
If-None-Match 对于 GET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为 200 。对于其他方法来说,当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候,才会对请求进行相应的处理。 HTTP/1.1
If-Modified-Since 服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为200。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的304响应,而在 Last-Modified 首部中会带有上次修改时间。不同于If-Unmodified-Since, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。 HTTP/1.1
If-Unmodified-Since 只有当资源在指定的时间之后没有进行过修改的情况下,服务器才会返回请求的资源,或是接受 POST 或其他 non-safe 方法的请求。如果所请求的资源在指定的时间之后发生了修改,那么会返回 412 (Precondition Failed) 错误。 HTTP/1.1
ETag TagHTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用ETag有助于防止资源的同时更新相互覆盖(“空中碰撞”) HTTP/1.1
Expires Expires 响应头包含日期/时间, 即在此时候之后,响应过期 HTTP/1.1
Last-Modified 包含源头服务器认定的资源做出修改的日期及时间。 它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。 HTTP/1.1
Date 消息生成的时间 HTTP/1.1
If-Range If-Range HTTP 请求头字段用来使得 Range 头字段在一定条件下起作用:当字段值中的条件得到满足时,Range 头字段才会起作用,同时服务器回复206 部分内容状态码,以及Range 头字段请求的相应部分;如果字段值中的条件没有得到满足,服务器将会返回 200 OK 状态码,并返回完整的请求资源。 HTTP/1.1

二、详细说明

一眼看上去,缓存相关的字段确实有很多。但是实际上,稍微理一理思路即可。

上面所有的字段都在围绕着3个点:

  • 1.是否要缓存
  • 2.缓存多久
  • 3.缓存是否有效

2.1 是否要缓存

一个HTTP的客户端(包括浏览器,以及CDN等缓存代理)如何知道当前的请求是否要缓存呢?

Cache-Control中,有下列取值来决定是否缓存:

public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。
private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),可以缓存响应内容。
no-store:缓存不应存储有关客户端请求或服务器响应的任何内容。

2.2 缓存多久

源服务器上的内容可能随时发生变化,那么如何知道什么时候去检查缓存是否更新了呢?HTTP协议中有以下字段规定了一个缓存的有效期。

Cache-Control

max-age={seconds}:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
s-maxage={seconds}:覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。
max-stale[={seconds}]:表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。
min-fresh={seconds}:表示客户端希望在指定的时间内获取最新的响应。

Expire

Expires: {http-date}: 表示在http-date之后,这个缓存就过期了。如果http-date是一个无效的时间值,则代表已过期。如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age" 指令,那么 Expires 头会被忽略。

Date和Last-Modified

如果在`Cache-Control`和`Expire`都没有返回的情况下,也可以通过`Date`头和`Last-Modified`头去计算缓存的有效期。缓存的寿命就等于头里面Date的值减去Last-Modified的值除以10。

2.3 缓存是否有效

源服务器上的内容可能随时发生变化, 那么HTTP客户端如果知道自己缓存的内容是否有效呢?这里要分几种情况:

  • 服务器的响应中有Cache-Control:must-revalidate头:当前缓存在有效时间内,此时缓存默认就是有效的。当前缓存过了有效时间,则会向服务器验证缓存是否过期。如果服务返回304,则代表缓存没有过期。
  • 服务器的响应中有Cache-Control: no-cachePragma:no-cache头:则表示每一次都要从服务器验证缓存是否过期。no-cache的优先级是要大于Pragma的

注:no-cache和must-revalidate的区别

在RFC7234中说到:

“must-revalidate” 响应头指令表示一旦该响应过期,这个缓存在向源服务器成功验证之前禁止使用。在任何情况下,一个缓存都必须遵循”must-revalidate”指令;特殊情况下,如果源服务器无法连接,必须生成504(Getway Timeout)响应。

“no-cache”响应头指令表示缓存在向源服务器成功验证之前禁止使用(注:不论缓存是否过期)。如果”no-cache”指令指明了一或多个字段,缓存可以被用来响应之后的请求。但是,在没有和源服务器进行验证的情况下,任何”no-cache”中列出的字段都禁止在之后的响应中被发送。这使得源服务器可以阻止某些字段被重复使用,但是仍然可以缓存响应的其他部分。

“no-cache”中的字段不仅仅限于http1.1协议中列举出来的字段。字段名是大小写不敏感的。使用双引号包围。

因此个人认为,在某些时候max-age=0;must-revalidate 可以等同于 no-cache。

那么这个验证机制是什么样的?也分几种情况

  • 根据文件指纹ETag:在服务器返回了一个文件的ETag的情况下,HTTP客户端可以根据If-None-MatchIf-Match来向服务器验证当前缓存是否过期。
  • 根据文件修改时间:在服务器返回了Last-Modified的情况下,HTTP客户端可以根据If-Unmodified-SinceIf-Modified-Since来向服务器验证当前缓存是否过期。
  • 一个比较特殊的If-RangeIf-Range通常出现在分段请求当中,用来分段请求的资源主体是否发生了变化。它的值既可以是etag,也可以是GMT时间戳。

注:因为Last-Modified精确到秒,在精确度上比ETag低,所以应该以ETag为主。

2.4 关于vary字段

上面说了三个点,但是没有涉及到vary字段。vary和缓存并不是直接相关的。取一段MDN的说明:

Vary 是一个HTTP响应头部信息,它决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复。它被服务器用来表明在 content negotiation algorithm(内容协商算法)中选择一个资源代表的时候应该使用哪些头部信息(headers).

在响应状态码为 304 Not Modified 的响应中,也要设置 Vary 首部,而且要与相应的 200 OK 响应设置得一模一样。

举个例子,如果服务器返回的网页是分手机版和电脑版的,一般我们会根据user-agent来判断浏览器是手机浏览器还是电脑上的浏览器。假设有一个中间代理的请求:

手机用户1请求index.html---------->中间代理------------>源服务器
电脑用户1请求index.html---------->中间代理------------>源服务器

在电脑用户1请求index.html时,中间代理会向原服务器请求还是直接使用本地缓存的副本呢?

如果原服务器在第一次请求时响应头中有vary:user-agent则会重新请求。因为两次请求的user-agent是不同的,因此缓存不能被重复使用。但是如果没有指定则使用本地缓存作为响应。


参考文档

HTTP缓存控制小结
HTTP 协议中 Vary 的一些研究
http://www.cnblogs.com/chyingp/p/no-cache-vs-must-revalidate.html
HTTP缓存

HTTP Caching | MDN

Cache-Control | MDN
Pragma | MDN
Vary | MDN
If-Match | MDN
If-None-Match | MDN
If-Modified-Since | MDN
If-Unmodified-Since | MDN
ETag | MDN
Expires | MDN
Last-Modified | MDN
If-Range | MDN

从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