Pecl_Http 与 unix domain socket 客户端封装

Posted by LB on Tue, Jun 5, 2018

1. 背景描述

Pecl/HTTP是一个PHP扩展,历史非常悠久了,从2005年至2018年不断完善其功能,它主要帮助PHP对于HTTP请求的相关操作。不同于CURL,其具有更丰富的扩展接口,既包括平常的请求,也包括对于HTTP数据的封包或拆包操作。

对于PHP和HTTP,大部分程序员关心的如何完成一个请求。但是更深一步,我们会发现HTTP数据包的文件格式也很重要,比如传统的HTTP请求性能很弱,对于请求密集型业务,传统的HTTP并不能达到很好的RPS,就算是开启了Keep-Alive,性能还是很弱。对于这种情况,我们失望的是HTTP请求,而不是HTTP的数据包格式,所以我们可以在数据封包拆包上继续使用HTTP协议,但是对于数据传输这层,我们可以采用性能更高的一些通路,比如Unix domain socket。

2. 研究难点

HTTP的通信协议数据包格式总体来说并不复杂,但是为了便于系统研发及维护,一般都站在巨人的肩膀上进行研发,目前针对PHP进行数据拆包与封包的库很少。一方面我们可以从网上比较火的一些Http Client库中抽取相关的调用层,另一方面我们可以从pecl中找寻一些扩展库,基于pecl的扩展库都是基于C语言进行的研发,性能更强,而且安装也很方便。

在本文中,我们主要围绕Pecl/HTTP库进行相关阐述,在接下来的文章中,我们也会专门围绕Guzzle或者更合适的PHP库来讲解数据包的装包和拆包。

3. HTTP数据包:封包-拆包

3.1 扩展类设计思路

本文主要讲解HTTP数据包的封包和拆包操作,我们首先封装一个HTTP数据包相关接口,接口主要包括对于Get、Post的封包;对于数据包的解包操作。

1<?php
2interface HttpPack{
3    public function encodeGetPack($url,$header);
4    public function encodePostPack($url,$header,$body);
5    public function decodePack($packStr);
6}
7?>
	随后,我们定义了一个SimpleTun类来实现这个接口,具体函数实现在各节详细描述,此处为框架代码。
 1<?php
 2class SimpleTun implements HttpPack
 3{
 4    public function encodeGetPack($url, $header)
 5    {
 6        // TODO: Implement encodeGetPack() method.
 7
 8    }
 9
10    public function encodePostPack($url, $header, $body)
11    {
12        // TODO: Implement encodePostPack() method.
13    }
14
15    public function decodePack($packStr)
16    {
17        // TODO: Implement decodePack() method.
18    }
19}
20?>

3.2 Pecl/Http组件介绍

Pecl/Http组件主要包括Client、Cookie、Encoding、Env、Exception、Header、Message、Params、QueryString、Url这十个组件,下面将做选择性介绍。 1. Client:此组件主要模拟Http客户端、针对这个组件,可以完成GET、POST等类型的请求构造和请求发送,此组件实现了Request请求接口、Response响应接口,可以完成数据包的构造与拆分。 2. Cookie:此组件主要完成Cookie数据的构建。 3. Encoding:此组件包含stream组件,包含一系列流操作、有分块、压缩、刷新缓冲区。 4. Env:获取当前请求的HTTP环境情况。 5. Exception:异常组件,主要用于抓取pecl/http内部的运行异常。Example 6. Header:主要用于操作、匹配、评判和序列化HTTP头部信息。 7. Message:构建任何请求和响应的信息体,重心在HTTP数据包的封包与拆包。 8. Params:组件解析,解释和组合HTTP(标题)参数。 9. QueryString:组件提供了通用的设施进行检索,使用和操作的查询字符串和表单数据。 10. Url:组件提供了通用手段解析,构造和操作的网址。

3.3 配置文件结构设计

