PHP之异步调用的理解和实践

【推荐阅读】微服务还能火多久?>>>

1.前言

公司项目使用到了异步调用,但是说实际的,我的脑海对异步调用没有形成概念,所以下面,我对这部分内容进行总结和实现一些案例,帮助自己理解和汇总。

代码分享: https://github.com/mtdgclub/asyCall

2.概念

一般来说,我们的调用分为两种,一种是异步调用,一种是同步调用。

  • 同步调用:就是客户端等待调用执行完成并返回结果。
  • 异步调用:实现一个可以无需等待被调用函数的返回值就让操作继续进行的方法。

其实说到异步和同步的概念,就不得不说一下阻塞和非阻塞的概念。这是一些很容易搞混的概念!学过进程的朋友都应该知道,进程间的通信是通过send()和receive()操作完成通信的。而相对于发送方,要么阻塞式发送、要么非阻塞式发送,而对于接收方就是我们上面说过的同步和异步方式接收,所以同步、异步、阻塞、非阻塞是相对于发送方和接收方而言的。

  • 阻塞式发送:发送方进程会一直被阻塞,直到消息被接收方进程收到。
  • 非阻塞式发送:发送方进程调用 send() 后,就可以进行其他操作,而不是一直等待。
  • 同步:接收方调用 receive() 函数后一直阻塞,直到消息到达可用。
  • 异步:接收方调用 receive() 函数后,就可以进行其他操作,后期通过某种手段得到结果。

就好像我们现在去银行办理业务,根据场景可以解释为以下情况:当我们拿到号后,自己是一直在等候区等待(阻塞),还是出去逛街,时不时回来确认一下(非阻塞)。轮到我们办理业务的时候,是我们自己确认轮到自己了(同步),还是银行会通知我们去办理业务(异步)。

3.异步原理

如果我们使用一个异步调用方法的时候,可以理解为,发送完请求后,我们就可以继续去做自己的事情,然后在一个合适的节点去取数据即可。

3.1 异步的方法类型

3.1.1 本地IO操作

本地IO操作时,可以通过DMA功能实现,在调用DMA传输数据的时候,CPU是不需要执行处理的,只需要发起传输和等待传输即可,也就是说,在这段时间里,CPU可以进行其他的事情,这种情况就是异步

PS:DMA,直接内存访问,英文拼写是“Direct Memory Access”。既可指内存和外设直接存取数据这种内存访问的计算机技术,又可指实现该技术的硬件模块。

3.1.2线程

在单线程方式下,计算机是一台严格意义上的冯·诺依曼式机器,一段代码调用另一段代码只能采用同步调用,必须等待这段代码执行完返回结果后,调用方才能继续往下执行,这就是同步调用

多线程的支持可以采用异步调用,调用方和被调方可以属于两个不同的线程,调用方启动被调方线程后,不等对方返回结果就继续执行后续代码。被调方执行完毕后,通过某种手段通知调用方处理结果。

3.2 如何通知接收方

常用的通知手段有回调、互斥对象和消息。这里主要讲解一下回调。

3.2.1回调概念

回调就是在调用异步函数时,我们会在参数中放入一个函数地址,异步函数保存此地址,待有了结果后回调到该函数地址便可以向调用方发出通知,告知异步调用的执行情况

3.2.2异步实战概念

主要是针对第二种情况进行案例实战,下面让我们一起动手通过实践理解吧。

  • 同步调用在发起请求后,只能等待结果,中间不能去干其他的事情。我们称这种模式为请求-响应模式。这个模式优点在于时序清晰,逻辑简单,缺点是在高并发的情况下,大量的CPU时间会阻塞在等待请求的响应上,并且存在只能由客户端向服务端发送请求,而服务端无法主动向客户端发送事件通知,也就是缺乏callback机制。
  • 异步调用建立在双向会话的基础上,让调用方通过注册回调函数来获得请求结果。双向会话式通讯机制通过去掉请求的返回值,所有的方法请求都定义为无返回结果,调用方在发出请求之后就可以继续干其他事情了,而不需要再等待服务返回结果。同时针对服务接口定义一个Callback接口用于服务端向客户端发送请求结果和事件通知,通过回调函数,服务器就可以主动向客户端发送消息,将消息推回给请求方。

3.2.3异步实战场景

假设我们需要发1000封邮件给用户,但由于PHP是单进程执行,发送1000封邮件如果同步处理显然需要等待很长的时间,那么,此时你是等程序运行完、返回结果给你,还是让程序告诉你“发送成功”、让程序在后台慢慢运行呢?

首先我能想到的异步调用的方法有:使用CURL异步执行实现、使用fsockopen()异步执行实现、使用popen()指向进程管道异步执行实现、使用REDIS消息队列+定时任务异步执行实现、使用swoole扩展提供的异步执行实现。

3.2.3.1 情况一:请求-响应模式(同步)

