PHP 实现混合请求的并发

在接口测试中我们不仅要做单接口层面的正向测试和异常测试,常常还需要对一些接口做并发请求测试,比如相同信息并发创建订单或者并发支付,并发查询同一个优惠券模板 id,并发更新同一个用户等等。为了方便起见,我就用 PHP 的 curl 封装了并发的请求方法。

POST 请求的并发

/**
 * POST 请求的并发
 * @param $requestBodyArr , 请求的 json 二维数组
 * @param $category , 比如 transfers,charges, v1 后面的 url
 * @return array
 * @throws \Exception
 */
public static function apiMultiCreate($category, $requestBodyArr)
{
    $handles = $data = $headers = array();
    $threadCount = count($requestBodyArr);

    //create the multiple cURL handle
    $mh = curl_multi_init();
    
    //一个用来判断操作是否仍在执行的标识的引用。
    $active = null;

    for ($i = 0; $i < $threadCount; $i++) {
        $handles[$i] = curl_init();
        curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, 1);//设为TRUE把curl_exec()结果转化为字串,而不是直接输出
        curl_setopt($handles[$i], CURLOPT_POST, 1);//post提交方式
        if (is_array($category)) {
            $url = '/v1/' . $category[$i];
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        } else {
            $url = '/v1/' . $category;
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//Pingpp::$apiBaseUrl 表示 https://host
        }
        if (!is_array($requestBodyArr)) {
            $data[$i] = $requestBodyArr;//$arr是一维数组
        } else {
            $data[$i] = $requestBodyArr[$i];//$arr是二维数组
        }

        $request_TimeStamp = time();

        $headers[$i] = array('Authorization: Bearer ' . Pingpp::$apiKey,
            'Content-type: application/json;charset=UTF-8',
            'Pingplusplus-Request-Timestamp:' . $request_TimeStamp,
            'Pingplusplus-Signature: ' . Util::genSignatureForAPI(json_encode($data[$i]), $url, $request_TimeStamp)
        );
        curl_setopt($handles[$i], CURLOPT_HTTPHEADER, array_filter($headers[$i]));

        curl_setopt($handles[$i], CURLOPT_POSTFIELDS, json_encode($data[$i]));

        //向curl批处理会话中添加单独的curl句柄
        curl_multi_add_handle($mh, $handles[$i]);
    }

    //execute the handles
    //curl_multi_exec — 运行当前 cURL 句柄的子连接
    do {
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);

    while ($active && $mrc == CURLM_OK) {
        if (curl_multi_select($mh) != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
    }
    $responseArr = array();

    /**
     * curl_multi_getcontent-如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
     * curl_multi_remove_handle-移除curl批处理句柄资源中的某个句柄资源
     */
    for ($i = 0; $i < $threadCount; $i++) {
        $info = curl_getinfo($handles[$i]);
        print_r("Took " . $info['total_time'] . " seconds to send a request to " . urldecode($info['url']) . " and http status code is " . $info['http_code'] . "\n");
        print_r('Thread#' . $i . " content is \n" . curl_multi_getcontent($handles[$i]) . "\n");
        curl_multi_remove_handle($mh, $handles[$i]);
        $responseArr[] = curl_multi_getcontent($handles[$i]) . "\n";//值为 string 类型的 数组
        curl_close($handles[$i]);
    }
    //关闭一组cURL句柄
    curl_multi_close($mh);
    return $responseArr;
}

代码中 Util::genSignatureForAPI 是用来签名的,你可以选择忽略。

当需要测试相同内容并发创建订单时就可以像如下方式操作:

$threads = 2;
$data = array();
for ($i = 0; $i < $threads; $i++) {
    $data[$i] = array(
        "app" => $this->appId,
        "uid" => 'user007', //email、手机号、UID 唯一标识(不区分大小写)
        "merchant_order_no" => Util::genString(20),//商户订单号
        "amount" => 10,
        "currency" => 'cny,
        "client_ip" => '127.0.0.1',
        "subject" => '并发创建', //商品的标题
        "body" => $this->body, //商品的描述信息
    );
}
HttpRequest::apiMultiCreate("orders", $data);

