Swoole系列(2) - 快速启动

1.应用场景

主要用于用代码来学习和实践swoole,学习用法以及如何在项目中开发,同时学习实现原理。

2.学习/操作

1.文档阅读

Swoole4 文档 - start_tcp_server

Swoole4 文档 - start_udp_server

Swoole系列(1) - 基于Linux[centOS]/Docker/Mac安装

2.整理输出

前言

只要修改了文件,都要重启服务「不管手动还是后期实现自动」

2.1 TCP服务器

服务端

tcp_server.php

<?php

//创建Server对象,监听 127.0.0.1:9501 端口

$server = new Swoole\Server('127.0.0.1', 9501);

//监听连接进入事件

$server->on('Connect', function ($server, $fd) {

echo "Client: Connect.\n";

});

//监听数据接收事件

$server->on('Receive', function ($server, $fd, $reactor_id, $data) {

$server->send($fd, "Server: {$data}");

});

//监听连接关闭事件

$server->on('Close', function ($server, $fd) {

echo "Client: Close.\n";

});

//启动服务器

$server->start();

CLI下 启动服务:

php tcp_server.php

查看是否启动9501端口,监听服务

netstat -an | grep 9501

 CLI输出「所有的输出都是在CLI下」

客户端

「使用 telnet/netcat 工具连接服务器」

排查故障

无法连接到服务器的简单检测手段

  • 在 Linux 下,使用 netstat -an | grep 端口,查看端口是否已经被打开处于 Listening 状态「如:netstat -an | grep 9501
  • 上一步确认后,再检查防火墙问题
  • 注意服务器所使用的 IP 地址,如果是 127.0.0.1 回环地址,则客户端只能使用 127.0.0.1 才能连接上
  • 用的阿里云服务或者腾讯服务,需要在安全权限组进行设置开发的端口

TCP 数据包边界问题

参考 TCP 数据包边界问题

2.2 UDP服务器

服务端

udp_server.php

<?php

$server = new Swoole\Server('127.0.0.1', 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);

//监听数据接收事件

$server->on('Packet', function ($server, $data, $clientInfo) {

var_dump($clientInfo);

$server->sendto($clientInfo['address'], $clientInfo['port'], "Server:{$data}");

});

//启动服务器

$server->start();

CLI下 启动服务:

php udp_server.php

查看是否启动9502端口,监听服务

netstat -an | grep 9502

UDP 服务器与 TCP 服务器不同,UDP 没有连接的概念。「即UDP面向无连接」

启动 Server 后,客户端无需 Connect,直接可以向 Server 监听的 9502 端口发送数据包。

对应的事件为 onPacket。

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

客户端

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

netcat -u 127.0.0.1 9502
hello
Server: hello

服务端CLI下输出

2.3 HTTP服务器

服务端

http_server.php

<?php

$http = new Swoole\Http\Server('0.0.0.0', 9501);

$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
  • 9501 监听的端口,如果被占用程序会抛出致命错误,中断执行。

启动服务

php http_server.php

客户端

  • 可以打开浏览器,访问 http://127.0.0.1:9501 查看程序的结果。
  • 也可以使用 Apache ab 工具对服务器进行压力测试

Chrome 请求两次问题

使用 Chrome 浏览器访问服务器,会产生额外的一次请求,/favicon.ico,可以在代码中响应 404 错误。「这是浏览器自己的默认行为,解决办法就是可以过滤掉,或者,在web root目录下,增加一个avicon.ico的文件」

服务端cli下这时是没有输出内容的,因为没有echo,var_dump等语句输出。

修改代码:「记得重启服务」

$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>');
});
 

需要重启服务

浏览器刷新

服务端cli输出

➜  learning_of_swoole git:(main) ✗ php server/http_server.php 
array(1) {
  ["name"]=>
  string(7) "william"
}
NULL

URL 路由

应用程序可以根据 $request->server['request_uri'] 实现路由。

「经常使用的web mvc框架都是这样做的,而且也只能这样做,不然怎么实现请求对应到具体的控制和动作中,可以看下个人写的mvc 框架:PHP - 从零开始编写自己的PHP框架

如:http://127.0.0.1:9501/test/index/?a=1,代码中可以这样实现 URL 路由。

$http->on('Request', function ($request, $response) {
    list($controller, $action) = explode('/', trim($request->server['request_uri'], '/'));
    //根据 $controller, $action 映射到不同的控制器类和方法
    (new $controller)->$action($request, $response);

});