如果程序要等待返回结果,才能继续执行下去,那么这种模式就是典型的请求-响应模式,该模式的特点是,一旦某步执行处于等待中,用户在前端也处于等待状态,这样的用户体验肯定是不友好的。

2.2.3.2情况二:使用CURL异步执行实现

CURL是一个利用URL语法规定来传输文件和数据的工具,PHP支持由Daniel Stenberg创建的libcurl库允许你与各种的服务器使用各种类型的协议进行连接和通讯。libcurl支持http、https、ftp、gopher、telnet、dict、file和ldap协议。libcurl同时也支持HTTPS认证、HTTP POST、HTTP PUT、 FTP 上传、HTTP 基于表单的上传、代理、cookies和用户名+密码的认证。CURL实现异步的原理是因为在PHP中,CURL能够实现Get和Post请求,这些函数在PHP 4.0.2中被引入,我们只要实现在不影响本身文件执行的情况下,对除了本身文件外的PHP文件进行访问,使得服务器创建新的进程/线程这一特点,就能够实现PHP的伪异步。

案例1:详见curlPost.php和curlReceive.php

function asynch($data,$url)
{
    if (is_array($data)) {
        $data = http_build_query($data, null, '&');
    }
    $ch = curl_init();//初始化
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_TIMEOUT, 1);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$result['response'] = curl_exec($ch);

$result['httpCode'] = curl_getinfo($ch, CURLINFO_HTTP_CODE); 

var_dump(curl_error($ch));//显示错误原因
    curl_close($ch);//关闭cURL资源,并且释放系统资源
    return $result;
}

案例2:详见curlPostArray.php和curlReceiveArray.php

function asynch($data,$url)
{
    $ch = curl_init();
    $curl_opt = array(
        CURLOPT_URL=>$url,
        CURLOPT_RETURNTRANSFER=>1,
        CURLOPT_POST=>1,
        CURLOPT_POSTFIELDS=>$data,
        CURLOPT_TIMEOUT=>1
    );
    curl_setopt_array($ch, $curl_opt);

    $result['response'] = curl_exec($ch);

$result['httpCode'] = curl_getinfo($ch, CURLINFO_HTTP_CODE);

var_dump(curl_error($ch));//显示错误原因
    curl_close($ch);//关闭cURL资源,并且释放系统资源
    return $result;
}

以上两则案例都是一样的,只不过案例二将各种设置以批量设置(数组)声明。另外,CURL还有很多种用途,具体可以自行百度关于CURL的资料进行深入学习~

3.2.3.3情况三:使用fsockopen()异步执行实现

fsockopen()用于打开网络的 Socket 链接。Scoket又称为套接字、是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。fsockopen()能够模拟成HTTP连接、或者模拟成post 和 get 传送数据,这恰好符合了我们的需求,访问别的PHP文件,让服务器创建新的进程/线程,用来执行对应的PHP文件,实现PHP的伪异步操作。

场景:假设客户端调用服务器a.php接口,需要执行一个长达10s-20s不等的耗资源操作,但是客户端响应请求时间为5秒(请求响应超时时间),5s以上无回复即断开连接;这时候使用异步多线程调用是个合适的解决方案,让a.php调用b.php后即刻响应客户端请求,b.php自动执行相应操作。

案例1:实现fsockopen()异步调用GET请求

_sock_get.php和get_return.php文件

/**
 * 远程GET请求(不获取内容)函数
 * @param string $host 域名
 * @param string $url 路径
 * @param array $param 传输数据
 * @return array 返回error_code
 */
function _sock_get($host, $url, $param)
{
    $port = parse_url($url, PHP_URL_PORT);//获取端口
    $port = $port ? $port : 80;
    $scheme = parse_url($url, PHP_URL_SCHEME);//获取协议 http https
    $path = parse_url($url, PHP_URL_PATH);//获取域名
    $query = isset($param) ? http_build_query($param) : '';//获取路径

    if ($scheme == 'https') {
        $host = 'ssl://' . $host;
    }

    $fp = fsockopen($host, $port, $error_code, $error_msg, 1);

    if (!$fp) {
        return array('error_code' => $error_code, 'error_msg' => $error_msg);
    } else {
        stream_set_blocking($fp, true);//开启非阻塞模式
        stream_set_timeout($fp, 1);//设置超时
        $header = "GET $path" . "?" . "$query" . " HTTP/1.1\r\n";
        $header .= "Host: $host\r\n";
        $header .= "Connection: close\r\n\r\n";//长连接关闭
        fwrite($fp, $header);
        usleep(2000); // 延时,防止在nginx服务器上无法执行成功
        fclose($fp);
        return array('error_code' => 0);
    }
}

案例2:实现fsockopen()异步调用POST请求

_sock_post.php和post_return.php文件

