Swoole协程系统
总算到协程了,大家期待还是兴奋还是又期待又兴奋呢?不管怎么说,协程现在都是最流行的开发方式,没有之一。即使是 Java 它们提出的 纤程 ,其实在概念上也是大差不差的。协程的特点,我们在进阶篇 Swoole进程 相关的第一篇文章就已经说过了。相信大家都已经有了一个初步的概念。接下来,我们就看看在 Swoole 中如何应用协程服务。
异步服务的问题
对于异步来说,我们需要监听事件,并且监听的进程是并发的,所以会有一个问题,那就是无法保证前后顺序。
$serv = new Swoole\Server("0.0.0.0", 9501);
//监听连接进入事件
$serv->on('Connect', function ($serv, $fd) {
Swoole\Coroutine\System::sleep(5);//此处sleep模拟connect比较慢的情况,这种sleep()是不阻塞的
echo "onConnect", PHP_EOL;
});
//监听数据接收事件
$serv->on('Receive', function ($serv, $fd, $reactor_id, $data) {
echo "onReceive", PHP_EOL;
});
//监听连接关闭事件
$serv->on('Close', function ($serv, $fd) {
echo "Client: Close.\n";
});
//启动服务器
$serv->start();
在这个例子中,我们通过在 Connect 事件中暂停5秒,来模拟 connect 可能出现连接比较慢的问题,然后再用 telnet 测试,就会发现 Receive 事件被先输出了出来。
[root@localhost source]# php 3.3Swoole协程系统.php
onReceive
onConnect
照理说,我们的 Connect 应该是先于 Receive 执行的,毕竟要建立起连接才有数据的接收,但是由于并行多进程的执行,这一步有可能是在并行一起完成的,而回调函数中的逻辑代码则会受到各种因素的影响出现阻塞,这时候,Receive 中的回调函数先于 Connect 回调函数执行也是非常有可能出现的严重问题。
对于协程来说,它不需要监听事件,代码也是顺序执行的。这一点我们在讲协程概念的时候就说过了,它就是个函数,也没有并行性质是并发的,工作在线程之上,同一个线程内也是顺序执行的,自然也就不会有这些问题。不记得 并行 和 并发 区别的小伙伴要回去补补课了哦 【Swoole系列3.1】进程、线程、协程,面试你被问了吗?https://mp.weixin.qq.com/s/GM_oGeVYOADcsKf43BN38A 。
协程 Http 服务
使用协程来提供 Http 服务非常简单,甚至比异步方式更简单,因为我们不需要去记住那么多的事件名称了。
Swoole\Coroutine\run (function () {
$server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9501, false);
$server->handle('/', function ($request, $response) {
$response->end("<h1>Index</h1>");
});
$server->handle('/test', function ($request, $response) {
$response->end("<h1>Test</h1>");
});
$server->handle('/stop', function ($request, $response) use ($server) {
$response->end("<h1>Stop</h1>");
$server->shutdown();
});
$server->start();
});
我们需要先建立一个协程容器,也就是这个 Swoole\Coroutine\run() 方法,这是一种开启协程容器的方式,其它的方式我们后面聊到了再说。这个协程容器是什么意思呢?它就像是一个 C 或者 Java 中的 main() 函数,提供程序的入口。
在协程服务中,我们真的不需要去监听事件了,只需要在这个协程容器的回调函数中实例化一个 Swoole\Coroutine\Http\Server 对象,然后通过它的 handle() 方法获得请求路径的内容,并交给回调函数进行处理即可。这里的回调函数中的参数与异步的 onRequest 监听中的回调参数是一样的,一个请求参数,一个响应参数。
协程 TCP 服务
对于 TCP 服务来说实现协程服务端也非常简单方便,和上面的 Http 服务类似,我们还是通过在协程容器中创建 TCP 服务对象并使用 handle() 方法操作连接数据。
Swoole\Coroutine\run (function () {
$server = new Swoole\Coroutine\Server('0.0.0.0', 9501, false);
$server->handle(function(Swoole\Coroutine\Server\Connection $conn){
$data = $conn->recv();
echo $data, PHP_EOL;
$conn->send("协程 TCP :" . $data);
});
$server->start();
});
注意我们这里实例化的 Server 对象是不带 Http 命名空间的。同时,在它的 handle() 方法中,也不用路径参数了,直接就是一个回调函数。这个回调函数的参数,是一个 Swoole\Coroutine\Server\Connection 对象,它返回的实际上就是建立好的 TCP 连接对象,在这个对象中,有 recv() 和 send() 方法,分别就是接收和发送数据的两个方法。
没有 UDP 的?确实没有,如果要实现 UDP 的协程服务端,需要单独使用 Socket 方式。
Swoole\Coroutine\run(function () {
$socket = new Swoole\Coroutine\Socket(AF_INET, SOCK_DGRAM, 0);
$socket->bind('0.0.0.0', 9501);
while (true) {
$peer = null;
$data = $socket->recvfrom($peer);
echo "[Server] recvfrom[{$peer['address']}:{$peer['port']}] : $data\n";
$socket->sendto($peer['address'], $peer['port'], "Swoole: $data");
}
});
这个 Socket 相关的内容,在后面我专门讲 Socket 对象的时候再说。
协程 WebSocket 服务
WebScoket 的代码比较长,但其实还是基于的是 Http 服务。
Swoole\Coroutine\run(function () {
$server = new Swoole\Coroutine\Http\Server('0.0.0.0', 9501, false);
$server->handle('/websocket', function (Swoole\Http\Request $request, Swoole\Http\Response $ws) {
$ws->upgrade();
while (true) {
$frame = $ws->recv();
if ($frame === '') {
$ws->close();
break;
} else if ($frame === false) {
echo 'errorCode: ' . swoole_last_error() . "\n";
$ws->close();
break;
} else {
if ($frame->data == 'close' || get_class($frame) === Swoole\WebSocket\CloseFrame::class) {
$ws->close();
break;
}
$ws->push("Hello {$frame->data}!");
$ws->push("How are you, {$frame->data}?");
}
}
});
$server->handle('/', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
$response->end(<<<HTML
<h1>Swoole WebSocket Server</h1>
<script>
var wsServer = 'ws://192.168.56.133:9501/websocket';
var websocket = new WebSocket(wsServer);
websocket.onopen = function (evt) {
console.log("Connected to WebSocket server.");
websocket.send('hello');
};
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);
};
</script>
HTML
);
});
$server->start();
});
在这个示例代码中,我们其实主要操作的是 Response 对象,通过它的 upgrade() 方法向客户端发送 WebSocket 握手消息,然后循环监听消息的接收和发送。在循环体内部,通过 recv() 和 push() 方法接收和发送信息。
另外一个路径其实就是一个前端页面,为了方便测试。这个例子也是官网上的例子,当然,你也可以拿我们之前在基础阶段的静态测试页面来进行测试。
总结
通过今天的内容,我们简单地了解到了使用协程和异步方式搭起的服务器有什么不同。另外也顺便就搭起了 Http/TCP/WebSocket 的服务端程序,其中 UDP 是个特殊情况,官方并没有直接的 UDP 服务器,需要我们通过 Socket 的方式自己搭建一个。关于 Socket 的内容,我们在后面的学习中还会再讲到。
测试代码:
https://github.com/zhangyue0503/swoole/blob/main/4.Swoole%E5%8D%8F%E7%A8%8B/source/4.1Swoole%E5%8D%8F%E7%A8%8B%E6%9C%8D%E5%8A%A1.php
参考文档:
https://wiki.swoole.com/#/server/co_init
文章评论