2.4 WebSocket服务器

服务端

ws_server.php

<?php

//创建WebSocket Server对象,监听0.0.0.0:9502端口
$ws = new Swoole\WebSocket\Server('0.0.0.0', 9502);

//监听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();
 

  • 客户端向服务器端发送信息时,服务器端触发 onMessage 事件回调
  • 服务器端可以调用 $server->push() 向某个客户端(使用 $fd 标识符)发送消息

运行服务端程序

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);
};
 

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

详细使用方法参考 Swoole\WebSocket

截图

浏览器输出

 服务端输出

无输出,因为客户端与服务端此刻并没有通信

具体的通信见后面

2.5 MQTT (物联网) 服务器

通过设置 open_mqtt_protocol 选项,启用后会解析 MQTT 包头,Worker 进程的 onReceive 事件每次会返回一个完整的 MQTT 数据包。

可以使用 Swoole 作为 MQTT 服务端或客户端,实现一套完整物联网(IOT)解决方案。

完整的 MQTT 协议解析和协程客户端可以使用 simps/mqtt

服务端

mqtt_server.php

<?php

// https://wiki.swoole.com/#/start/start_mqtt

function decodeValue($data)
{
    return 256 * ord($data[0]) + ord($data[1]);
}

function decodeString($data)
{
    $length = decodeValue($data);
    return substr($data, 2, $length);
}

function mqttGetHeader($data)
{
    $byte = ord($data[0]);

    $header['type'] = ($byte & 0xF0) >> 4;
    $header['dup'] = ($byte & 0x08) >> 3;
    $header['qos'] = ($byte & 0x06) >> 1;
    $header['retain'] = $byte & 0x01;

    return $header;
}

function eventConnect($header, $data)
{
    $connect_info['protocol_name'] = decodeString($data);
    $offset = strlen($connect_info['protocol_name']) + 2;

    $connect_info['version'] = ord(substr($data, $offset, 1));
    $offset += 1;

    $byte = ord($data[$offset]);
    $connect_info['willRetain'] = ($byte & 0x20 == 0x20);
    $connect_info['willQos'] = ($byte & 0x18 >> 3);
    $connect_info['willFlag'] = ($byte & 0x04 == 0x04);
    $connect_info['cleanStart'] = ($byte & 0x02 == 0x02);
    $offset += 1;

    $connect_info['keepalive'] = decodeValue(substr($data, $offset, 2));
    $offset += 2;
    $connect_info['clientId'] = decodeString(substr($data, $offset));
    return $connect_info;
}

$server = new Swoole\Server('127.0.0.1', 9501, SWOOLE_BASE);

$server->set([
    'open_mqtt_protocol' => true, // 启用 MQTT 协议
    'worker_num' => 1,
]);

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

$server->on('Receive', function ($server, $fd, $reactor_id, $data) {
    $header = mqttGetHeader($data);
    var_dump($header);

    if ($header['type'] == 1) {
        $resp = chr(32) . chr(2) . chr(0) . chr(0);
        eventConnect($header, substr($data, 2));
        $server->send($fd, $resp);
    } elseif ($header['type'] == 3) {
        $offset = 2;
        $topic = decodeString(substr($data, $offset));
        $offset += strlen($topic) + 2;
        $msg = substr($data, $offset);
        echo "client msg: {$topic}\n----------\n{$msg}\n";
        //file_put_contents(__DIR__.'/data.log', $data);
    }


    echo "received length=" . strlen($data) . "\n";
});

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

$server->start();

2.6 执行异步任务 (Task)

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

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

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

服务端

tcp_server_with_async_task.php

<?php

// 创建Server对象,监听 127.0.0.1:9501 端口
$server = new Swoole\Server('127.0.0.1', 9509);

// 设置异步任务的工作进程数量
$server->set([

    // 未设置 worker_num,底层会启动与 CPU 数量一致的 Worker 进程「这里mac cpu就是8核」
    // 'worker_num' => 8, 
    'task_worker_num' => 4
]);

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

// 监听数据接收事件,此回调函数在worker进程中执行
$server->on('Receive', function ($server, $fd, $reactor_id, $data) {
    // 投递异步任务
    $task_id = $server->task($data);
    echo "Dispatch AsyncTask: id={$task_id}\n";
});

// 处理异步任务(此回调函数在task进程中执行)
$server->on('Task', function ($server, $task_id, $reactor_id, $data) {
    echo "New AsyncTask[id={$task_id}]".PHP_EOL;
    //返回任务执行的结果 [finish 操作是可选的,也可以不返回任何结果]
    $server->finish("{$data} -> OK");
});

