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