go WebAssembly初体验

WebAssembly是一门新的浏览器技术。可以取代一部分js的角色,并且从性能上来说,要比js好很多。WebAssembly目前还处于早期的发展阶段,仍然不够成熟。go语言对WebAssembly的支持也是在1.11版本上刚刚加入。虽然不能投入生产环境,但是可以用来做一些很有意思的事情。

官方WebAssembly的文档:https://github.com/golang/go/wiki/WebAssembly。详细的介绍可以看官方的文档。

一个go WebAssembly的例子

main.go

package main

import "fmt"

func main() {
    fmt.Println("hello WebAssembly")
}

编译这个go文件: GOOS=js GOARCH=wasm go build -o main.wasm main.go

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Go wasm</title>
</head>
<body>
    <script src="wasm_exec.js"></script>
    <script>
        const go = new Go();
        WebAssembly.instantiateStreaming(
            fetch("main.wasm"),go.importObject).then((result) => {
            go.run(result.instance)
        });

    </script>
</body>
</html>

这里还需要一个wasm_exec.js。这个文件是官方提供的,可以理解为是go编译出来的二进制文件和js间的连接桥梁。这个时候打开浏览器,可以看到控制台下有:hello WebAssembly

需要说明的是,在go里面的所有STDOUT,都会在浏览器的控制台打印出来。

wasm的初始化过程

WebAssembly.instantiateStreaming(fetch("main.wasm"),go.importObject)。浏览器执行这样一段代码来加载main.wasm,并返回一个Promise对象。在加载完毕之后,调用go.run(result.instance)来执行wasm中的代码。

加载一个wasm文件就是这样的简单。

go WebAssembly如何和js交互

上面的例子非常简单。但是WebAssembly技术绝非这样简单。我们可以在go中轻松的调用js中的方法。比如下面这句话:

js.Global().Get("document").Call("getElementById", "maxCubes").Set("value", 256)

相当于js中的

document.getElementById("maxCubes).setAttribute('value', 256)

同样的,我们也可以在go中定义js方法,这样就可以在js中直接调用了。

api.onMemInitCb = js.NewCallback(func(args []js.Value) {
        length := args[0].Int()
        api.console.Call("log", "length", length) // 调用js的console.log("length", length)
        api.inBuf = make([]uint8, length)
        // 拿到这个slice的SliceHeader
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&api.inBuf))
        ptr := uintptr(unsafe.Pointer(hdr.Data))

        api.console.Call("log", "ptr:", ptr)
        js.Global().Call("gotMem", ptr)

        fmt.Println("初始化Mem成功")
    })

js.Global().Set("initMem", api.onMemInitCb)

这样,我们就可以在js中直接使用initMem方法了。这样子,就相当于打通了go和js之间的通道,使得go WebAssembly几乎无所不能。

go和js之间如何通过内存传值。

这个部分是我在看go WebAssembly部分时最关注的。因为大多数时候,我们传参都不仅仅是256这样的字面值。比如我们在做图片处理的时候,浏览器加载图片后传给wasm去处理,这种时候传递的肯定是一个指向一段内存的指针。

go代码中:

api.onMemInitCb = js.NewCallback(func(args []js.Value) {
    length := args[0].Int()
    api.console.Call("log", "length", length) // 调用js的console.log("length", length)
    api.inBuf = make([]uint8, length)
    // 拿到这个slice的SliceHeader
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&api.inBuf))
    ptr := uintptr(unsafe.Pointer(hdr.Data))

    api.console.Call("log", "ptr:", ptr)
    js.Global().Call("gotMem", ptr)

    fmt.Println("初始化Mem成功")
})

js代码中:

function gotMem(pointer) {

    console.log("pointer", pointer)

    memoryBytes.set(bytes, pointer);
    // Now the image can be loaded from the slice.

    console.log("load image")
    loadImage();
}

......

let reader = new FileReader();
reader.onload = (ev) => {
    bytes = new Uint8Array(ev.target.result);
    initMem(bytes.length);
    let blob = new Blob([bytes], {'type': imageType});
    document.getElementById("sourceImg").src = URL.createObjectURL(blob);
};
imageType = this.files[0].type;
reader.readAsArrayBuffer(this.files[0]);