/**
 * 远程POST请求(不获取内容)函数
 * @param string $host 域名
 * @param string $url 路径
 * @param array $param 传输数据
 * @return array 返回error_code
 */
function _sock_post($host, $url, $param)
{
    $port = parse_url($url, PHP_URL_PORT);//获取端口
    $port = $port ? $port : 80;
    $scheme = parse_url($url, PHP_URL_SCHEME);//获取协议 http https
    $path = parse_url($url, PHP_URL_PATH);//获取域名
    $query = isset($param) ? http_build_query($param) : '';//获取路径

    if ($scheme == 'https') {
        $host = 'ssl://' . $host;
    }

    $fp = fsockopen($host, $port, $error_code, $error_msg, 1);

    if (!$fp) {
        return array('error_code' => $error_code, 'error_msg' => $error_msg);
    } else {
        stream_set_blocking($fp, true);//开启非阻塞模式
        stream_set_timeout($fp, 1);//设置超时
        $header = "POST $path HTTP/1.1\r\n";
        $header .= "Host: $host\r\n";
        $header .= "Content-length:" . strlen(trim($query)) . "\r\n";
        $header .= "Content-type:application/x-www-form-urlencoded\r\n";
        $header .= "Connection: close\r\n\r\n";//长连接关闭
        $header .= "\r\n";
        $header .= $query . "\r\n";
        fwrite($fp, $header);
        usleep(2000); // 延时,防止在nginx服务器上无法执行成功
        fclose($fp);
        return array('error_code' => 0);
    }
}

3.2.3.4情况四:使用popen()指向进程管道异步执行实现

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。

resource popen ( string command, string mode );//打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。所以可以通过调用它,但忽略它的输出。

案例1:代码演示案例

pclose(popen("/b.php &", 'r'));

但通过该方法实现异步调用,不能通过HTTP协议请求另外的一个服务器,也就是说不能跨域请求,只能执行本地的php脚本文件。并且只能单向打开,无法传输大量参数给被调用脚本。当访问量大的时候,会产生大量的进程。所以一般不推荐使用该方法实现PHP的伪多线程/进程。

3.2.3.5情况五:使用REDIS消息队列+定时任务异步执行实现

说到REDIS消息队列+定时任务实现异步操作,我想大家应该都有大概的实现思路,这种情况,以后再另开单章总结~

3.2.3.6情况六:使用swoole扩展提供的异步执行实现

还没有接触到swoole,但是听说过别人说swoole可以实现异步操作,这里挖个坑,等学习过后再补单章~

4.PHP内置回调函数

4.1 call_user_func()函数

把第一个参数作为回调函数调用,具体手册解释:https://www.php.net/call_user_func/

4.1.1 调用普通函数

<?php
function a($b, $c) {
    echo $b;
    echo $c;
}
call_user_func('a', "111", "222");
call_user_func('a', "333", "444");
//显示 111 222 333 444
?>

4.1.2调用类的方法

<?php

class a {
    function b($i) {
        echo $i;
    }
    public static c($k) {
        echo $k;
    }
}

//当php <5.3时,可以如下使用,此时会把 b()方法当作是a的一个静态方式。
call_user_func(array("a", "b"), "111");
//当php >=5.3时,类的公开的非静态的方法必须在类实例化后方可被调用,否则会提示Strict性错误(为了兼容先前及以后的版本,还是用对象方法传入)。
$obj = new a;
call_user_func(array($obj, "b"), "111");//显示 111
//静态方法可以如下方式调用
call_user_func(array("a", "b"), "111");
//或
call_user_func("a::b","111");
?>

4.2 call_user_func_array()函数

调用回调函数,并把一个数组参数作为回调函数的参数,具体手册解释:https://www.php.net/manual/zh/function.call-user-func-array.php

4.2.1 调用普通函数

<?php

function a($b, $c) {
    echo $b;
    echo $c;
}
call_user_func_array('a', array("111", "222"));
//显示 111 222
?>

4.2.2调用类的方法

<?php

Class ClassA
{
    function bc($b, $c) {
        $bc = $b + $c;
        echo $bc;
    }
    function d() {
         $bc = $b + $c;
         echo $bc;
    }
}
//php<5.3时,非静态的方法可直接传入类名
call_user_func_array(array('ClassA', 'bc'), array("111", "222"));
//php>=5.3时,非静态的方法 只有在类被实例化后方可调用,否则会提示Strict性错误
$obj = new classA;
call_user_func_array(array($obj, 'bc'), array("111", "222"));
//静态方法调用如下
call_user_func_array(array('ClassA','bc'), array("111", "222"));
//或
call_user_func_array('ClassA::bc', array("111", "222"));
?>

4.总结

异步与回调的概念,在开发中会经常使用到,因此希望通过对其基础进行整理和总结达到加深印象的目的。

来源: https://my.oschina.net/mtdg/blog/3295983

标签