孙宇的技术专栏 大数据/机器学习

Swoole 基础

2018-03-15

阅读:


Swoole 使用纯 C 语言编写,提供了 PHP 语言的异步多线程服务器,异步 TCP/UDP 网络客户端,异步 MySQL,异步 Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。 Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。

它是一个 php 的扩展。

安装

Swoole-2.x需要 PHP-7.0.0 或更高版本 同时,2.x 版本默认添加了许多内置依赖,不再要求必须安装诸如 mysqld 等扩展

下载地址:

http://pecl.php.net/package/swoole

这里选择 2.x 的 stable 版本: 2.1.1

wget http://pecl.php.net/get/swoole-2.1.1.tgz
tar -zxf swoole-2.1.1.tgz
cd swoole-2.1.1
phpize
./configure --with-php-config=/usr/local/opt/php/bin/php-config
make clean
make
make install

1.8.7或更高版本不再需要设置--enable-async-mysql--enable-async-httpclient. async_mysql 和 async_httpclient改为内置了

其它参数有:

--enable-swoole-debug

打开调试日志,开启此选项后swoole将打印各类细节的调试日志。生产环境不要启用。

--enable-sockets

增加对sockets资源的支持,依赖sockets扩展。开启此参数,swoole_event_add就可以添加sockets扩展创建的连接到swoole的事件循环中。 另外Server和Client的getSocket()方法也需要依赖此编译参数。

--enable-openssl

启用SSL支持,使用操作系统提供的libssl.so动态连接库

--with-openssl-dir

指定openssl库的路径。–with-openssl-dir=/opt/openssl/

--enable-http2

增加对HTTP2的支持,依赖nghttp2库

--enable-async-redis

增加异步 Redis 客户端支持, 依赖 hiredis 库

先安装 hiredis 库

git clone https://github.com/redis/hiredis
cd hiredis/
make -j
make install
// ldconfig
--enable-mysqlnd

启用mysqlnd支持,启用swoole_mysql::escapse方法。启用此参数后,PHP必须有mysqlnd模块,否则会导致swoole无法运行。

安装成功后会生成 swoole.so,将它添加到 php.ini 中

添加完后可以通过如下命令查看是否成功:

php --ri swoole

或者
php -m | grep swoole

进程管理

默认使用 SWOOLE_PROCESS 模式,因此会额外创建 Master 和 Manager 两个进程。 在设置worker_num之后,实际会出现2 + worker_num个进程。 服务器启动后,可以通过kill 主进程ID来结束所有工作进程。

Swoole提供的绝大的部分模块只能用于cli命令行终端。目前只有Client同步客户端可以用于php-fpm环境下。请勿在Web环境中使用Server等模块。

案例

tcp服务器

tcp_server.php

<?php
//创建Server对象,监听 127.0.0.1:9501端口
$serv = new swoole_server("127.0.0.1", 9501);

//监听连接进入事件
$serv->on('connect', function ($serv, $fd) {
    echo "Client: Connect.\n";
});

//监听数据接收事件
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
    echo "Client: $data";
    $serv->send($fd, "Server: replay ".$data);
});

//监听连接关闭事件
$serv->on('close', function ($serv, $fd) {
    echo "Client: Close.\n";
});

//启动服务器
$serv->start();
?>

这里就创建了一个TCP服务器,监听本机9501端口。 它的逻辑很简单,当客户端Socket通过网络发送一个 hello 字符串时,服务器会回复一个 Server: replay hello 字符串。并打印 Client: hello

swoole_server是异步服务器,所以是通过监听事件的方式来编写程序的。当对应的事件发生时底层会主动回调指定的PHP函数。

如当有新的TCP连接进入时会执行onConnect事件回调,当某个连接向服务器发送数据时会回调onReceive函数。

  1. 服务器可以同时被成千上万个客户端连接,$fd就是客户端连接的唯一标识符
  2. 调用 $server->send() 方法向客户端连接发送数据,参数就是$fd客户端标识符
  3. 调用 $server->close() 方法可以强制关闭某个客户端连接
  4. 客户端可能会主动断开连接,此时会触发onClose事件回调

执行程序

php tcp_server.php

在命令行下运行server.php程序,启动成功后可以使用 netstat 工具看到,已经在监听9501端口。

netstat -nat | grep LISTEN

