您当前的位置: 首页 >  php

蔚1

暂无认证

  • 0浏览

    0关注

    4753博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

PHP 实现混合请求的并发

蔚1 发布时间:2020-03-19 23:31:13 ,浏览量:0

在接口测试中我们不仅要做单接口层面的正向测试和异常测试,常常还需要对一些接口做并发请求测试,比如相同信息并发创建订单,并发查询同一个优惠券模板 ID,并发更新同一个用户等等。但是或许你还会遇到混合并发测试的请求,比如一个订单要么支付(POST)、要么取消(PUT),这该如何实现呢?

本 Chat 就带你用 PHP 来实现如下操作:

  1. 单请求方法的并发
  2. 混合请求方法的并发

适合人群: 对并发请求感兴趣的技术人员

在接口测试中我们不仅要做单接口层面的正向测试和异常测试,常常还需要对一些接口做并发请求测试,比如相同信息并发创建订单或者并发支付,并发查询同一个优惠券模板 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 实现并发请求,讲解的还是很清晰的。

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

阅读全文: http://gitbook.cn/gitchat/activity/5e7319f2d401d5583eb7ceee

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

关注
打赏
1560489824
查看更多评论
立即登录/注册

微信扫码登录

0.0975s