PUT 请求的并发

/**
 * PUT 请求的并发
 * @param $requestBodyArr , 请求的 json 二维数组
 * @param $category , 比如transfers,charges
 * @return array
 * @throws \Exception
 */
public static function apiMultiPut($category, $requestBodyArr)
{
    $handles = $data = $headers = array();

    $threadCount = count($requestBodyArr);

    //create the multiple cURL handle
    $mh = curl_multi_init();

    //一个用来判断操作是否仍在执行的标识的引用。
    $active = null;
    for ($i = 0; $i < $threadCount; $i++) {
        $handles[$i] = curl_init();
        curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, 1);//设为TRUE把curl_exec()结果转化为字串,而不是直接输出
        curl_setopt($handles[$i], CURLOPT_CUSTOMREQUEST, 'PUT');//put提交方式
        if (is_array($category)) {
            $url = '/v1/' . $category[$i];
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        } else {
            $url = '/v1/' . $category;
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        }

        if (!is_array($requestBodyArr)){
            $data[$i] = $requestBodyArr;//$arr是一维数组
        } else {
            $data[$i] = $requestBodyArr[$i];//$arr是二维数组
        }
        $request_TimeStamp = time();

        $headers[$i] = array('Authorization: Bearer ' . Pingpp::$apiKey,
            'Content-type: application/json;charset=UTF-8',
            'Pingplusplus-Request-Timestamp:' . $request_TimeStamp,
            'Pingplusplus-Signature: ' . Util::genSignatureForAPI(json_encode($data[$i]), $url, $request_TimeStamp)
        );
        curl_setopt($handles[$i], CURLOPT_HTTPHEADER, array_filter($headers[$i]));
        curl_setopt($handles[$i], CURLOPT_POSTFIELDS, json_encode($data[$i]));

        //向curl批处理会话中添加单独的curl句柄
        curl_multi_add_handle($mh, $handles[$i]);
    }

    //execute the handles
    //curl_multi_exec — 运行当前 cURL 句柄的子连接
    do {
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);

    while ($active && $mrc == CURLM_OK) {
        if (curl_multi_select($mh) != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
    }
    $responseArr = array();

    /**
     * curl_multi_getcontent-如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
     * curl_multi_remove_handle-移除curl批处理句柄资源中的某个句柄资源
     */
    for ($i = 0; $i < $threadCount; $i++) {
        $info = curl_getinfo($handles[$i]);
        print_r("Took " . $info['total_time'] . " seconds to send a request to " . urldecode($info['url']) . " and http status code is " . $info['http_code'] . "\n");
        print_r('Thread#' . $i . " content is \n" . curl_multi_getcontent($handles[$i]) . "\n");
        curl_multi_remove_handle($mh, $handles[$i]);
        $responseArr[] = curl_multi_getcontent($handles[$i]) . "\n";//值为 string 类型的 数组
        curl_close($handles[$i]);
    }
    //关闭一组cURL句柄
    curl_multi_close($mh);
    return $responseArr;
}

比如目前我们有这样一个场景需要测试,一个新创建的用户 id,要么更新它要么禁用它,这样一个并发操作你就可以像下面这样组建并发操作:

$user_id = "user1568020989";
$data = array(
    array(
        "address" => $this->address . "update", //商户订单号
    ),
    array(
        "disabled" => true //是否禁用。使用该参数时,不能同时使用其他参数。
    )
);
HttpRequest::apiMultiPut("apps/" . $this->appId . "/users/" . $user_id, $data);

GET 请求的并发

/**
 * 通过 id 查询
 * @param $category , 比如 transfers,charges
 * @param $idArr
 * @return array
 * @throws \Exception
 */