通过 telnet 进行连接测试:

telnet 127.0.0.1 9501
hello
Server: hello

udp服务器

udp_server.php

<?php
//创建Server对象,监听 127.0.0.1:9502端口,类型为SWOOLE_SOCK_UDP
$serv = new swoole_server("127.0.0.1", 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);

//监听数据接收事件
$serv->on('Packet', function ($serv, $data, $clientInfo) {
    $serv->sendto($clientInfo['address'], $clientInfo['port'], "Server ".$data);
    var_dump($clientInfo);
});

//启动服务器
$serv->start();
?>

UDP服务器与TCP服务器不同,UDP没有连接的概念。启动Server后,客户端无需Connect,直接可以向Server监听的9502端口发送数据包。 对应的事件为onPacket。

$clientInfo是客户端的相关信息,是一个数组,有客户端的IP和端口等内容 调用 $server->sendto 方法向客户端发送数据

执行程序

php udp_server.php

UDP服务器可以使用 netcat -u 来连接测试

netcat -u 127.0.0.1 9502
hello
Server: hello

服务端打印内容:

array(4) {
  ["server_socket"]=>
  int(5)
  ["server_port"]=>
  int(9502)
  ["address"]=>
  string(9) "127.0.0.1"
  ["port"]=>
  int(51019)
}

http服务器

http_server.php

<?php

$http = new swoole_http_server("0.0.0.0", 9501);

$http->on('request', function ($request, $response) {
    var_dump($request->get, $request->post);
    $response->header("Content-Type", "text/html; charset=utf-8");
    $response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});

$http->start();

?>

Http服务器只需要关注请求响应即可,所以只需要监听一个onRequest事件。当有新的Http请求进入就会触发此事件。 事件回调函数有2个参数,一个是$request对象,包含了请求的相关信息,如GET/POST请求的数据。

另外一个是response对象,对request的响应可以通过操作response对象来完成。

$response->end() 方法表示输出一段HTML内容,并结束此请求。

0.0.0.0 表示监听所有IP地址,一台服务器可能同时有多个IP, 如127.0.0.1;本地回环IP:192.168.1.100;局域网IP:210.127.20.2 外网IP。这里也可以单独指定监听一个IP

9501 监听的端口,如果被占用,程序会抛出致命错误,中断执行。

执行程序

php http_server.php

同样执行完后可以查看当前 LISTEN 的端口。 这时候在浏览器中访问: http://127.0.0.1:9501,页面上显示 Hello Swoole. #6552

websocket服务器

ws_server.php

<?php
//创建websocket服务器对象,监听0.0.0.0:9502端口
$ws = new swoole_websocket_server("0.0.0.0", 9502);

//监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {
    var_dump($request->fd, $request->get, $request->server);
    $ws->push($request->fd, "hello, welcome\n");
});

//监听WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
    echo "Message: {$frame->data}\n";
    $ws->push($frame->fd, "server: {$frame->data}");
});

//监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
    echo "client-{$fd} is closed\n";
});

$ws->start();
?>

WebSocket服务器是建立在Http服务器之上的长连接服务器,客户端首先会发送一个Http的请求与服务器进行握手。 握手成功后会触发onOpen事件,表示连接已就绪,onOpen函数中可以得到 $request 对象,包含了Http握手的相关信息,如GET参数、Cookie、Http头信息等。

建立连接后客户端与服务器端就可以双向通信了。

客户端向服务器端发送信息时,服务器端触发onMessage事件回调 服务器端可以调用$server->push()向某个客户端(使用$fd标识符)发送消息 服务器端可以设置onHandShake事件回调来手工处理WebSocket握手

执行程序

php ws_server.php

可以使用Chrome浏览器进行测试,JS代码为:

var wsServer = 'ws://127.0.0.1:9502';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
    console.log("Connected to WebSocket server.");
};

websocket.onclose = function (evt) {
    console.log("Disconnected");
};

websocket.onmessage = function (evt) {
    console.log('Retrieved data from server: ' + evt.data);
};

websocket.onerror = function (evt, e) {
    console.log('Error occured: ' + evt.data);
};