上面有两段代码,js部分代码是在加载一张图片,然后转换为Uint8Array数组bytes,然后调用initMem方法,传递一个数组bytes的长度作为参数。而initMem是在go代码中定义的,initMem负责调用make([]uint8, length)去初始化需要的内存。然后通过一系列的转换获得申请的内存区域的指针ptr。然后调用js的gotMem将这个ptr传递给js代码。在gotMem中,memoryBytes.set(bytes, pointer)这句代码初始化了这块内存。

这里值得指出的是,memoryBytes是一段在wasm初始化时申请的内存,保存在一个Uint8Array数组中。如果我们打印出来的话,可以发现这段内存有1G。然后我们的go代码中调用make([]uint8, length)申请一块内存时,其实是在这段内存中申请的。比如说申请的区域为201883648~(201883648+107003)。我们只要在js中向memoryBytes的数组中给这块区域赋值,就把值传递给go的对象了。

再考虑一个问题。1G的初始化内存是不是太大了?这个内存是在编译go代码时由编译工具指定的。但是如果我们使用浏览器(比如chrome)的任务管理器查看这个窗口的占用内存时就会发现,实际占用并不会这么大。

在我的理解中(并不一定正确),这和C语言的malloc类似。malloc可以申请大于物理内存的虚拟内存,但是只要你不实际占用这么大内存,是不会有问题的。所以虽然go WebAssembly打印出来有1G的初始化内存,但是如果不是真的会使用,是不会占用这么大物理内存的。

关于初始化内存过大的问题,可以参考这个issues:cmd/compile: wasm code causes out of memory error on Chrome and Firefox for Android

一些关于WebAssembly内存的设计:Finer-grained control over memory

一个用go WebAssembly实现中位切分法处理图片的例子

关于中切分法可以参考我之前的这篇文章:中位切分法颜色量化

可以在这个地址进行预览: 预览地址

在浏览器中运行的效果(这里只展示了FastMap的效果,BestMap会好很多):

fastmap

代码的github地址是:WebAssembly-MedianCut

里面大多数的代码都是用的我之前的代码。可见go WebAssembly还可以非常舒服的复用之前的代码。

中位切分法颜色量化

首先举两个例子。

有一种视频接口叫VGA,这种视频接口有一个最大的缺点,就是同一时刻无法显示超过256种颜色。而对于一张true-color的图片,R、G、B都是有1个byte标示,因此一张true-color的图片可能有2^24种颜色。这远远超过了VGA可以同时展示的颜色数量。

我们都知道有一种图片格式叫PNG(
PNG格式分析与压缩原理
)。它的编码方案中,有一种使用调色板的编码方案。调色板上最多有256种颜色。这样每一种颜色都可以用一个byte的索引值代替。如果将一副超过256种颜色的png图使用调色板的编码方式,那么就可以明显的减少图片的体积。

那么如何将2^24种颜色用256种颜色表示呢?或者说,如何将m种颜色使用n种颜色来代替表示(m>n)。这就是这篇文章主要讨论的问题。

首先,如下图所示,RGB可以映射到三维空间中,R代表X轴,G代表Y轴,B代表Z轴。这样,任意一个RGB的组合都可以在空间中以一个点表示。

rgb-cube

对于一个24-bit的图片来说,通常来说颜色空间是连续的,因为颜色之间的最小差异是几乎不可能察觉的。现在,这个连续的颜色空间要被映射到256种离散的颜色上。将一个连续的变量映射到离散的集合上,就叫做量化

有了上面这些说明后,现在有一张颜色很多的图片。图片上所有的点都被映射到一个空间中的立方体上。因为图片上点的分布一般不是均匀的,那么就可能有很多个点的聚集块。一般来说,聚集的越密,代表这些点的颜色越接近,那么如果我们将这些聚集块的点用聚集块中心点的颜色来表示,这样就可以将很多接近的颜色用一种颜色来代替。其实,这个过程跟聚类是很相似的。只不过一般的聚类算法都是无监督的机器学习,效率要较差。而中位切分法则可以用很快的速度完成这样的聚类。

