swoole环境搭好之后,demo测试相关服务。
1)、虚拟机环境:
虚拟机ip:192.168.142.128
Linux : CentOS Linux release 7.9.2009 (Core)
swoole:4.8.8
2)、本机 添加 host,192.168.142.128 www.cuikai.com
1、TCP 服务器
新建 swoole_tcp.php
<?php //创建Server对象,监听 127.0.0.1:9501 端口 $server = new Swoole\Server('192.168.142.128', 9501); //监听连接进入事件 $server->on('Connect', function ($server, $fd) { echo "Client: Connect.\n"; }); //监听数据接收事件 $server->on('Receive', function ($server, $fd, $reactor_id, $data) { $server->send($fd, "receive: {$data}"); }); //监听连接关闭事件 $server->on('Close', function ($server, $fd) { echo "Client: Close.\n"; }); //启动服务器 $server->start();
虚拟机执行程序,启动服务:
[root@kafka1 ]# php swoole_tcp.php
在命令行下运行 swoole_tcp.php 程序,启动成功后可以使用 netstat 工具看到已经在监听 9501 端口。
netstat工具使用方法:netstat -an | grep 9501
[root@kafka1 ]# netstat -an | grep 9501 tcp 0 0 192.168.241.129:9501 0.0.0.0:* LISTEN tcp 0 0 192.168.241.129:9501 192.168.241.1:51945 ESTABLISHED tcp 0 0 192.168.241.129:9501 192.168.241.1:51692 ESTABLISHED tcp 0 0 192.168.241.129:9501 192.168.241.1:51425 ESTABLISHED
这时就可以使用 telnet/netcat 工具连接服务器。具体如图:
Linux下:
[root@kafka1 ]# telnet 192.168.142.128 9501 Trying 192.168.142.128... Connected to 192.168.142.128. Escape character is '^]'. ^] //连接后,敲击键盘`ctrl+]`键,就可以发送消息了, telnet> //回车 1111 //发送消息 receive: 1111 //回显 ^] //连接后,敲击键盘`ctrl+]`键,输入 quit,回车即可退出 telnet> quit Connection closed. [root@kafka1 ]#
服务安装 telnet命令---centos安装telnet命令的方法:
yum list telnet* ##列出telnet相关的安装包 yum install telnet-server ##安装telnet服务 yum install telnet.* ##安装telnet客户端
Windows下:
1、Ctrl + R, 输入 cmd, 进入命令行模式。
2、输入 telnet 192.168.241.129 9501 命令,Enter 敲击回车,敲击键盘`ctrl+]`键。
3、发送内容后,两次回车即可看到打印的相应信息。
亦可以使用 TCP/UDP调试工具,发送消息进行端口测试。
代码逻辑分析:
通过以上代码就可以创建一个TCP服务,监听的端口是9501。它的逻辑很简单,当客户端$socket通过网络发送一个hello字符串时,服务器端会回复一个Server:hello字符串。
Server是异步服务器,所以是通过监听事件的方式来编写程序的。当对应的事件发生时,底层会主动回调指定的函数。如当有限的TCP连接进入时,会执行 onConnect 事件回调,当某个连接向服务器发送数据时会回调 onReceive 函数。
注意事项:
1)、服务器可以同时被成千上万个客户端连接,$fd 就是客户端连接的唯一标识符
2)、Receive 事件的回调函数中 $reactor_id为线程ID
3)、调用 $server->send() 方法向客户端连接发送数据,参数就是 $fd 柯达护短标识符
4)、调用 $server->close() 方法可以强制关闭某个客户端连接
5)、客户端可能会主动断开连接,此时会触发 onClose 事件回调
无法连接到服务器的简单检测手段:
1)、在linux下,使用 netstat -an | grep 端口,查看端口是否已经被打开处于Listening状态
2)、上一步确认后,再检查防火墙问题
3)、注意服务器所用的IP地址,如果是127.0.0.1回环地址,则客户端只能使用127.0.0.1才能连接上
4)、用的阿里云服务器或者腾讯云服务器,需要在安全权限组进行设置开发的端口
2、UDP 服务器
新建 swoole_udp.php
<?php $server = new Swoole\Server('192.168.142.128', 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); //监听数据接收事件 $server->on('Packet', function ($server, $data, $clientInfo) { var_dump($clientInfo); $server->sendto($clientInfo['address'], $clientInfo['port'], "recevice:{$data}"); }); //启动服务器 $server->start();
UDP 服务器与 TCP 服务器不同,UDP 没有连接的概念。启动 Server 后,客户端无需 Connect,直接可以向 Server 监听的 9502 端口发送数据包。对应的事件为 onPacket。
$clientInfo 是客户端的相关信息,是一个数组,有客户端的 IP 和端口等内容
调用 $server->sendto 方法向客户端发送数据
启动服务
[root@kafka1 ]# php swoole_udp.php
即可看到 swoole UDP服务器在收到监听后,打印客户端相应信息。
UDP 服务器可以使用 netcat -u 来连接测试。
[root@bogon async]# nc -u 127.0.0.1 9502 111 Server:111 222 Server:222
3、HTTP 服务器
新建 swoole_http.php
<?php $http = new Swoole\Http\Server('0.0.0.0', 9503); $http->on('Request', function ($request, $response) { $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。
9503 监听的端口,如果被占用程序会抛出致命错误,中断执行。
启动服务
[root@kafka1 ]# php swoole_http.php
本地浏览器访问地址:www.cuikai.com:9503/
Chrome 请求两次问题
使用 Chrome 浏览器访问服务器,会产生额外的一次请求,/favicon.ico,可以在代码中响应 404 错误。
<?php $http = new Swoole\Http\Server('0.0.0.0', 9503); $http->on('Request', function ($request, $response) { if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') { $response->end(); return; } 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();
URL 路由
应用程序可以根据 $request->server['request_uri'] 实现路由。如:http://127.0.0.1:9501/test/index/?a=1,代码中可以这样实现 URL 路由。
<?php $http = new Swoole\Http\Server('0.0.0.0', 9501); $http->on('Request', function ($request, $response) { list($controller, $action) = explode('/', trim($request->server['request_uri'], '/')); //根据 $controller, $action 映射到不同的控制器类和方法 (new $controller)->$action($request, $response); }); $http->start();
4、WebSocket 服务器
新建 swoole_websocket.php
<?php //创建WebSocket Server对象,监听0.0.0.0:9502端口 $ws = new Swoole\WebSocket\Server('0.0.0.0', 9505); //监听WebSocket连接打开事件 $ws->on('Open', function ($ws, $request) { $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握手
swoole_http_server是swoole_server的子类,内置了Http的支持
swoole_websocket_server是swoole_http_server的子类, 内置了WebSocket的支持
启动服务
[root@kafka1 ]# php swoole_websocket.php
可以使用 Chrome 浏览器进行测试,JS 代码为:
var wsServer = 'ws://192.168.142.128:9505'; 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客户端
Comet
WebSocket 服务器除了提供 WebSocket 功能之外,实际上也可以处理 HTTP 长连接。只需要增加 onRequest 事件监听即可实现 Comet 方案 HTTP 长轮询。
详细使用方法参考 Swoole\WebSocket
5、执行异步任务 (Task)
在 Server 程序中如果需要执行很耗时的操作,比如一个聊天服务器发送广播,Web 服务器中发送邮件。如果直接去执行这些函数就会阻塞当前进程,导致服务器响应变慢。
Swoole 提供了异步任务处理的功能,可以投递一个异步任务到 TaskWorker 进程池中执行,不影响当前请求的处理速度。
基于第一个 TCP 服务器,只需要增加 onTask 和 onFinish 2 个事件回调函数即可。另外需要设置 task 进程数量,可以根据任务的耗时和任务量配置适量的 task 进程。
<?php $serv = new Swoole\Server('192.168.142.128', 9506); //设置异步任务的工作进程数量 $serv->set([ 'task_worker_num' => 4 ]); //此回调函数在worker进程中执行 $serv->on('Receive', function($serv, $fd, $reactor_id, $data) { //投递异步任务 $task_id = $serv->task($data); echo "Dispatch AsyncTask: id={$task_id}\n"; }); //处理异步任务(此回调函数在task进程中执行) $serv->on('Task', function ($serv, $task_id, $reactor_id, $data) { echo "New AsyncTask[id={$task_id}]".PHP_EOL; //返回任务执行的结果 $serv->finish("{$data} -> OK"); }); //处理异步任务的结果(此回调函数在worker进程中执行) $serv->on('Finish', function ($serv, $task_id, $data) { echo "AsyncTask[{$task_id}] Finish: {$data}".PHP_EOL; }); $serv->start();
调用 $serv->task() 后,程序立即返回,继续向下执行代码。onTask 回调函数 Task 进程池内被异步执行。执行完成后调用 $serv->finish() 返回结果。
finish 操作是可选的,也可以不返回任何结果。
6、Coroutine 协程
从 4.0 版本开始 Swoole 提供了完整的协程(Coroutine)+ 通道(Channel)特性,带来全新的 CSP 编程模型。
1)、开发者可以无感知的用同步的代码编写方式达到异步 IO 的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护
2)、同时由于底层封装了协程,所以对比传统的 PHP 层协程框架,开发者不需要使用 yield 关键词来标识一个协程 IO 操作,所以不再需要对 yield 的语义进行深入理解以及对每一级的调用都修改为 yield,这极大的提高了开发效率
3)、提供了各种类型完善的协程客户端,可以满足大部分开发者的需求。
什么是协程?
协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核 cpu 的,想利用多核 cpu 需要依赖 Swoole 的多进程模型。
什么是 channel?
channel 可以理解为消息队列,只不过是协程间的消息队列,多个协程通过 push 和 pop 操作生产消息和消费消息,用来协程之间的通讯。需要注意的是 channel 是没法跨进程的,只能一个 Swoole 进程里的协程间通讯,最典型的应用是连接池和并发调用。
什么是协程容器?
使用 Coroutine::create 或 go 方法创建协程 (参考别名小节),在创建的协程中才能使用协程 API,而协程必须创建在协程容器里面,参考协程容器。
协程调度
这里将尽量通俗的讲述什么是协程调度,首先每个协程可以简单的理解为一个线程,大家知道多线程是为了提高程序的并发,同样的多协程也是为了提高并发。
用户的每个请求都会创建一个协程,请求结束后协程结束,如果同时有成千上万的并发请求,某一时刻某个进程内部会存在成千上万的协程,那么 CPU 资源是有限的,到底执行哪个协程的代码?
决定到底让 CPU 执行哪个协程的代码决断过程就是协程调度,Swoole 的调度策略又是怎么样的呢?
首先,在执行某个协程代码的过程中发现这行代码遇到了 Co::sleep() 或者产生了网络 IO,例如 MySQL->query(),这肯定是一个耗时的过程,Swoole 就会把这个 Mysql 连接的 Fd 放到 EventLoop 中。
然后让出这个协程的 CPU 给其他协程使用:即 yield(挂起)
等待 MySQL 数据返回后就继续执行这个协程:即 resume(恢复)
其次,如果协程的代码有 CPU 密集型代码,可以开启 enable_preemptive_scheduler,Swoole 会强行让这个协程让出 CPU。
父子协程优先级
优先执行子协程 (即 go() 里面的逻辑),直到发生协程 yield(co::sleep 处),然后协程调度到外层协程
use Swoole\Coroutine; use function Swoole\Coroutine\run; echo "main start\n"; run(function () { echo "coro " . Coroutine::getcid() . " start\n"; Coroutine::create(function () { echo "coro " . Coroutine::getcid() . " start\n"; Coroutine::sleep(.2); echo "coro " . Coroutine::getcid() . " end\n"; }); echo "coro " . Coroutine::getcid() . " do not wait children coroutine\n"; Coroutine::sleep(.1); echo "coro " . Coroutine::getcid() . " end\n"; }); echo "end\n"; /* main start coro 1 start coro 2 start coro 1 do not wait children coroutine coro 1 end coro 2 end end */
全局变量
协程使得原有的异步逻辑同步化,但是在协程的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及 static 变量的一致性。
在 PHP-FPM 下可以通过全局变量获取到请求的参数,服务器的参数等,在 Swoole 内,无法 通过 $_GET/$_POST/$_REQUEST/$_SESSION/$_COOKIE/$_SERVER 等 $_开头的变量获取到任何属性参数。
可以使用 context 用协程 id 做隔离,实现全局变量的隔离。
7、TCP客户端
TCP服务已经启动,下面我们要用Swoole搭建TCP客户端来连接TCP服务。新建一个文件,命名为 swoole_client.php,代码如下:
<?php $client = new Swoole\Client(SWOOLE_SOCK_TCP); //连接到服务器 if (!$client->connect('192.168.142.128', 9501, -1)) { exit("connect failed. Error: {$client->errCode} \n"); } //向服务器发送数据 if(!$client->send("hello world")) { exit("send failed. \n"); } //从服务器接收数据 $data = $client->recv(); if(!$data) { exit("recv failed. \n"); } echo $data; //关闭连接 $client->close();
上面代码创建了一个TCP的同步客户端,此客户端用于连接 server_tcp.php 开启的TCP服务。向服务端发送一个 hello world 字符串,服务器会返回一个 Server:hello world 字符串。
这个客户端是同步阻塞的,connect/send/recv 会等待IO完成后再返回。同步阻塞操作并不消耗CPU资源。当IO操作未完成时,当前进程会自动转入 sleep 模式。当IO完成后,操作系统会唤醒当前进程,继续向下执行代码。
流程如下:
TCP需要进行3次握手,所以connect至少需要3次网络传输过程
在发送少量数据时,$client->send 都是可以立即返回的。发送大量数据时,socket 缓冲区可能会塞满,send 操作会阻塞。
recv 操作会阻塞等待服务器返回数据,recv 耗时等于服务器处理时间+网络传输耗时之和。
TCP客户端还有一种异步非阻塞的实现形式。使用异步模式时,connect 会立即返回 true。但实际上连接并未建立。这时不能在 connect 后立即使用 send 发送数据,需要先通过isConnected() 判断是否连接成功。当连接成功后,系统会自动回调 onConnect 函数。这时才可以使用 send 函数向服务器发送数据。
通过上面的代码我们就实现了TCP服务端和客户端,当服务端和客户端建立TCP连接后,如何维持连接不断开呢?下面我们将介绍一下基于Swoole搭建的TCP服务器心跳维持方案。
正常情况下客户端中断TCP连接时,会发送一个FIN包,进行4次断开握手来通知服务器。但一些异常情况下,如客户端突然断电断网或者网络异常,服务器可能无法得知客户端已断开连接。尤其是移动网络,TCP连接非常不稳定,所以需要一套机制来保证服务器和客户端之间的连接的有效性。
Swoole扩展本身内置了这种机制,开发者只需要配置一个参数即可启用。Swoole在每次收到客户端数据会记录一个时间戳,当客户端在一定时间内未向服务器端发送数据,那服务器会自动切断连接。
配置方法如下:
$serv->set(array( 'heartbeat_check_interval' => 5, 'heartbeat_idle_time' => 10, ));
上面的设置就是每5秒侦测一次心跳,一个TCP连接如果在10秒内未向服务器发送数据,连接将会被切断。
通过以上案例我们对Swoole的事件驱动模型有了一定的了解。下面我们将详细介绍Swoole的回调事件。
事件执行顺序
所有事件回调均在 $server->start 后发生
服务器关闭程序终止时最后一次事件是 onShutdown
服务器启动成功后,onStart /onManagerStart / onWorkerStart 会在不同的进程内并发执行
onReceive / onConnect / onClose 在 Worker 进程中触发
Worker / Task 进程启动/结束时会分别调用一次 onWorkerStart / onWorkerStop
onTask 事件仅在 task 进程中发生
onFinish 事件仅在 Worker进程中发生
查看当前版本
[root@kafka1 www.cuikai.com]# php --ri swoole swoole Swoole => enabled Author => Swoole Team <team@swoole.com> Version => 4.8.8 Built => Mar 19 2022 23:33:08 coroutine => enabled with boost asm context epoll => enabled eventfd => enabled signalfd => enabled cpu_affinity => enabled spinlock => enabled rwlock => enabled pcre => enabled zlib => 1.2.7 mutex_timedlock => enabled pthread_barrier => enabled futex => enabled async_redis => enabled Directive => Local Value => Master Value swoole.enable_coroutine => On => On swoole.enable_library => On => On swoole.enable_preemptive_scheduler => Off => Off swoole.display_errors => On => On swoole.use_shortname => On => On swoole.unixsock_buffer_size => 8388608 => 8388608
查看进程是否启动
[root@localhost ~]# ps -ef |grep swoole.php root 98182 3833 1 23:16 pts/0 00:00:00 php swoole.php root 98183 98182 0 23:16 pts/0 00:00:00 php swoole.php root 98188 98183 0 23:16 pts/0 00:00:00 php swoole.php root 98189 98183 0 23:16 pts/0 00:00:00 php swoole.php root 98190 98183 0 23:16 pts/0 00:00:00 php swoole.php root 98191 98183 0 23:16 pts/0 00:00:00 php swoole.php root 98193 98110 0 23:16 pts/3 00:00:00 grep --color=auto swoole.php 或 [root@localhost ~]# ps -ajft PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3822 98110 98110 98110 pts/3 98205 Ss 0 0:00 -bash 98110 98205 98205 98110 pts/3 98205 R+ 0 0:00 \_ ps -ajft 3822 98048 98048 98048 pts/2 98048 Ss+ 0 0:00 -bash 3822 97955 97955 97955 pts/1 98011 Ss 0 0:00 -bash 97955 98011 98011 97955 pts/1 98011 S+ 0 0:00 \_ tail -f error.log 3822 3833 3833 3833 pts/0 98182 Ss 0 0:00 -bash 3833 98182 98182 3833 pts/0 98182 Sl+ 0 0:00 \_ php swoole.php 98182 98183 98182 3833 pts/0 98182 S+ 0 0:00 \_ php swoole.php 98183 98188 98182 3833 pts/0 98182 S+ 0 0:00 \_ php swoole.php 98183 98189 98182 3833 pts/0 98182 S+ 0 0:00 \_ php swoole.php 98183 98190 98182 3833 pts/0 98182 S+ 0 0:00 \_ php swoole.php 98183 98191 98182 3833 pts/0 98182 S+ 0 0:00 \_ php swoole.php 1088 1733 1733 1733 tty1 1733 Ssl+ 0 0:05 /usr/bin/X :0 -background none -noreset -
参考:
附件:
TCPIP Socket调试工具 链接:https://pan.baidu.com/s/1OoGvQDcnuj8i73bheB050A 提取码:sqx4
本文为崔凯原创文章,转载无需和我联系,但请注明来自冷暖自知一抹茶ckhttp://www.cksite.cn