不能直接使用swoole_client与websocket服务器通信,swoole_client是TCP客户端 必须实现WebSocket协议才能和WebSocket服务器通信,可以使用swoole/framework提供的PHP WebSocket客户端 (https://github.com/swoole/framework/blob/master/libs/Swoole/Client/WebSocket.php)

WebSocket服务器除了提供WebSocket功能之外,实际上也可以处理Http长连接。只需要增加onRequest事件监听即可实现Comet方案Http长轮询。

定时器

swoole提供了类似JavaScript的setInterval/setTimeout异步高精度定时器,粒度为毫秒级。使用也非常简单。

<?php
//每隔2000ms触发一次
swoole_timer_tick(2000, function ($timer_id) {
    echo "tick-2000ms\n";
});

//3000ms后执行此函数
swoole_timer_after(3000, function () {
    echo "after 3000ms.\n";
});
?>

swoole_timer_tick函数就相当于setInterval,是持续触发的

swoole_timer_after函数相当于setTimeout,仅在约定的时间触发一次

swoole_timer_tick和swoole_timer_after函数会返回一个整数,表示定时器的ID 可以使用 swoole_timer_clear 清除此定时器,参数为定时器ID

异步任务

在Server程序中如果需要执行很耗时的操作,比如一个聊天服务器发送广播,Web服务器中发送邮件。如果直接去执行这些函数就会阻塞当前进程,导致服务器响应变慢。

Swoole提供了异步任务处理的功能,可以投递一个异步任务到TaskWorker进程池中执行,不影响当前请求的处理速度。

基于第一个TCP服务器,只需要增加onTask和onFinish,2个事件回调函数即可。另外需要设置task进程数量,可以根据任务的耗时和任务量配置适量的task进程。

async_task.php

<?php

$serv = new swoole_server("127.0.0.1", 9501);

//设置异步任务的工作进程数量
$serv->set(array('task_worker_num' => 4));

$serv->on('receive', function($serv, $fd, $from_id, $data) {
    //投递异步任务
    $task_id = $serv->task($data);
    echo "Dispath AsyncTask: id=$task_id\n";
});

//处理异步任务
$serv->on('task', function ($serv, $task_id, $from_id, $data) {
    echo "New AsyncTask[id=$task_id]".PHP_EOL;
    for($i = 0; $i<10;$i++){
      echo $task_id . " - " . $i.PHP_EOL;
      sleep(1);
    }
    //返回任务执行的结果
    $serv->finish("$data -> OK");
});

//处理异步任务的结果
$serv->on('finish', function ($serv, $task_id, $data) {
    echo "AsyncTask[$task_id] Finish: $data".PHP_EOL;
});

$serv->start();
?>

执行程序

php async_task.php

在另外一个终端

telnet 127.0.0.1 9501
abc
def
bbc

服务端的打印:

Dispath AsyncTask: id=0
New AsyncTask[id=0]
0 - 0
0 - 1
0 - 2
Dispath AsyncTask: id=1
New AsyncTask[id=1]
1 - 0
0 - 3
1 - 1
0 - 4
1 - 2
0 - 5
1 - 3
Dispath AsyncTask: id=2
New AsyncTask[id=2]
2 - 0
0 - 6
1 - 4
2 - 1
0 - 7
1 - 5
2 - 2
0 - 8

在客户端输出的时候是可以连续输入的,而且输入后服务端马上做出影响。 但从 task 里的输出可以看出,多次任务并不是按我们的输入顺序同步执行的。而是异步。

同步tcp客户端

tcp_client.php

<?php
$client = new swoole_client(SWOOLE_SOCK_TCP);

//连接到服务器
if (!$client->connect('127.0.0.1', 9501, 0.5))
{
    die("connect failed.");
}
//向服务器发送数据
if (!$client->send("hello world"))
{
    die("send failed.");
}
//从服务器接收数据
$data = $client->recv();
if (!$data)
{
    die("recv failed.");
}
echo $data;
//关闭连接
$client->close();
?>

先运行前面的 php tcp_server.php 然后再运行 php tcp_client.php

当前代码会向服务端发送一个 hello world,服务端会响应一个 Server: hello world。

这个客户端是同步阻塞的,connect/send/recv 会等待IO完成后再返回。

同步阻塞操作并不消耗CPU资源,IO操作未完成当前进程会自动转入sleep模式,当IO完成后操作系统会唤醒当前进程,继续向下执行代码。

TCP需要进行3次握手,所以connect至少需要3次网络传输过程

在发送少量数据时 $client->send 都是可以立即返回的。发送大量数据时,socket缓存区可能会塞满,send操作会阻塞。

recv操作会阻塞等待服务器返回数据,recv耗时等于服务器处理时间+网络传输耗时之和。

断开连接时又要进行四次网络传输,即四次挥手。

异步tcp客户端

tcp_async_client.php

<?php

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);