配置文件是加载用户请求的源头,针对配置文件,在设计上进行了“项目”与“服务”的抽象,下面是一个样例配置。

 1<?php
 2//配置优先级:同参数名的配置函数参数 > 服务层 > 项目层
 3return [
 4    'project1'=>[  //项目层
 5        'UDS'=>'/opt/meshUds.sock',
 6        'PATH_COMMON'=>'http://www.looake.com:8081',
 7        'HEADER'=>[
 8            'HOST'=>'www.looake.com:8081',
 9        ],
10        'SERVERS'=>[  //服务层
11            'testpost'=>[
12                'PATH'=>'/Test/post',
13                'HEADER'=>[
14                    'Content-type'=>'application/x-www-form-urlencoded',
15                ],
16            ],
17            'testget'=>[
18                'PATH'=>'/Test/hello',
19                'HEADER'=>[
20
21                ],
22            ],
23            'sleepget'=>[
24                'PATH'=>'/Test/sleep',
25            ]
26        ],
27        'TIMEOUT'=>3,
28        'FUSE_LEVEL'=>0,
29    ]
30];
31?>

3.4 encodeGetPack函数实现

	1. 此函数主要获取Get请求的数据包信息体,由于Get请求的数据体比较简单,主要的参数包括URL(请求路径)和 header(请求头部)。
	2. 函数目标:构造Message信息体,主要围绕项目配置单元、服务名、请求头部、URL附属参数。
	3. 代码实现:
 1<?php
 2    public function encodeGetPack($moudleConfig, $serviceName, $headers = [], $params = [])
 3    {
 4        // TODO: Implement encodeGetPack() method.
 5
 6        if (empty($moudleConfig) || empty($serviceName) || (!isset($moudleConfig['SERVERS'][$serviceName]))) {
 7            return false;
 8        }
 9
10        if (empty($moudleConfig['PATH_COMMON']) || (!isset($moudleConfig['SERVERS'][$serviceName]['PATH'])))
11            return false;
12
13        $url = $moudleConfig['PATH_COMMON'] . $moudleConfig['SERVERS'][$serviceName]['PATH'];
14
15        if (!empty($params)) {
16            $url .= '?';
17            foreach ($params as $key => $value) {
18                $url .= $key . '=' . $value;
19            }
20        }
21        $msg = new http\Message();
22        $msg->setType(http\Message::TYPE_REQUEST);
23        $msg->setRequestMethod('GET');
24
25        $msg->setRequestUrl($url);
26
27        //设置项目header头部
28        if (!empty($moudleConfig['HEADER'])) {
29            $msg->addHeaders($moudleConfig['HEADER']);
30        }
31
32        //设置指定服务的header头部
33        if (!empty($moudleConfig['SERVERS'][$serviceName]['HEADER'])) {
34            $msg->addHeaders($moudleConfig['SERVERS'][$serviceName]['HEADER']);
35        }
36
37        //设置函数附属header头部
38        if (!empty($headers)) {
39            $msg->addHeaders($headers);
40        }
41
42        $getRequestRaw = $msg->serialize();  //对象序列化
43        return $getRequestRaw;
44    }
45?>

3.5 encodePostPack函数实现

	1. 此函数主要获取Post请求的数据包信息体,主要的参数包括URL(请求路径)、 header(请求头部)、body(请求体)。
	2. 函数目标:构造Message信息体,主要围绕项目配置单元、服务名、请求头部、请求体。
	3. 代码实现:
 1<?php
 2    public function encodePostPack($moudleConfig, $serviceName, $headers = [], $body = [])
 3    {
 4        // TODO: Implement encodePostPack() method.
 5        if (empty($moudleConfig) || empty($serviceName) || (!isset($moudleConfig['SERVERS'][$serviceName]))) {
 6            return false;
 7        }
 8
 9        if (empty($moudleConfig['PATH_COMMON']) || (!isset($moudleConfig['SERVERS'][$serviceName]['PATH'])))
10            return false;
11
12        $url = $moudleConfig['PATH_COMMON'] . $moudleConfig['SERVERS'][$serviceName]['PATH'];
13
14        $msg = new http\Message();
15        $msg->setType(http\Message::TYPE_REQUEST);
16        $msg->setRequestMethod('POST');
17        $msg->setRequestUrl($url);
18
19        //设置项目header头部
20        if (!empty($moudleConfig['HEADER'])) {
21            $msg->addHeaders($moudleConfig['HEADER']);
22        }
23
24        //设置指定服务的header头部
25        if (!empty($moudleConfig['SERVERS'][$serviceName]['HEADER'])) {
26            $msg->addHeaders($moudleConfig['SERVERS'][$serviceName]['HEADER']);
27        }
28
29        //设置函数附属header头部
30        if (!empty($headers)) {
31            $msg->addHeaders($headers);
32        }
33
34        //array to x-www-form-urlencoded
35        if (!empty($body)) {
36            if (empty($msg->getHeader('content-type'))) {
37                $msg->addHeaders(['Content-type' => 'application/x-www-form-urlencoded']);
38            }
39            $msgbody = new http\Message\Body();
40            //$msgbody->addForm($body);
41            $msgbody->append(new http\QueryString($body));
42            $msg->addBody($msgbody);
43        }
44
45        $getRequestRaw = $msg->serialize();  //对象序列化
46        return $getRequestRaw;
47    }
48?>

