标题:【Swoole系列4.7】协程服务客户端

文章目录
    分类:PHP 标签:Swoole

    协程服务客户端

    协程的学习依然还在继续,要知道,Swoole 现在最核心的就是协程,或者说,整个软件开发语言中,协程都是热门的内容。对于协程的理论以及一些基础的操作我们都已经了解过了,接下来,我们再看看 Swoole 中提供的一些协程客户端功能。在协程之前,异步客户端是 Swoole 的主流应用,但是,现在已经不推荐了,所以我们就直接拿协程来讲这些客户端相关的内容。

    TCP客户端

    TCP 和 UDP 就不分开说了,无非就是换些参数或方法的事,所以我们就以 TCP 为例,来看一下在 Swoole 中如何实现一个协程版的 TCP 客户端。

    php\Swoole\Coroutineine\run(function () {
       $client = new \Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
       if (!$client->connect('127.0.0.1', 9501, 0.5)) {
           echo "connect failed. Error: {$client->errCode}\n";
       }
       $client->send("hello world\n");
    
       var_dump($client->isConnected()); // bool(true)
    
       while (true) {
           $data = $client->recv();
           if (strlen($data) > 0) {
               echo $data;
               $client->send(time() . PHP_EOL);
           } else {
               var_dump($data);
               if ($data === '') {
                   // 全等于空 直接关闭连接
                   $client->close();
                   var_dump($client->isConnected()); // bool(true)
                   break;
               } else {
                   if ($data === false) {
                       // 可以自行根据业务逻辑和错误码进行处理,例如:
                       // 如果超时时则不关闭连接,其他情况直接关闭连接
                       if ($client->errCode !== SOCKET_ETIMEDOUT) {
                           $client->close();
                           break;
                       }
                   } else {
                       $client->close();
                       break;
                   }
               }
           }
           \Co::sleep(1);
       }
    });
    // bool(true)
    // 协程 TCP :hello world
    // 协程 TCP :1640837327
    // string(0) ""
    // bool(false)

    这个例子其实就是官网的例子,然后服务端我们直接使用 【Swoole系列4.1】Swoole协程系统https://mp.weixin.qq.com/s/6VhloFezWEs5bEdMVRGiLA 上的那个 TCP 服务端来进行测试的。


    \Swoole\Coroutine\Client 就是一个协程客户端对象,它的第一个参数就可以指定连接的类型,这里我们就是使用 TCP 连接。然后调用这个对象的 connect() 方法建立连接,连接建立成功后,就可以使用 send() 发送数据,recv() 接收数据。


    isConnected() 方法用于判断连接是否正常,close() 可以关闭连接,errCode 属性返回错误信息。在这里,我们修改了服务端代码,进行两次通信之后就关闭服务端的连接,所以输出的效果就是像上面的。服务端的代码类似下面这样:

    Swoole\Coroutine\run (function () {
        $server = new Swoole\Coroutine\Server('0.0.0.0', 9501, false);
        $server->handle(function(Swoole\Coroutine\Server\Connection $conn){
            $i = 2;
            while($i){
                $data = $conn->recv();
                echo $data, PHP_EOL;
                $conn->send("协程 TCP :" . $data);
                sleep(1);
                $i--;
            }
            $conn->close();
        });
    
        $server->start();
    });

    HTTP客户端

    HTTP 客户端的就更好理解了,你其实就可以把它相像成是一个高效的、协程版的、封装好的 CURL 。

    \Swoole\Coroutine\run(function () {
       go(function(){
           $cli = new Swoole\Coroutine\Http\Client('www.baidu.com', 80);
           $cli->get('/s?wd=php');
           echo $cli->statusCode, '===', $cli->errCode, PHP_EOL;
           preg_match("/<title>(.*)?<\/title>/i", $cli->body, $match);
           var_dump($match);
           var_dump($cli->getHeaders());
           $cli->close();
       });
    //    200===0
    //    array(2) {
    //            [0]=>
    //      string(31) "<title>php_百度搜索</title>"
    //            [1]=>
    //      string(16) "php_百度搜索"
    //    }
    //    array(17) {
    //            ["bdpagetype"]=>
    //      string(1) "3"
    //            ["bdqid"]=>
    //      string(18) "0xdec6e00b000009a8"
    //            ["cache-control"]=>
    //      string(7) "private"
    //            ["ckpacknum"]=>
    //      string(1) "2"
    //      string(9) "b000009a8"
    //            ["ckrndstr"]=>
    //            ["connection"]=>
    //      string(10) "keep-alive"
    //            ["content-encoding"]=>
    //      string(4) "gzip"
    //            ["content-type"]=>
    //      string(23) "text/html;charset=utf-8"
    //            ["date"]=>
    //      string(29) "Fri, 31 Dec 2021 00:57:32 GMT"
    //            ["p3p"]=>
    //      string(34) "CP=" OTI DSP COR IVA OUR IND COM ""
    //            ["server"]=>
    //      string(7) "BWS/1.1"
    //            ["set-cookie"]=>
    //      string(115) "H_PS_PSSID=34444_35105_35628_35489_34584_35491_35695_35234_35644_35318_26350_35620_22159; path=/; domain=.baidu.com"
    //            ["traceid"]=>
    //      string(40) "1640912252031691930616052764259657976232"
    //            ["vary"]=>
    //      string(15) "Accept-Encoding"
    //            ["x-frame-options"]=>
    //      string(10) "sameorigin"
    //            ["x-ua-compatible"]=>
    //      string(16) "IE=Edge,chrome=1"
    //            ["transfer-encoding"]=>
    //      string(7) "chunked"
    //    }
    
       go(function(){
           $cli = new Swoole\Coroutine\Http\Client('127.0.0.1', 9501);
           $cli->setHeaders(['X-Requested-With'=>'xmlhttprequest','Content-type'=>'application/x-www-form-urlencoded']);
           $cli->post('/showname', ['name'=>'Zyblog']);
           echo $cli->statusCode, '===', $cli->errCode, PHP_EOL;
           echo $cli->body, PHP_EOL;
           $cli->close();
       });
    //    200===0
    //    <h1>Hello Zyblog</h1>
    
    });

    第一个协程中,我们直接 GET 请求百度的首页,然后打印了返回页面中的 title 标签里面的内容以及响应的头部信息。


    statusCode 属性打印出来的请求状态码,也就是我们常见的 200 、404 这些。errCode 返回的是连接的错误信息,这个和 TCP 客户端是一样的。


    第二个协程中,我们使用的是 POST 的方式请求一个本地的数据,同样也是使用之前 【Swoole系列4.1】Swoole协程系统https://mp.weixin.qq.com/s/6VhloFezWEs5bEdMVRGiLA 中的协程 HTTP 服务端来测试的,这个也没太多好说的。


    剩下的其实g还有很多方法,在这里就不一一列举了,包括设置请求头,上传下载文件之类的,反正常用的功能基本上都有了。


    HTTP 客户端使用的对象是 Swoole\Coroutine\Http\Client 对象,比上面的 TCP 客户端多了一层命名空间。不过大家也都清楚,本身 HTTP 就是基于 TCP 的封装。

    FastCGI客户端

    FastCGI 是啥?怎么看着这么眼熟?


    还没想起来?传统的 php-fpm 呀!没错,我们可以直接去请求 php-fpm ,让 Swoole 变成一个 Nginx 一样的前端多进程服务器。

    \Swoole\Coroutine\run(function(){
       echo \Swoole\Coroutine\FastCGI\Client::call(
           '127.0.0.1:9000', // FPM监听地址, 也可以是形如 unix:/tmp/php-cgi.sock 的unixsocket地址
           '/home/www/4.Swoole协程/source/4.71fpm.php', // 想要执行的入口文件
           ['name' => 'ZyBlog'] // 附带的POST信息
       );
    });
    // Hello ZyBlog
    
    // 4.71fpm.php
    <?php
    echo 'Hello ' . ($_POST['name'] ?? 'no one');

    好玩不,有意思不?当然,你得在本地把 php-fpm 启动起来。它可以使用 9000 端口这种形式连接,也可以使用 UnixSocket 的方式。使用的对象是 \Swoole\Coroutine\FastCGI\Client 。

    Socket

    最后我们再来看一个今天的重点内容,也就是 Socket 服务。不仅包括客户端,还包括服务端。


    其实之前,在 【Swoole系列4.1】Swoole协程系统https://mp.weixin.qq.com/s/6VhloFezWEs5bEdMVRGiLA 的时候,我们发现了 Swoole 并没有提供现成的 TCP 协程服务端,所以我们就自己实现了一个。当时使用的就是 Socket 相关的组件。


    Socket 是更偏底层的一层协议,上面所有的其实最后都是基于 Socket 来实现的。包括 TCP/UDP 。如果你之前有过 Socket 编程经验的话,这一段就非常简单了。但如果你和我一样,从来没做过 Socket 开发的话,那么咱们还是好好一起看一下吧。具体的原理我就不多说了,毕竟我们还是以框架的学习为主,而且即使要说,我也说不出个所以然来,因此,大家可以自己去查阅理详细资料进行深入的学习。


    首先,我们起一个 Socket 服务端,使用的是 Swoole\Coroutine\Socket 对象,这个对象里面包含的方法有些是客户端的,有些是服务端的,有些是两边都可以使用的。

    Swoole\Coroutine\run(function () {
        $socket = new Swoole\Coroutine\Socket(AF_INET, SOCK_STREAM);
        $socket->bind('0.0.0.0', 9501);
        $socket->listen();
    
        while(1){
            $client = $socket->accept();
            if ($client !== false) {
                go(function () use ($client) {
                    while (1) {
                        $data = $client->recv();
                        if($data == 'exit'){
                            echo "checkLiveness:";
                            var_dump($client->checkLiveness());
                            echo "isClosed:";
                            var_dump($client->isClosed());
                            $client->close();
    
                            echo "断开连接", PHP_EOL;
                            co::sleep(1);
    
                            echo "checkLiveness:";
                            var_dump($client->checkLiveness());
                            echo "isClosed:";
                            var_dump($client->isClosed());
                            break;
                        }else if ($data) {
                            $client->send("收到了客户端:[{$client->fd}] 的数据:" . $data);
                            var_dump($client->getsockname());
                            var_dump($client->getpeername());
                        }
                    }
                });
            }
        }
    });

    和之前我们实现的 UDP 服务端差不多,只是把 SOCK_DGRAM 换成了 SOCK_STREAM 。简单地理解,SOCK_DGRAM 就是 UDP ,SOCK_STREAM 就是 TCP 。后面我们还会讲到这两块的问题,主要是解决 TCP数据包边界 问题。


    实例化 Swoole\Coroutine\Socket 对象之后,我们就 bind() 一个端口,并启动 listen() 进行监听。然后挂起脚本,通过 accept() 对象获得一个客户端的连接,如果有连接来了,就启动一个协程开始处理这个连接的请求。这几个方法都是专门用于服务端的。


    在处理协程中,我们判断了一下发来的请求,如果是一个 exit 字符串,就关闭连接,并且结束这个挂起协程。


    剩下的就是一些方法的展示了,send() 和 recv() 发送与接收数据。checkLiveness() 检查连接是否活跃,isClosed() 检查连接是否关闭。getsockname() 获得当前端的 Socket 信息,getpeername() 获得对端的 Socket 信息。


    接下来就是客户端。

    \Swoole\Coroutine\run(function(){
       $socket = new Swoole\Coroutine\Socket(AF_INET, SOCK_STREAM, 0);
       $socket->connect('127.0.0.1', '9501');
       $socket->send("客户端发来信息啦!");
       $data = $socket->recv();
       echo $data, PHP_EOL;
       var_dump($socket->getsockname());
       var_dump($socket->getpeername());
    
       co::sleep(2);
    
       $socket->send("客户端发来第二条信息啦!");
       $data = $socket->recv();
       echo $data, PHP_EOL;
    
       co::sleep(2);
    
       var_dump($socket->isClosed());
       var_dump($socket->checkLiveness());
    
       $socket->send("exit");
    
       co::sleep(1);
    
       echo $socket->send("客户端发来第三条信息啦!"), PHP_EOL;
       $data = $socket->recv();
       echo $data, PHP_EOL;
    
       var_dump($socket->isClosed());
       var_dump($socket->checkLiveness());
    
    });

    同样地使用 Swoole\Coroutine\Socket 对象,但这里我们只需要使用一个 connect() 来指定要连接的服务端就可以了。然后就马上可以用 send() 和 recv() 进行操作。输出的结果是这样的。

    // [root@localhost source]# php 4.7协程TCP、UDP、HTTP客户端.php
    // 收到了客户端:[6] 的数据:客户端发来信息啦!
    // array(2) {
    //   ["address"]=>
    //   string(9) "127.0.0.1"
    //   ["port"]=>
    //   int(41458)
    // }
    // array(2) {
    //   ["address"]=>
    //   string(9) "127.0.0.1"
    //   ["port"]=>
    //   int(9501)
    // }
    // 收到了客户端:[6] 的数据:客户端发来第二条信息啦!
    // bool(false)
    // bool(true)
    // 36
    // 
    // bool(false)
    // bool(false)

    第一条发送并且获得了响应,第二条也正常打印出来了,但第三条,我们先发送了一个 exit ,然后再发送第三条数据信息。还记得上面我们在服务端如果接收到了 exit 就会关闭连接吧,这时候连接已经被关闭了,这边也不会接收到什么消息内容。


    但是需要注意的是 isClosed() 在客户端是没什么效果的,一直返回的是 false ,而 checkLiveness() 是有效果的。那么 isClosed() 在服务端有效果吗?

    // [root@localhost source]# php 4.1Swoole协程服务.php
    // array(2) {
    //   ["address"]=>
    //   string(9) "127.0.0.1"
    //   ["port"]=>
    //   int(9501)
    // }
    // array(2) {
    //   ["address"]=>
    //   string(9) "127.0.0.1"
    //   ["port"]=>
    //   int(41458)
    // }
    // array(2) {
    //   ["address"]=>
    //   string(9) "127.0.0.1"
    //   ["port"]=>
    //   int(9501)
    // }
    // array(2) {
    //   ["address"]=>
    //   string(9) "127.0.0.1"
    //   ["port"]=>
    //   int(41458)
    // }
    // checkLiveness:bool(true)
    // isClosed:bool(false)
    // 断开连接
    // checkLiveness:bool(false)
    // isClosed:bool(true)

    还好,这两个方法在服务端都是生效的,可以清晰地看到它们的变化。

    总结

    今天的内容很长,但其实就是几段代码比较长而已,实际的内容并不是特别多。除了这些客户端之外,还可以实现 WebSocket、HTTP2 客户端,大家可以试试。另外还有 MySQL 和 Redis 客户端,但现在已经不推荐使用了,为什么呢?后面马上就会讲。


    对于协程最基础的组件我们就介绍的差不多了,最后下一篇,也是我们协程篇的最后一篇文章,我们来简单地说说 一键协程化 的问题。


    测试代码:


    https://github.com/zhangyue0503/swoole/blob/main/4.Swoole%E5%8D%8F%E7%A8%8B/source/4.7%E5%8D%8F%E7%A8%8B%E6%9C%8D%E5%8A%A1%E5%AE%A2%E6%88%B7%E7%AB%AF.php


    参考文档:


    https://wiki.swoole.com/#/coroutine_client/init

    视频链接

    微信文章地址:https://mp.weixin.qq.com/s/Bmuwe2c-VI3UiieEIY3afQ

    B站视频地址:https://www.bilibili.com/video/BV1B84y1q7qN/

    微信视频地址:https://mp.weixin.qq.com/s/H6cjAxkPzkwLFlpz3ha8-A

    搜索
    关注