中位切分法

中位切分法用简短的话描述:将一个图片对应的RGB立方体切割成目标数量的紧凑的RGB立方体,然后用立方体的质心值代表立方体内所有点的值。重复这个过程直到得到想要的颜色数量。

这里假设我们要获取256个颜色。详细的流程如下:

  • 将整张图片转换成一个RGB立方体
  • 找到立方体的最长边,从中位数的地方开始切割。得到两个包含相同数量点的立方体。
  • 对分割成的立方体重复上一步的切割过程直到得到256个立方体。
  • 256个立方体的质心就是要计算的256个颜色值。

用一个例子来说明(为了简单,这里没有B颜色空间),这里有6种颜色,共14个点。想要得到4中颜色输出。

  1. 初始情况
Color (r,g)-coordinates Count
C0 (20,40) 3
C1 (40,20) 2
C2 (5,60) 4
C3 (50,80) 2
C4 (60,30) 1
C5 (80,50) 2
  1. 3次切割后的情况
Cube HistPtr.lower HistPtr.upper Colors Enclosed Cube Centroid
A3 0 0 C0 (20,40)
B3 1 1 C2 (5,60)
A2 2 3 C1,C4 (46.7,23.3)
B2 4 5 C5,C3 (65,65)

具体流程图如下:

第一次:最长边是R,中位数是(20 + 40) / 2 = 30。从30处切割。得到收缩后的矩形。左边的矩形为A1,右边为B1。
第二次: 切割B1的R边,得到A2,B2
第三次: 切割A1的R边,得到A3,B3

这个时候我们已经有4个矩形了。

这里我们有两个颜色映射方式。第一种是快速映射,以矩形的质心作为映射后的颜色值。第二种是最佳映射,得到和其他点的距离和最短的点作为映射后的颜色值。

切割过程

中位切分法的实现

  1. 递归方式。在描述的时候,我们RGB块描述成递归的方式。如下
Split(Cube){
  if (ncubes == 4) return;
  find longest axis of Cube;
  cut Cube at median to form CubeA, CubeB;
  Split(CubeA);
  Split(CubeB);
}

但是这种递归切割的方式是有问题的。结果如下图
递归切割

在递归情况下,B1将一直不会被处理。

2.为了解决1中的问题。我们可以设定一个最大深度(level)。比如我们要得到4个输出颜色。那么log 2 4=2。最大深度应该是2。当切割到最大深度时,我们就不再往下切割,转而切割那些还未被处理的更大的区域。