// 处理异步任务的结果(此回调函数在worker进程中执行)
$server->on('Finish', function ($server, $task_id, $data) {
    echo "AsyncTask[{$task_id}] Finish: {$data}".PHP_EOL;
});

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

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

客户端

还是使用telnet进行进行测试即可

测试结果

  当关闭服务端服务时,客户端的连接关闭「所有的客户端」

同时结合异步任务,分析swoole的进程树,进程模型

Swoole Server支持两种进程模型运行,默认是 SWOOLE_PROCESS

Swoole的进程模型分析:

➜  learning_of_swoole git:(main) ✗ ps -ef | grep 2-快速启动/server/tcp_server_with_async_task.php
  501 23702     1   0  4:44下午 ??         0:00.12 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23703 23702   0  4:44下午 ??         0:00.02 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23708 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23709 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23711 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23712 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23713 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23714 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 23715 23703   0  4:44下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 25403 23703   0  4:55下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 25407 23703   0  4:55下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 25409 23703   0  4:55下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 25417 23703   0  4:55下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 25430 23703   0  4:56下午 ??         0:00.00 php 2-快速启动/server/tcp_server_with_async_task.php
  501 27660 25496   0  5:13下午 ttys001    0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox 2-快速启动/server/tcp_server_with_async_task.php

// 查看进程树
➜  learning_of_swoole git:(main) ✗ pstree -p 23702
-+= 00001 root /sbin/launchd
 \-+= 23702 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
   \-+- 23703 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23708 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23709 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23711 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23712 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23713 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23714 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 23715 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 25403 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 25407 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 25409 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     |--- 25417 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
     \--- 25430 huangbaoyin php 2-快速启动/server/tcp_server_with_async_task.php
➜  learning_of_swoole git:(main) ✗  

Swoole进程模型:
-+= 00001 // 操作系统真正的祖宗进程
 \-+= 23702 // Swoole的主进程「Master 进程」swoole的祖宗进程
   \-+- 23703 // Manager进程,父进程为23702,下面的进程都是它的子进程,可以看到12个,8{worker进程}+4{task进程}
     |--- 23708 
     |--- 23709 
     |--- 23711 
     |--- 23712 
     |--- 23713 
     |--- 23714 
     |--- 23715 
     |--- 25403 
     |--- 25407 
     |--- 25409 
     |--- 25417 
     \--- 25430 

关于Swoole的进程 / 线程结构图,参见

Swoole4 文档 - 服务端 (异步风格)

https://blog.csdn.net/william_n/article/details/127598014

后续补充

...

3.问题/补充

1. 遇到的问题:

Swoole进程杀死master进程,存在异常场景,强制杀掉master之后,manager和worker还在的情况

网上已经有讨论

  

Manager进程-Swoole-Swoole文档中心

异常问题如下;

先是使用kill -9 master进程ID

learning_of_swoole git:(main) ✗ kill -9 23702

➜ learning_of_swoole git:(main) ✗ pstree -p 23702

➜ learning_of_swoole git:(main) ✗

然后,重新启动服务,依然出现端口被占用的问题

之后看到,manager进程和worker/task进程依然存在

  

现在杀死了manager进程后,worker/task进程都依然存在,父进程变了系统祖先进程

这里面涉及到进程回收的问题「另外可以看下,僵尸进程的概念」

Linux系统僵尸进程详解 - 良许Linux - 博客园

【Linux随笔】Killall 、Kill 、Pkill三个命令之间的区别 - 腾讯云开发者社区-腾讯云

现在的解决办法:

使用killall 命令,杀死所有相同进程名的进程们

killall -9 php

截图

   

重启服务端

同时启用多个TCP客户端进行与服务端进行通信

总结方式有:

解决办法:

方式一:使用killall // 好处,不论是否已kill master进程

killall -9 php 2-快速启动/server/tcp_server_with_async_task.php // 杀死所有「php 2-快速启动/server/tcp_server_with_async_task.php」进程名的进程

方式二: 如果还没有kill master进程「或者说只要这些进程的父进程不是PID=1的进程就可以」

kill -15 父进程  // 推荐,但是当父进程为1时,不要这么做,除非你想重启电脑

方式三: 一个一个kill // 没什么好说的,就是麻烦

4.参考

参见上面文档列表

后续补充

...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值