//注册连接成功回调
$client->on("connect", function($cli) {
    $cli->send("hello world\n");
});

//注册数据接收回调
$client->on("receive", function($cli, $data){
    echo "Received: ".$data."\n";
});

//注册连接失败回调
$client->on("error", function($cli){
    echo "Connect failed\n";
});

//注册连接关闭回调
$client->on("close", function($cli){
    echo "Connection close\n";
});

//发起连接
$client->connect('127.0.0.1', 9501, 0.5);
?>

异步客户端与上一个同步TCP客户端不同,异步客户端是非阻塞的。可以用于编写高并发的程序。 swoole官方提供的 redis-async、mysql-async都是基于异步swoole_client实现的。

异步客户端需要设置回调函数,有4个事件回调必须设置onConnect、onError、onReceive、onClose。分别在客户端连接成功、连接失败、收到数据、连接关闭时触发。

$client->connect() 发起连接的操作会立即返回,不存在任何等待。当对应的IO事件完成后,swoole底层会自动调用设置好的回调函数。

异步客户端只能用于cli环境。不能用在 php-fpm 这类处理中。fpm本身是leader follower同步阻塞模型,同一时间只能处理一个请求,支持不了异步。 这样可以看出,异步客户端主要用来一些微服务化的处理。比如聊天室消息处理。 虽然异步客户端只能在 cli 环境下用,但我们可以用异步服务端。

异步 mysql 客户端

PHP提供的MySQL、CURL、Redis 等客户端是同步的,会导致服务器程序发生阻塞。 Swoole提供了常用的异步客户端组件,来解决此问题。编写纯异步服务器程序时,可以使用这些异步客户端。

异步客户端可以配合使用SplQueue实现连接池,以达到长连接复用的目的。在实际项目中可以使用PHP提供的Yield/Generator语法实现半协程的异步框架。 也可以基于Promises简化异步程序的编写。

async_mysql.php

<?php
$db = new Swoole\MySQL;
$server = array(
    'host' => '127.0.0.1',
    'user' => 'test',
    'password' => 'test',
    'database' => 'test',
);

$db->connect($server, function ($db, $result) {
    $db->query("show tables", function (Swoole\MySQL $db, $result) {
        var_dump($result);
        $db->close();
    });
});
?>

与mysqli和PDO等客户端不同,Swoole\MySQL是异步非阻塞的,连接服务器、执行SQL时,需要传入一个回调函数。

connect的结果不在返回值中,而是在回调函数中。query的结果也需要在回调函数中进行处理。

异步 redis 客户端

async_redis.php

<?php
$redis = new Swoole\Redis;
$redis->connect('127.0.0.1', 6379, function ($redis, $result) {
    $redis->set('test_key', 'value', function ($redis, $result) {
        $redis->get('test_key', function ($redis, $result) {
            var_dump($result);
        });
    });
});
?>

异步 http 客户端

async_http.php

<?php
$cli = new Swoole\Http\Client('http://www.edeng.cn', 80);
$cli->setHeaders(array('User-Agent' => 'swoole-http-client'));
$cli->setCookies(array('test' => 'value'));

$cli->post('/', array("test" => 'abc'), function ($cli) {
    var_dump($cli->body);
    $cli->get('/index.php', function ($cli) {
        var_dump($cli->cookies);
        var_dump($cli->headers);
    });
});
?>

注意事项

常见问题

不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞

exit/die是危险的,会导致Worker进程退出

PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出

类/函数重复定义

由于Swoole是常驻内存的,所以加载类/函数定义的文件后不会释放。

因此引入类/函数的php文件时必须要使用include_once或require_once,否会发生cannot redeclare function/class 的致命错误。

异步编程

异步程序要求代码中不得包含任何同步阻塞操作

异步与同步代码不能混用,一旦应用程序使用了任何同步阻塞的代码,程序即退化为同步模式


下一篇 Git 常用命令

评论

文章