3.6 decodePack函数实现

	1. 此函数主要用于对HTTP数据包进行解包操作,针对HTTP报文字符串。
	2. 函数目标:构造信息解析数组,主要包括响应码、请求头部、请求体。
	3. 代码实现:
 1<?php
 2    public function decodePack($packStr)
 3    {
 4        // TODO: Implement decodePack() method.
 5        $msgParser = new http\Message\Parser();
 6        $httpInfo = '';
 7        $msgParser->parse($packStr, $msgParser::CLEANUP, $httpInfo);
 8        $retInfo['responseCode'] = $httpInfo->getResponseCode();
 9        $retInfo['responseStatus'] = $httpInfo->getResponseStatus();
10        $retInfo['header'] = $httpInfo->getHeaders();
11        $retInfo['body'] = $httpInfo->getBody()->toString();
12        return $retInfo;
13    }
14?>

4. Unix domain Socket发包封装

4.1 sendDataUds函数实现

	1. 此函数主要用于通过Unix domain socket发送数据,并引入Socket超时。
	2. 函数目标:发送数据、成功执行返回socket对方返回数据、失败返回false。
	3. 代码实现:
 1<?php
 2public function sendDataUds($udsUrl, $data, $timeout = 3)
 3    {
 4        $socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
 5        if ($socket == false)
 6            return false;
 7        $setTimeOut = socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $timeout, 'usec' => 0]);
 8        if ($setTimeOut == false) {
 9            return false;
10        }
11        $socket_conn = socket_connect($socket, $udsUrl);
12        if ($socket_conn == false)
13            return false;
14        $socket_send = socket_send($socket, $data, strlen($data), 0);
15        if ($socket_send == false)
16            return false;
17        $buf = socket_read($socket, self::BUFSIZE, PHP_BINARY_READ);
18        $recvData = $buf;
19        while (strlen($buf) >= self::BUFSIZE) { //缓冲区循环读取
20            $buf = socket_read($socket, self::BUFSIZE, PHP_BINARY_READ);
21            $recvData .= $buf;
22        }
23        return $recvData;
24    }
25?>

5. 测试样例代码

 1<?php
 2require "../SimpleTun-NoComposer/src/SimpleTun.php";
 3
 4$config = include "../SimpleTun-NoComposer/config/BaseConfig.php";
 5$project1Config = $config['project1'];
 6
 7$simpleTun = new SimpleTun();
 8$testPostHttp = $simpleTun->encodePostPack($project1Config, 'testpost', [], ['data' => 123]);
 9
10$sleepGetHttp = $simpleTun->encodeGetPack($project1Config, 'sleepget', [], ['sleep' => 3000]);
11
12$recvData = $simpleTun->sendDataUds('/usr/local/program/mesh_sock/meshUds.sock', $sleepGetHttp, 2);
13
14//如果数据读取成功
15if ($recvData != false) {
16    $data = $simpleTun->decodePack($recvData);
17    var_dump($data);
18} else {
19    print "send Info faild.\n";
20}
21?>