swoole_event_add实现异步

swoole提供了swoole_event_add函数,可以实现异步。此函数可以用在Server或Client模式下。

swoole_event_add属于AsyncIO,必须运行在CLI 模式。

异步tcp客户端

stream_socket_client实现tcp同步客户端
示例:

<?php

$start_time = microtime(TRUE);

$fp = stream_socket_client("tcp://www.52fhy.com:80", $errno, $errstr, 30);
fwrite($fp,"GET /test.json HTTP/1.1\r\nHost: www.52fhy.com\r\n\r\n");

echo $resp = fread($fp, 8192);
fclose($fp);
echo "Finish\n";  

$end_time = microtime(TRUE);
echo sprintf("use time:%.3f s\n", $end_time - $start_time);

上述代码是同步执行的。如何变成异步呢?

stream_socket_client实现tcp异步客户端
由于fread读取响应数据是同步堵塞的,我们将$fp加入到事件监听后,底层会自动将该socket设置为非阻塞模式。修改fread那一行:

swoole_event_add($fp, function($fp) {
    echo $resp = fread($fp, 8192);
    swoole_event_del($fp);//socket处理完成后,从epoll事件中移除socket
    fclose($fp);
});
echo "Finish\n";  //swoole_event_add不会阻塞进程,这行代码会顺序执行

执行后输出:

Finish
use time:0.087 s
HTTP/1.1 200 OK
Server: AliyunOSS
Date: Sat, 21 Apr 2018 08:36:40 GMT
Content-Type: application/json
Content-Length: 26
Connection: keep-alive
x-oss-request-id: 5ADAF81884D23C965A5D2614
Accept-Ranges: bytes
ETag: "3B3B50D9C802324BB72A74FCD9060004"
Last-Modified: Sat, 21 Apr 2018 04:43:33 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 9917578698767912878
x-oss-storage-class: Standard
Content-MD5: OztQ2cgCMku3KnT82QYABA==
x-oss-server-time: 5

{"url":"http://52fhy.com"}

swoole_event_add函数原型:

bool swoole_event_add(mixed $sock, mixed $read_callback, mixed $write_callback = null,
    int $flags = null);

$sock可以为以下四种类型:

int,就是文件描述符,包括swoole_client->sock、swoole_process->pipe或者其他fd
stream资源,就是stream_socket_client/fsockopen创建的资源
sockets资源,就是sockets扩展中socket_create创建的资源,需要在编译时加入 ./configure --enable-sockets
object,swoole_process或swoole_client,底层自动转换为管道或客户端连接的socket

多个tcp客户端实时交互

上面的例子,已经实现了异步tcp客户端。接下来的例子会复杂些:可以在客户端A输入,客户端B能实时收到,反之也可以。

首先,我们得创建个tcp_server:
swoole_tcp_server.php

<?php 

$serv = new swoole_server('0.0.0.0', 9001);

$serv->on('Start', function(){
    echo "Tcp server start. Waiting client... \n";
});

$serv->on('Connect', function($serv, $fd){
    echo "New client fd:{$fd}. \n";
});

$serv->on('Receive', function($serv, $fd, $from_id, $data){
    echo "Recv msg from fd:{$fd}:{$data}\n";
    foreach ($serv->connections as $client) {
        if($fd != $client){
            $serv->send($client, $data);
        }
    }
});

$serv->on('Close', function($serv, $fd){
    echo "Client fd:{$fd} closed. \n";
});

$serv->start();

然后实现客户端:
event_add_tcp_client.php

<?php

$socket = @stream_socket_client("tcp://127.0.0.1:9001", $errno, $errstr, 30);
if(!$socket) exit("connect server err!");

function onRead($socket){
    $msg = stream_socket_recvfrom($socket, 1024);
    if(!$msg){
        swoole_event_del($socket);
    }
    echo "Recv: {$msg}\n";
    fwrite(STDOUT, "ENTER MSG:");
}

function onWrite($socket){
    echo "onWrite\n";
}

function onStdin(){
    global $socket;
    $msg = trim(fgets(STDIN));
    if($msg == 'exit'){ //必须trim此处才会相等
        swoole_event_exit();
        // exit();
    }
    fwrite($socket, $msg);//数据量大的时候用swoole_event_write
    fwrite(STDOUT, "ENTER MSG:");
}

swoole_event_add($socket, 'onRead', 'onWrite');
swoole_event_add(STDIN, 'onStdin');

fwrite(STDOUT, "ENTER MSG:");  //swoole_event_add不会阻塞进程,这行代码会顺序执行

先运行服务端:

$ php swoole_tcp_server.php
Tcp server start. Waiting client...

打开两个终端,运行客户端:

$ php event_add_tcp_client.php
ENTER MSG:
image.png
swoole_client

其实swoole已经提供了异步的swoole_client,无需使用stream_socket_*系列函数:

<?php 

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
$client->on("connect", function(swoole_client $cli) {
    
});
$client->on("receive", function(swoole_client $cli, $data){
    echo "Receive: $data";
    $cli->send(str_repeat('A', 100)."\n");
    sleep(1);
});
$client->on("error", function(swoole_client $cli){
    echo "error\n";
});
$client->on("close", function(swoole_client $cli){
    echo "Connection close\n";
});
$client->connect('127.0.0.1', 9001);

还有swoole实现的tcp/udp同步阻塞客户端:

$client = new swoole_client(SWOOLE_SOCK_TCP);
if (!$client->connect('127.0.0.1', 9001, -1))
{
    exit("connect failed. Error: {$client->errCode}\n");
}
$client->send("hello world\n");
echo $client->recv();
$client->close();

swoole_client 函数原型:

