代码封装:Pecl_Http 与 UDS 客户端封装

@李彪  June 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的封包;对于数据包的解包操作。

<?php
interface HttpPack{
    public function encodeGetPack($url,$header);
    public function encodePostPack($url,$header,$body);
    public function decodePack($packStr);
}
?>
    随后,我们定义了一个SimpleTun类来实现这个接口,具体函数实现在各节详细描述,此处为框架代码。
<?php
class SimpleTun implements HttpPack
{
    public function encodeGetPack($url, $header)
    {
        // TODO: Implement encodeGetPack() method.

    }

    public function encodePostPack($url, $header, $body)
    {
        // TODO: Implement encodePostPack() method.
    }

    public function decodePack($packStr)
    {
        // TODO: Implement decodePack() method.
    }
}
?>

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](https://mdref.m6w6.name/http/Exception "Example")
    6. Header:主要用于操作、匹配、评判和序列化HTTP头部信息。
    7. Message:构建任何请求和响应的信息体,重心在HTTP数据包的封包与拆包。
    8. Params:组件解析,解释和组合HTTP(标题)参数。
    9. QueryString:组件提供了通用的设施进行检索,使用和操作的查询字符串和表单数据。
    10. Url:组件提供了通用手段解析,构造和操作的网址。

3.3 配置文件结构设计

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

<?php
//配置优先级:同参数名的配置函数参数 > 服务层 > 项目层
return [
    'project1'=>[  //项目层
        'UDS'=>'/opt/meshUds.sock',
        'PATH_COMMON'=>'http://www.looake.com:8081',
        'HEADER'=>[
            'HOST'=>'www.looake.com:8081',
        ],
        'SERVERS'=>[  //服务层
            'testpost'=>[
                'PATH'=>'/Test/post',
                'HEADER'=>[
                    'Content-type'=>'application/x-www-form-urlencoded',
                ],
            ],
            'testget'=>[
                'PATH'=>'/Test/hello',
                'HEADER'=>[

                ],
            ],
            'sleepget'=>[
                'PATH'=>'/Test/sleep',
            ]
        ],
        'TIMEOUT'=>3,
        'FUSE_LEVEL'=>0,
    ]
];
?>

3.4 encodeGetPack函数实现

    1. 此函数主要获取Get请求的数据包信息体,由于Get请求的数据体比较简单,主要的参数包括URL(请求路径)和 header(请求头部)。
    2. 函数目标:构造Message信息体,主要围绕项目配置单元、服务名、请求头部、URL附属参数。
    3. 代码实现:
<?php
    public function encodeGetPack($moudleConfig, $serviceName, $headers = [], $params = [])
    {
        // TODO: Implement encodeGetPack() method.

        if (empty($moudleConfig) || empty($serviceName) || (!isset($moudleConfig['SERVERS'][$serviceName]))) {
            return false;
        }

        if (empty($moudleConfig['PATH_COMMON']) || (!isset($moudleConfig['SERVERS'][$serviceName]['PATH'])))
            return false;

        $url = $moudleConfig['PATH_COMMON'] . $moudleConfig['SERVERS'][$serviceName]['PATH'];

        if (!empty($params)) {
            $url .= '?';
            foreach ($params as $key => $value) {
                $url .= $key . '=' . $value;
            }
        }
        $msg = new http\Message();
        $msg->setType(http\Message::TYPE_REQUEST);
        $msg->setRequestMethod('GET');

        $msg->setRequestUrl($url);

        //设置项目header头部
        if (!empty($moudleConfig['HEADER'])) {
            $msg->addHeaders($moudleConfig['HEADER']);
        }

        //设置指定服务的header头部
        if (!empty($moudleConfig['SERVERS'][$serviceName]['HEADER'])) {
            $msg->addHeaders($moudleConfig['SERVERS'][$serviceName]['HEADER']);
        }

        //设置函数附属header头部
        if (!empty($headers)) {
            $msg->addHeaders($headers);
        }

        $getRequestRaw = $msg->serialize();  //对象序列化
        return $getRequestRaw;
    }
?>

3.5 encodePostPack函数实现

    1. 此函数主要获取Post请求的数据包信息体,主要的参数包括URL(请求路径)、 header(请求头部)、body(请求体)。
    2. 函数目标:构造Message信息体,主要围绕项目配置单元、服务名、请求头部、请求体。
    3. 代码实现:
<?php
    public function encodePostPack($moudleConfig, $serviceName, $headers = [], $body = [])
    {
        // TODO: Implement encodePostPack() method.
        if (empty($moudleConfig) || empty($serviceName) || (!isset($moudleConfig['SERVERS'][$serviceName]))) {
            return false;
        }

        if (empty($moudleConfig['PATH_COMMON']) || (!isset($moudleConfig['SERVERS'][$serviceName]['PATH'])))
            return false;

        $url = $moudleConfig['PATH_COMMON'] . $moudleConfig['SERVERS'][$serviceName]['PATH'];

        $msg = new http\Message();
        $msg->setType(http\Message::TYPE_REQUEST);
        $msg->setRequestMethod('POST');
        $msg->setRequestUrl($url);

        //设置项目header头部
        if (!empty($moudleConfig['HEADER'])) {
            $msg->addHeaders($moudleConfig['HEADER']);
        }

        //设置指定服务的header头部
        if (!empty($moudleConfig['SERVERS'][$serviceName]['HEADER'])) {
            $msg->addHeaders($moudleConfig['SERVERS'][$serviceName]['HEADER']);
        }

        //设置函数附属header头部
        if (!empty($headers)) {
            $msg->addHeaders($headers);
        }

        //array to x-www-form-urlencoded
        if (!empty($body)) {
            if (empty($msg->getHeader('content-type'))) {
                $msg->addHeaders(['Content-type' => 'application/x-www-form-urlencoded']);
            }
            $msgbody = new http\Message\Body();
            //$msgbody->addForm($body);
            $msgbody->append(new http\QueryString($body));
            $msg->addBody($msgbody);
        }

        $getRequestRaw = $msg->serialize();  //对象序列化
        return $getRequestRaw;
    }
?>

3.6 decodePack函数实现

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

4. Unix domain Socket发包封装

4.1 sendDataUds函数实现

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

5. 测试样例代码

<?php
require "../SimpleTun-NoComposer/src/SimpleTun.php";

$config = include "../SimpleTun-NoComposer/config/BaseConfig.php";
$project1Config = $config['project1'];

$simpleTun = new SimpleTun();
$testPostHttp = $simpleTun->encodePostPack($project1Config, 'testpost', [], ['data' => 123]);

$sleepGetHttp = $simpleTun->encodeGetPack($project1Config, 'sleepget', [], ['sleep' => 3000]);

$recvData = $simpleTun->sendDataUds('/usr/local/program/mesh_sock/meshUds.sock', $sleepGetHttp, 2);

//如果数据读取成功
if ($recvData != false) {
    $data = $simpleTun->decodePack($recvData);
    var_dump($data);
} else {
    print "send Info faild.\n";
}
?>

评论已关闭