public static function apiMultiGet($category, $idArr)
{
    $handles = $headers = array();

    //create the multiple cURL handle
    $mh = curl_multi_init();

    //一个用来判断操作是否仍在执行的标识的引用。
    $active = null;
    $threadCount = count($idArr);

    for ($i = 0; $i < $threadCount; $i++) {
        $handles[$i] = curl_init();
        curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, 1);//设为TRUE把curl_exec()结果转化为字串,而不是直接输出
        if (is_array($category)) {
            $url = '/v1/' . $category[$i] . '/' . $idArr[$i];
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        } else {
            $url = '/v1/' . $category . '/' . $idArr[$i];
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        }
        $request_TimeStamp = time();
        $headers[$i] = array('Authorization: Bearer ' . Pingpp::$apiKey,
            'Content-type: application/json;charset=UTF-8',
            'Pingplusplus-Request-Timestamp:' . $request_TimeStamp,
            'Pingplusplus-Signature: ' . Util::genSignatureForAPI(null, $url, $request_TimeStamp)
        );
        curl_setopt($handles[$i], CURLOPT_HTTPHEADER, array_filter($headers[$i]));

        //向curl批处理会话中添加单独的curl句柄
        curl_multi_add_handle($mh, $handles[$i]);
    }
    //execute the handles
    //curl_multi_exec — 运行当前 cURL 句柄的子连接
    do {
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);

    while ($active && $mrc == CURLM_OK) {
        if (curl_multi_select($mh) != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
    }
    $responseArr = array();
    /**
     * curl_multi_getcontent-如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
     * curl_multi_remove_handle-移除curl批处理句柄资源中的某个句柄资源
     */
    for ($i = 0; $i < $threadCount; $i++) {
        $info = curl_getinfo($handles[$i]);
        print_r("Took " . $info['total_time'] . " seconds to send a request to " . urldecode($info['url']) . " and http status code is " . $info['http_code'] . "\n");
        print_r("Took " . $info['namelookup_time'] . " seconds -- (namelookup_time)从开始到域名解析完毕的时间\n");
        print_r("Took " . $info['connect_time'] . " seconds -- (connect_time)从开始直到对远程主机(或代理)的连接完毕的时间\n");
        print_r("Took " . $info['pretransfer_time'] . " seconds -- (pretransfer_time)从开始直到文件刚刚开始传输的时间\n");
        print_r("Took " . $info['starttransfer_time'] . " seconds -- (starttransfer_time)从开始到第一个字节被curl收到的时间\n");
        print_r('Thread#' . $i . " content is \n" . curl_multi_getcontent($handles[$i]) . "\n");
        curl_multi_remove_handle($mh, $handles[$i]);
        $responseArr[] = curl_multi_getcontent($handles[$i]) . "\n";//值为 string 类型的 数组
        curl_close($handles[$i]);
    }
    //关闭一组cURL句柄
    curl_multi_close($mh);
    return $responseArr;
}

有时候你可能会需要并发查询一个未支付的订单 id,粗略的看一下其性能怎么样,比如我们的 order id,当你查询它的时候,它会做很多请求,曾经的一个性能槽点就是这样被发现的(并发请求后查看日志找出耗时最多的请求,发现可优化点),有时也可能是不同的 id 并发查询,都可以按照下面的方式组建你的并发 id 查询脚本:

$transferArr = array(
    'tr_nLyrrHvjTO0880y58Oub5OSS',
    'tr_C0Kyf15CS8u5OiT8y9LS4i50'
);
HttpRequest::apiMultiGet( "transfers", $transferArr);

可能看到这里你会觉得为什么要自己写并发请求的方法呢?用性能测试工具测试不是更好吗?当然你想的很对,可是大多数时候性能测试只是针对特定的场景和需求,而不是每一个接口都需要去做,更要知道一点测试的时间通常是很紧张的,很多时候粗略的了解一下接口的性能会使测试经济比更高。

混合请求的并发

当然一定会有同仁在看 PUT 接口并发的时候就想到了要混合请求并发的场景。的确,这种场景虽然不多,但必不可少。 最近我们就新开发了一个需求,一个订单在创建之后有如下三种操作,且只能有一个成功:

  1. 调用 pay 接口完成支付
  2. 调用取消接口把订单取消
  3. 调用更新接口,更新订单的描述信息、金额等

其中 1 是 POST 请求,2 和 3 是 PUT 请求,要求这三个请求只能成功一个,显然上面单个请求方法的并发是满足不了这样的测试场景的。于是就想到了混合并发,(之前用 LoadRunner 做性能测试的时候做过混合场景的测试,有感于此) ,就是把 POST/PUT/GET/DELETE 请求混合在一起做并发测试。 混合并发请求的方法如下:

/**
 * 对外 API 接口并发测试, curl_multi 会消耗很多的系统资源,在并发请求时并发数有一定阈值,一般为 512,是由于CURL内部限制,超过最大并发会导致失败。你可以自己在自己的机器上做一下测试,来制定你的阈值。
 * 当做 post 或 put 操作时,需 $requestBodyArr 是二维数组,如 ("POST", "charges",$requestBodyArr)
 * 当做 post/put/get/delete 混合操作时,需 $methods, $urls, $requestBodyArr 三者都是数组,且内容要逐一对应
 * 当 get 或者 delete 不同的 id 时,只需将不同的 id 组成 url 数据即可,如 ("GET", $urls 数组)
 * 当 get 或者 delete 相同的 id 时,$requestBodyArr 为 null,设置 $threadCount 为并发数即可,如 ("GET", "charges/CHARGE_ID",null,100)
 * @param $urls , 比如transfers,charges
 * @param string $methods , 比如 post,put,get,delete
 * @param null $requestBodyArr , 请求的 json 二维数组,Get/Delete 请求时也可以是 null
 * @param null $threadCount
 * @return array
 * @throws \Exception
 */
public static function apiMultiRequests($urls, $methods="GET", $requestBodyArr = null, $threadCount = null)
{
    $handles = $data = $headers = array();

    //create the multiple cURL handle
    $mh = curl_multi_init();

    if ($threadCount !== null && is_array($requestBodyArr)) {
        assert($threadCount == count($requestBodyArr), "并发数和请求 body 的数组长度要一致!");
    } elseif ($threadCount === null && is_array($requestBodyArr)) {//不同的请求 body 组成的数组,比如不同的创建 charge 的请求 body
        $threadCount = count($requestBodyArr);
    } elseif ($threadCount === null && is_array($urls)) {//不同的 url 组成的数组,比如不同的 id 查询,直接组成 url 的数组即可
        $threadCount = count($urls);
    }

    for ($i = 0; $i < $threadCount; $i++) {
        $handles[$i] = curl_init();
        curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, 1);//设为TRUE把curl_exec()结果转化为字串,而不是直接输出
        //为了防止慢请求影响整个服务,可以设置CURLOPT_TIMEOUT来控制超时时间,防止部分假死的请求无限阻塞进程处理,最后打死机器服务。
        curl_setopt($handles[$i], CURLOPT_TIMEOUT, 60); //允许 cURL 函数执行的最长秒数,设置为 60 s
        if (is_array($methods)) {
            curl_setopt($handles[$i], CURLOPT_CUSTOMREQUEST, strtoupper($methods[$i]));//提交方式
        } else {
            curl_setopt($handles[$i], CURLOPT_CUSTOMREQUEST, strtoupper($methods));//提交方式
        }
        if (is_array($urls)) {
            $url = '/v1/' . $urls[$i];
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        } else {
            $url = '/v1/' . $urls;
            curl_setopt($handles[$i], CURLOPT_URL, Pingpp::$apiBaseUrl . $url);//设置请求的URL
        }

        is_array($requestBodyArr) ? $data[$i] = $requestBodyArr[$i] : $data[$i] = $requestBodyArr;

        is_array($methods) ? $method = strtolower($methods[$i]) : $method = strtolower($methods);

        $request_TimeStamp = time();
        $signature = null;
        if ($method === 'post' || $method === 'put') {
            $signature = Util::genSignatureForAPI(json_encode($data[$i]), $url, $request_TimeStamp);
        } else {
            if (null != $requestBodyArr && is_array($requestBodyArr)) {
                $signature = Util::genSignatureForAPI(null, $url . http_build_query($data[$i]), $request_TimeStamp);
            } elseif (null != $requestBodyArr && !is_array($requestBodyArr)) {
                //$requestBodyArr,只是一个字符串
                $signature = Util::genSignatureForAPI(null, $url . $requestBodyArr, $request_TimeStamp);
            } elseif (null == $requestBodyArr) {
                $signature = Util::genSignatureForAPI(null, $url, $request_TimeStamp);
            }
        }

        $headers[$i] = array('Authorization: Bearer ' . Pingpp::$apiKey,
            'Content-type: application/json;charset=UTF-8',
            'Pingplusplus-Request-Timestamp:' . $request_TimeStamp,
            'Pingplusplus-Signature: ' . $signature,
        );
        curl_setopt($handles[$i], CURLOPT_HTTPHEADER, array_filter($headers[$i]));

        if ($method === 'post' || $method === 'put') {
            curl_setopt($handles[$i], CURLOPT_POSTFIELDS, json_encode($data[$i]));
        }

        //向 curl 批处理会话中添加单独的 curl 句柄
        curl_multi_add_handle($mh, $handles[$i]);
    }

    //一个用来判断操作是否仍在执行的标识的引用。
    $active = null;

    //curl_multi_exec — 运行当前 cURL 句柄的子连接
    //检测操作的初始状态是否 OK,CURLM_CALL_MULTI_PERFORM 为常量值-1
    do {
        // 返回的 $active 是活跃连接的数量,$mrc 是返回值,正常为 0,异常为 -1
        $mrc = curl_multi_exec($mh, $active);
    } while ($mrc == CURLM_CALL_MULTI_PERFORM);

    // 如果还有活动的请求,并且操作状态 OK,CURLM_OK 为常量值 0
    while ($active && $mrc == CURLM_OK) {
        // 持续查询状态并不利于处理任务,每 5ms 检查一次,此时释放 CPU,降低机器负载
        usleep(10000);
        if (curl_multi_select($mh) != -1) {
            do {
                $mrc = curl_multi_exec($mh, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        }
    }
    $responseArr = array();

    // 获取返回结果
    foreach ($handles as $index =>$ch) {
        $info = curl_getinfo($ch);
        print_r("Took " . $info['total_time'] . " seconds to send a request to " . urldecode($info['url']) . " and http status code is " . $info['http_code'] . "\n");
        print_r('Thread#' . $index . " content is \n" . curl_multi_getcontent($ch) . "\n");//curl_multi_getcontent-如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
        $responseArr[$index] = curl_multi_getcontent($ch) . "\n";//值为 string 类型的 数组
        curl_multi_remove_handle($mh, $ch);//移除curl批处理句柄资源中的某个句柄资源
        curl_close($ch);
    }
    //关闭一组cURL句柄
    curl_multi_close($mh);
    return $responseArr;
}

针对我们的新的需求,我组建的测试并发脚本如下,这里需要注意代码中的注释,重复的内容也一定不能省略,要做到数据的一一对应:

$orderId = '2012003060000123456';

$urls = array(
    "orders/" . $orderId . "/pay",
    "orders/" . $orderId,
    "orders/" . $orderId,//这个不可以省略,要和 $data 中的数据一一对应
);
$methods = array(
    "POST",
    "PUT",
    "PUT",//这个不可以省略,要和 $data 中的数据一一对应
);
$data = array(
    array(
        "charge_amount" => 10,//required, integer[0, 1000000000], 渠道支付金额
        "channel" => 'alipay'
    ),
    array(
        "amount" => 1
    ),
    array(
        "status" => "canceled"
    )
);
HttpRequest::apiMultiRequests($urls, $methods, $data);

后来发现,使用混合并发请求的方法还可以很好的实现列表查询的并发,脚本组建方式如下:

$path = "apps/" . Pingpp::$appId . "/users?";
$data = array(
    $path . http_build_query(array(
        "page" => 1,
        "per_page" => 2,
        "disabled" => false
    )),
    $path . http_build_query(array(
        "page" => 2,
        "per_page" => 2,
        "disabled" => true
    ))
);

HttpRequest::apiMultiRequests($data, 'GET');

至于其中的 curl_multi_* 几个函数的解释,我这里就偷个懒,请移步参考文章PHP实现并发请求,讲解的还是很清晰的。

当然,有了混合请求的并发方法之后,之前单个方法的并发请求也就不需要了,完全可以替代的!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值