maxlevel = 2;
Split(Cube,level){
  if (ncubes == 4) return;
  if (Cube's level == maxlevel) return;
  find longest axis of Cube;
  cut Cube at median to form CubeA, CubeB;
  Split(CubeA, level+1);
  Split(CubeB, level+1);
}
  1. 但是这样又会有新的问题出现。如果一个立方体中只有一种颜色(实际上此时只有一个点),不能再往下切割。因此我们需要一种方案去解决这个问题。这里可以维持一个包含所有立方体的数组,然后根据优先级排序切割。优先级可以按照level从小到大排序,但是所有颜色数量为1的立方体都会被忽略。这样每次切割前对这个数组做一次排序,取出优先级最高的立方体进行切割。直到切割出指定数量的立方体或者所有的立方体颜色都为1。
build initial cube from histogram;
set initial cube's level to 0;
insert initial cube in list of cubes;
ncubes = 1;
while (ncubes < maxcubes){
  search for Cube with smallest level;
  find the longest axis of Cube;
  find the median along this axis;
  cut Cube at median to form CubeA, CubeB;
  set CubeA's level = Cube's level + 1;
  set CubeB's level = Cube's level + 1;
  insert CubeA in Cube's slot;
  add CubeB to end of list of cubes;
  ncubes = ncubes + 1;
}

实践

  1. 使用中位切分法提取图片的主题色

  2. 使用中位切分法压缩图片

中位切分法的实现的go语言版本:joyme123/MedianCut点击这里进行效果预览

参考文献

Median-Cut Color Quantization

前端图片主题色提取

virtualbox 网络桥接

virtualbox的默认方式是NAT,用宿主机对虚拟机做端口转发。在组建本地的集群环境时,使用这种方式是不行的。可以使用桥接网卡的方式,使得虚拟机分配到一个宿主机局域网内的ip地址。

首先,修改/etc/network/interfaces文件。

这个文件原来的内容如下:

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
iface enp0s3 inet dhcp

修改成如下:

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto enp0s3
#iface enp0s3 inet dhcp
iface enp0s3 inet static
address 192.168.0.100
gateway 192.168.0.1
netmask 255.255.255.0

注意这里的addressgateway的网段要和宿主机网段一致。比如我的宿主机ip为192.168.0.8

注意这里还要修改一下dns,不然会出现无法解析域名的问题,编辑/etc/resolvconf/resolv.conf.d/base

添加一下的nameserver:

nameserver 8.8.8.8
nameserver 1.1.1.1

执行sudo resolvconf -u使dns的配置生效。

然后修改虚拟机的设置,在VirtualBox的菜单栏中:设备->网络->网络->连接方式:桥接网卡。

然后重启网络

sudo /etc/init.d/networking restart

如果网络还是有问题,可以尝试重启虚拟机。

之后在终端之中输入ifconfig,网络信息如下:

enp0s3    Link encap:Ethernet  HWaddr 08:00:27:f1:6d:fd  
          inet addr:192.168.0.100  Bcast:192.168.0.255  Mask:255.255.255.0
          inet6 addr: fe80::a00:27ff:fef1:6dfd/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:304 errors:0 dropped:0 overruns:0 frame:0
          TX packets:173 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:28254 (28.2 KB)  TX bytes:28063 (28.0 KB)

虚拟机的ip已经变成192.168.0.100。使用ping 192.168.0.8检查和宿主机的通信是否正常。

TLS1.2 RFC5426中一些术语解释

最近想为我的cats服务器加上https的支持。因为最近有一点点的忙,这个项目已经很久没有提交新的代码了。之所以没有用一些开源的库去做,因为这个项目的目的就是锻炼我的代码能力,以及英文RFC的阅读能力。但是在看RFC5426时遇到了一些挫折,里面有大量的密码学上的专业词汇。因此买了一本《图解密码技术》,这里将RFC5426中的专业词汇和概念单独拿出来做一次笔记。

PRF( pseudorandom function ) algorithm: 伪随机函数算法。随机数有三类性质:1.随机性。2.不可预测性。3.不可重现性。这三个性质要求越来越严格。满足1,称为弱伪随机数,满足1、2,称为强伪随机数,满足1、2、3,称为真伪随机数。在密码学的体系中,要求至少达到强伪随机数才能保证安全。

public key encryption:公开密钥加密(英语:Public-key cryptography),也称为非对称加密(英语:asymmetric cryptography),是密码学的一种算法,它需要两个密钥,一个是公开密钥,另一个是私有密钥;一个用作加密的时候,另一个则用作解密。使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文;甚至连最初用来加密的密钥也不能用作解密。由于加密和解密需要两个不同的密钥,故被称为非对称加密;不同于加密和解密都使用同一个密钥的对称加密。虽然两个密钥在数学上相关,但如果知道了其中一个,并不能凭此计算出另外一个;因此其中一个可以公开,称为公钥,任意向外发布;不公开的密钥为私钥,必须由用户自行严格秘密保管,绝不透过任何途径向任何人提供,也不会透露给要通信的另一方,即使他被信任。参考链接: 公开密钥加密

RSA: 名称不是什么缩写,而是发明人首字母的结合。是一种非对称加密的方法。它的速度比起DES等对称加密算法要慢的多。因为非对称加密中,有一个公钥和一个私钥,那么如果分配公钥则是一个问题。如果通过网络传输公钥,则可能被中间人替换掉公钥进行攻击.因此一般的做法是用可靠的第三方机构签发证书来防止这样的攻击。参考链接:RSA加密演算法

MAC (Message Authentication Code) algorithm: 消息验证码的算法.是经过特定算法后产生的一小段信息,检查某段消息的完整性,以及作身份验证 。它可以用来检查在消息传递过程中,其内容是否被更改过,不管更改的原因是来自意外或是蓄意攻击。同时可以作为消息来源的身份验证,确认消息的来源。在我的理解中,MAC是一种与密钥相关联的单向散列函数。参考链接:消息认证码

HMAC (Keyed-Hashing for Message Authentication): 它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。其实就是常用的加salt的方式,使得相同的原值生成不同的哈希值。HMAC是用来生成MAC值的。参考链接:密钥散列消息认证码

DSA (digital signing algorithm):DSA是一种更高级的验证方式。一般用于数字签名和认证。DSA 不单单只有公钥、私钥,还有数字签名。私钥加密生成数字签名,公钥验证数据及签名。在DSA数字签名和认证中,发送者使用自己的私钥对文件或消息进行签名,接受者收到消息后使用发送者的公钥来验证签名的真实性。如果数据和签名不匹配则认为验证失败!数字签名,不仅能验证数据的完整性,真实性,还能“对第三方证明”和“防止否认”。参考链接:常见的加密算法之DSA 算法

CBC (Cipher Block Chaining):分组密码的一种工作模式,允许使用同一个分组密码密钥对多于一块的数据进行加密,并保证其安全性。其他的工作模式还有:ECB,PCBC,CFB,OFB,CTR等。参考链接:分组密码工作模式

SHA256, SHA1, MD5: 常见的几种信息摘要算法(有时候也称为哈希算法,单向散列函数等)。

SSL (Secure Socket Layer):安全套接字层

TLS (Transport Layer Security Protocol):安全传输层协议,TLS的后续工作是在SSL的基础上进行的。

stream cipher encryption:在密码学中,流密码(英语:Stream cipher),又译为流加密、数据流加密,是一种对称加密算法,加密和解密双方使用相同伪随机加密数据流(pseudo-random stream)作为密钥,明文数据每次与密钥数据流顺次对应加密,得到密文数据流。实践中数据通常是一个位(bit)并用异或(xor)操作加密。参考链接: 流密码

block cipher encryption:在密码学中,分组加密(英语:Block cipher),又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密。分组加密是极其重要的加密协议组成,其中典型的如DES和AES作为美国政府核定的标准加密算法,应用领域从电子邮件加密到银行交易转帐,非常广泛。参考链接: 分组加密

authenticated encryption with additional data (AEAD) encryption:认证加密(英语:Authenticated encryption,AE)和用于关联数据的认证加密(authenticated encryption with associated data,AEAD,AE的变种)是一种能够同时保证数据的保密性、 完整性和真实性的一种加密模式。参考链接: 用于关联数据的认证加密

compression algorithm:数据压缩算法

master secret:主密码用来生成对称密码的秘钥,消息认证码的秘钥以及对称密码CBC模式所使用的初始化向量(IV)

rabbitmq遇到的一次tcp半打开的问题

问题描述

业务中有一个测试服务器,里面运行了好几个任务队列。但是自从将任务队列从redis迁移到rabbitmq上后,一直会在运行一段时间后停止运行。一般这种情况下,要么是进程退出了,要么是连接断开了。但是检查后发现,进程是正常运行的,并且通过netstat发现,连接也一直存在。如果是连接断开,我的代码中也做了断线重连的机制。

问题排查

一开始以为是php-amqplib这个库的问题,就去找它的issue,看看有没有类似的问题。这里没有找到类似的bug。

然后去复现了一个最小的demo,这个问题一直都是出现在运行相当长的时间之后。于是决定对测试服务器上已经出现问题的代码进行抓包。新push的任务,任务队列的进程就是接收不到,但是netstat结果,任务队列到rabbit服务的连接又确实是存在的。

没有办法就去随便翻翻《UNIX网络编程》的TCP章节,然后想到TCP的全双工特性。全双工特性就是说在一个给定的连接上应用可以在任何时候在进出两个方向上既发送数据又接收数据。建立一个全双工连接后,需要的话可以把它转换成一个单工连接。于是用netstat检查rabbit服务的连接,发现是没有到任务队列的连接的。所以问题就是出现在这里。**但是仔细思考一下,这里的问题并不能用全双工特性去解释,因为全双工转成单工是tcp的一个特性,但是在这个问题中,应该是一个异常情况。全双工是需要双端协商的,而我这里的问题应该是:

服务器关闭了连接,但是任务队列却没有收到关闭的Fin报文,很多时候被称为半打开

问题解决

解决这个问题很简单,启用php-amqplib的心跳包机制即可。

更多的思考

1.半连接,半打开,半关闭(以下用A,B代表连接的两端)

半连接:出现在tcp的三次握手阶段。A发送syn,B响应ack,syn后,此时处于半连接状态。如果A不发送ack,B将会一直为这个半连接分配一段内存空间。因此可以使用这个特点对B进行半连接攻击

半打开(half-open):A断开连接但是却没有发送Fin报文,导致B不知道。在维基百科上半打开和半连接是相同的。

半关闭:在关闭的4次挥手阶段,A端发送Fin,B端ack但是不发送Fin。

半连接、半关闭都是正常出现的情况。半打开则是不正常的状态,一个Unix进程无论自愿地(调用exit或是从main函数中返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。也就是说,只有当服务器断电等这种非正常关闭的情况下才会出现半连接,否则对端都应该收到Fin报文,然后关闭连接。

1.什么情况导致了半打开?

服务器断电这类情况肯定是没有出现的,所以一定是其他地方有问题导致了这个情况。因为这个问题只在测试服务器上出现,生产服务器上并没有。所以有点难推测。

2.双向连接(bidirectional)和全双工(full-duplex)

在我的理解中,双向连接指的是A端确认了到B端的连接,B端也确认了到A端的连接。全双工则指可以同时发送和接收,互不干扰。

3.心跳机制是如何避免这种情况的?

心跳机制一般都是隔一段时间主动发送一个消息给对端来确认连接是否存活。如果连接丢失,则必然不会收到对端的响应。这样在响应超时后重新发起连接即可。

其实tcp也有一个keep-alive机制。与心跳包作用类似,但是一是检查的周期长,二是一旦启用,机器上所有的连接都会启用这个机制,导致资源浪费。

参考

TCP half-open
半连接、半打开、半关闭

go 内存模型

简介

go的内存模型旨在说明:一个协程中对变量v的写入产生的值可以保证被另一个协程中的对变量v的读取观察到。

Happens Before

在一个协程内,读写操作必须按照程序指定的顺序进行。在一个协程内,编译器和处理器可能对读写操作重写排序,但是这个排序的前提是:在当前协程内,不会改变程序的执行行为。但是这个重新排序是不保证其他协程观测到执行顺序是不改变的。比如在协程1中a=1;b=2,但在其他协程的感知中,可能b比a先更新值。

我们这里定义Happens Before(在...之前发生),如果事件e1在事件e2之前发生,那么我们就可以说e2在e1之后发生。如果e1既不在e2之前发生,也不在e2之后发生。那么e1和e2就是同时发生的(并发)。

在一个协程内,Happens Before的顺序就是程序表达的那样。

如果下面两点可以保证,就说明对变量v的读取r允许观察到对变量v的写入w:

  • r不是在w之前发生
  • 在w之后并且r之前没有其他的对v的写入w’

为了保证变量v的读取r观察到v的特定写入w,并且保证w是唯一允许被r观察到的。也就是说,r保证能观察到w。需要做到下面两点:

  • w在r之前发生
  • 其他的对v的写入w’要么发生在w之前,要么发生在w之后

下面两点比上面两点要求更为严格。它保证了没有其他的写入w’和w、r同时发生。

在一个协程内,因为没有并发,所以这两种定义是一致的:读取r可以观察到写入w对变量v最近一次的写入。但是当多个协程同时访问同一个共享变量时,就必须使用同步事件来建立Happens Before语义来保证读取r可以观察到指定的写入w。

在内存模型中,对变量v以0值初始化是一次写入。

对于大于单机器字节的读取和写入,可以看做是对多个单机器字节的乱序操作。

同步

初始化

程序初始化是在单协程内运行的,但是这个协程可能创建其他的协程。它们是并发的。

如果包p引入了包q,则q的初始化函数会在p的初始化函数之前运行。

main.main函数在所有的初始化函数之后运行。

协程创建

go关键字创建协程发生在协程运行之前。

协程销毁

协程的销毁不保证在程序中的任何事件发生之前。比如:

var a string

func hello() {
    go func() { a = "hello" }()
    print(a)
}

这个赋值没有跟随任何同步事件,所以它不保证被其他协程观察到。事实上,激进的编译器会删除整个go语句。

如果需要,可以使用同步原语比如管道通信来建立一个相关的执行顺序。

管道通信

在go的协程中,管道通信是非常重要的一个同步方法。通常发送方和接受方在两个不同的协程中,利用发送和接收这两个有序的动作来进行同步。

1.在有缓冲的管道中,发送一定发生在接收完成前。(A send on a channel happens before the corresponding receive from that channel completes.

例如:

var c = make(chan int, 10)
var a string

func f() {
    a = "hello, world"
    c <- 0
}

func main() {
    go f()
    <-c
    print(a)
}

a = "hello, world"一定在c<-0之前发生,c<-0一定在<-c之前发生,<-c一定在print(a)之前发生。这样就能保证a = "hello, world"print(a)之前发生。则保证可以打印出hello, world

2.管道的关闭一定发生在从管道中接收值之前。

因此上面的例子将<-c替换成close(c)也是可以的。

3.在无缓冲管道中,接收一定发生在发送完成前。(The closing of a channel happens before a receive that returns a zero value because the channel is closed.)

例如下面的例子将发送和接收语句互换了位置。

var c = make(chan int)
var a string

func f() {
    a = "hello, world"
    <-c
}
func main() {
    go f()
    c <- 0
    print(a)
}

如果上述例子中管道是有缓冲的(e.g., c = make(chan int, 1)) ,就无法保证一定能打印出hello,world

4.在容量为C的管道中,第k个接收发生在k+C个发送完成之前。(The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes.)

第4点推广了第一点的规则。这里其实有一点绕,举个例子:第1个接收发生在1+C个发送完成之前。首先思考:第一个接收能否保证在0+C个发送完成之前?答案是不能。因为管道有C个容量的缓冲,C个发送语句发送完成前,完全可以不调用接收语句。那第1个接收发生在1+C个发送完成之前如何保证,我们知道,当管道缓冲满了之后,就无法向管道中发送,发送语句会阻塞。因此必须在接收之后发送语句才能继续执行。

第四点规则使得计数信号量可以由缓冲管道建模:管道中的数量对应了当前并发量,管道的容量对应了最大并发量。发送语句占用一个信用量,接收语句释放一个信号量。这是限制并发量的一个惯用手段。

下面的例子限制了最大并发量为3:

var limit = make(chan int, 3)

func main() {
    for _, w := range work {
        go func(w func()) {
            limit <- 1
            w()
            <-limit
        }(w)
    }
    select{}
}

同样的,我们也可以用lock和once来实现happens before语义

不正确的同步方式

即使读r可以观察到同时发生的写w的值,但这并不意味这在r之后发生的读r’可以观察到在w之前发生的写w’。例如:

var a, b int

func f() {
    a = 1
    b = 2
}

func g() {
    print(b)
    print(a)
}

func main() {
    go f()
    g()
}

这段程序可能打印出2、0

这种现象使得一些常见的方式失效。比如双重锁定检查

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func doprint() {
    if !done {
        once.Do(setup)
    }
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

这段程序不能保证print(a)时能够观察到a的值一定是hello, world

同样的,还有一种循环等待的写法也可能有问题,例如:

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {
    }
    print(a)
}

这段程序也不保证print(a)一定能打印出内容,甚至更坏的情况下无法观察到done发生
了改变,因此程序会死循环下去。

还有一种衍生版本的写法也会有问题

type T struct {
    msg string
}

var g *T

func setup() {
    t := new(T)
    t.msg = "hello, world"
    g = t
}

func main() {
    go setup()
    for g == nil {
    }
    print(g.msg)
}

即使main协程观察到了g被赋值,也不一定能观察到g.msg有值。

参考

The Go Memory Model(原文)

用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

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}}