swoole_client->__construct(int $sock_type, int $is_sync = SWOOLE_SOCK_SYNC, string $key);

可以使用swoole提供的宏来之指定类型,请参考 swoole常量定义

sock_type表示socket的类型,如TCP/UDP 使用sock_type | SWOOLE_SSL可以启用SSL加密
is_sync表示同步阻塞还是异步非阻塞,默认为同步阻塞key用于长连接的Key,默认使用IP:PORT作为key。相同key的连接会被复用.
php-fpm/apache环境下只能使用同步客户端。异步客户端只能使用在cli命令行环境。

异步http客户端

curl或者file_get_contents发送http请求是同步阻塞的。基swoole_event_add封装可以实现异步。

swoole_event_add实现异步http客户端
event_add_http_client.php:

<?php

class HttpClient{

    private $callback = null;

    public function __construct($url, $method = 'GET', $postfields = NULL, $headers = array()){
        
        //子进程发起请求
        $process = new swoole_process(function(swoole_process $worker) use($url, $method, $postfields, $headers){
            $response = $this->http($url, $method, $postfields, $headers);
            $worker->write($response);
        }, true);
        $process->start();

        //异步读取
        swoole_event_add($process->pipe, function($pipe) use ($process){
            $response = $process->read();
            // print_r($response);
            if(is_callable($this->callback)){
                call_user_func($this->callback, $response); //回调
            }
            swoole_event_del($pipe);
        });
    }

    public function setCallback($callback){
        $this->callback = $callback;
    }

    /**
     * http请求
     */
    private function http($url, $method, $postfields = NULL, $headers = array()) {
        try{
            $ssl = stripos($url,'https://') === 0 ? true : false;
            $ci = curl_init();
            /* Curl settings */
            curl_setopt($ci, CURLOPT_USERAGENT, @$_SERVER['HTTP_USER_AGENT']); //在HTTP请求中包含一个"User-Agent: "头的字符串。    
            curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 30);
            curl_setopt($ci, CURLOPT_TIMEOUT, 30);
            curl_setopt($ci, CURLOPT_RETURNTRANSFER, TRUE);
            curl_setopt($ci, CURLOPT_ENCODING, "");
            if ($ssl) {
                curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, 0); // 对认证证书来源的检查 
                curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, 2); // 从证书中检查SSL加密算法是否存在
            }
            curl_setopt($ci, CURLOPT_HEADER, FALSE);
    
            switch ($method) {
                case 'POST':
                    curl_setopt($ci, CURLOPT_POST, TRUE);
                    if (!empty($postfields)) {
                        curl_setopt($ci, CURLOPT_POSTFIELDS, $postfields);
                    }
                    break;
            }
    
            curl_setopt($ci, CURLOPT_URL, $url );
            curl_setopt($ci, CURLOPT_HTTPHEADER, $headers );
            curl_setopt($ci, CURLINFO_HEADER_OUT, TRUE );
    
            $response = curl_exec($ci);
            $httpCode = curl_getinfo($ci, CURLINFO_HTTP_CODE);
            $httpInfo = curl_getinfo($ci);
            
            if (FALSE === $response)
                throw new Exception(curl_error($ci), curl_errno($ci));
        
        } catch(Exception $e) {
            throw $e;
        }
        
        //echo '<pre>';
        //var_dump($response);
        //var_dump($httpInfo);

        curl_close ($ci);
        return $response;
    }
}

$client = new HttpClient('http://www.52fhy.com/test.json');
$client->setCallback(function($response){
    print_r($response);
});
echo "OK\n";

运行:

$ php event_add_http_client.php
OK
{"url":"http://52fhy.com"}[

由返回结果可以看出,客户端请求是异步执行的。

swoole_http_client

Swoole也内置了http异步客户端(swoole>=1.8.0)。

相比curl和file_get_contents这样PHP提供的Http客户端,swoole_http_client最大的优势是支持大量并发。
file_get_contents只能同时请求一个URL,并发只能通过开启多进程实现。curl提供了curl_multi功能实现并发基于select和多线程。并发能力都很差。而swoole_http_client是基于epoll实现的异步客户端,没有并发限制,可在一个进程内同时并发上万请求。

示例:

get:

$cli = new swoole_http_client('www.52fhy.com', 80);
$cli->setHeaders(['User-Agent' => "swoole"]);

$cli->get('/test.json', function ($cli) {
    echo $cli->body;
});

echo "ok\n";

输出:

ok
{"url":"http://52fhy.com"}

post:

$cli = new swoole_http_client('127.0.0.1', 81); 
$cli->post('/post_demo.php', array("a" => '1234', 'b' => '456'), function ($cli) {
    echo "Length: " . strlen($cli->body) . "\n";
    echo $cli->body;
});

echo "ok\n";

上传文件
swoole_http_client底层使用了sendfile系统调用实现了http上传超大文件,配合底层的epoll可以实现非常低的消耗完成超巨大文件的上传。sendfile是零拷贝的,占用内存非常少,并且不存在多次内存复制开销。

$cli = new swoole_http_client('127.0.0.1', 80);

$cli->addFile(__DIR__.'/post.data', 'post');
$cli->addFile(dirname(__DIR__).'/test.jpg', 'debug');

$cli->post('/dump2.php', array("xxx" => 'abc', 'x2' => 'rango'), function ($cli) {
    echo $cli->body;
    $cli->close();
});

websocket:

<?php 
$cli = new swoole_http_client('118.25.40.163', 8088);

$cli->on('message', function ($_cli, $frame) {
    // var_dump($frame);
    echo $frame->data;
});

$cli->upgrade('/', function ($cli) {
    $cli->push("hello world");
});

echo "ok\n";

发送完客户端会立即close。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容