概述
這是關於 Swoole 學習的第五篇文章:Swoole 多協議 多埠 的應用。
主要參考官方的這兩篇文章,進行實現的 Demo。
- 網路通訊協議設計:https://wiki.swoole.com/wiki/page/484.html
- 多埠監聽的使用:https://wiki.swoole.com/wiki/page/161.html
希望通過我提供的 Demo,能夠對文件有更加深刻的理解。
網路通訊協議設計
為什麼需要通訊協議?
官方:TCP協議在底層機制上解決了UDP協議的順序和丟包重傳問題。但相比UDP又帶來了新的問題,TCP協議是流式的,資料包沒有邊界。應用程式使用TCP通訊就會面臨這些難題。因為TCP通訊是流式的,在接收1個大資料包時,可能會被拆分成多個資料包傳送。多次Send底層也可能會合併成一次進行傳送。這裡就需要2個操作來解決:分包 和 合包,所以TCP網路通訊時需要設定通訊協議。
Swoole 支援了2種型別的自定義網路通訊協議 :EOF結束符協議、固定包頭+包體協議。
EOF結束符協議
先看下,未設定協議的效果:
傳送的每條資料長度都是 23,但在 onReceive 接收資料的時候每次接收的長度不一樣,並沒有按照想象的方式進行分包。
再看下,設定了EOF結束符協議的效果:
傳送的每條資料長度都是 23,在 onReceive 接收資料的時候每次接收的也是 23 ,完美。
主要設定項如下:
'package_max_length' => '8192',
'open_eof_split' => true,
'package_eof' => "\r\n"
不做解釋,官方文件已經寫的很清楚。
示例程式碼如下:
server.php
<?php
class Server
{
private $serv;
public function __construct() {
$this->serv = new swoole_server('0.0.0.0', 9501);
$this->serv->set([
'worker_num' => 2, //開啟2個worker程式
'max_request' => 4, //每個worker程式 max_request設定為4次
'dispatch_mode' => 2, //資料包分發策略 - 固定模式
//EOF結束符協議
'package_max_length' => '8192',
'open_eof_split' => true,
'package_eof' => "\r\n"
]);
$this->serv->on('Start', [$this, 'onStart']);
$this->serv->on('Connect', [$this, 'onConnect']);
$this->serv->on("Receive", [$this, 'onReceive']);
$this->serv->on("Close", [$this, 'onClose']);
$this->serv->start();
}
public function onStart($serv) {
echo "#### onStart ####".PHP_EOL;
echo "SWOOLE ".SWOOLE_VERSION . " 服務已啟動".PHP_EOL;
echo "swoole_cpu_num:".swoole_cpu_num().PHP_EOL;
echo "master_pid: {$serv->master_pid}".PHP_EOL;
echo "manager_pid: {$serv->manager_pid}".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onConnect($serv, $fd) {
echo "#### onConnect ####".PHP_EOL;
echo "客戶端:".$fd." 已連線".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onReceive($serv, $fd, $from_id, $data) {
echo "#### onReceive ####".PHP_EOL;
var_dump($data);
}
public function onClose($serv, $fd) {
echo "Client Close.".PHP_EOL;
}
}
$server = new Server();
client.php
<?php
class Client
{
private $client;
public function __construct() {
$this->client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
$this->client->on('Connect', [$this, 'onConnect']);
$this->client->on('Close', [$this, 'onClose']);
$this->client->on('Error', [$this, 'onError']);
}
public function connect() {
if(!$fp = $this->client->connect("127.0.0.1", 9501)) {
echo "Error: {$fp->errMsg}[{$fp->errCode}]".PHP_EOL;
return;
}
}
public function onConnect() {
fwrite(STDOUT, "傳送測試資料(Y or N):");
swoole_event_add(STDIN, function() {
$msg = trim(fgets(STDIN));
if ($msg == 'y') {
$this->send();
}
fwrite(STDOUT, "傳送測試資料(Y or N):");
});
}
public function send() {
$msg_info = "客戶端發資訊...\r\n";
$i = 0;
while ($i < 50) {
var_dump($msg_info);
$this->client->send($msg_info);
$i++;
}
}
public function onClose() {
echo "Client close connection".PHP_EOL;
}
public function onError() {
}
}
$client = new Client();
$client->connect();
固定包頭+包體協議
先看下,未設定協議的效果:
很明顯,在 onReceive 接收到的資料,是少的。
再看下,設定協議的效果:
主要設定項如下:
'open_length_check' => true,
'package_max_length' => '8192',
'package_length_type' => 'N',
'package_length_offset' => '0',
'package_body_offset' => '4',
不做解釋,官方文件已經寫的很清楚。
示例程式碼如下:
server.php
<?php
class Server
{
private $serv;
public function __construct() {
$this->serv = new swoole_server('0.0.0.0', 9501);
$this->serv->set([
'worker_num' => 2, //開啟2個worker程式
'max_request' => 4, //每個worker程式 max_request設定為4次
'dispatch_mode' => 2, //資料包分發策略 - 固定模式
//固定包頭+包體協議
'open_length_check' => true,
'package_max_length' => '8192',
'package_length_type' => 'N',
'package_length_offset' => '0',
'package_body_offset' => '4',
]);
$this->serv->on('Start', [$this, 'onStart']);
$this->serv->on('Connect', [$this, 'onConnect']);
$this->serv->on("Receive", [$this, 'onReceive']);
$this->serv->on("Close", [$this, 'onClose']);
$this->serv->start();
}
public function onStart($serv) {
echo "#### onStart ####".PHP_EOL;
echo "swoole_cpu_num:".swoole_cpu_num().PHP_EOL;
echo "SWOOLE ".SWOOLE_VERSION . " 服務已啟動".PHP_EOL;
echo "master_pid: {$serv->master_pid}".PHP_EOL;
echo "manager_pid: {$serv->manager_pid}".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onConnect($serv, $fd) {
echo "#### onConnect ####".PHP_EOL;
echo "客戶端:".$fd." 已連線".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onReceive($serv, $fd, $from_id, $data) {
echo "#### onReceive ####".PHP_EOL;
$length = unpack('N', $data)[1];
echo "Length:".$length.PHP_EOL;
$msg = substr($data, -$length);
echo "Msg:".$msg.PHP_EOL;
}
public function onClose($serv, $fd) {
echo "Client Close.".PHP_EOL;
}
}
$server = new Server();
client.php
<?php
class Client
{
private $client;
public function __construct() {
$this->client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
$this->client->on('Connect', [$this, 'onConnect']);
$this->client->on('Close', [$this, 'onClose']);
$this->client->on('Error', [$this, 'onError']);
}
public function connect() {
if(!$fp = $this->client->connect("127.0.0.1", 9501, 1)) {
echo "Error: {$fp->errMsg}[{$fp->errCode}]".PHP_EOL;
return;
}
}
public function onConnect() {
fwrite(STDOUT, "傳送測試資料(Y or N):");
swoole_event_add(STDIN, function() {
$msg = trim(fgets(STDIN));
if ($msg == 'y') {
$this->send();
}
fwrite(STDOUT, "傳送測試資料(Y or N):");
});
}
public function send() {
$msg = '客戶端發的資訊...';
$msg_info = pack('N', strlen($msg)).$msg;
$i = 0;
while ($i < 50) {
var_dump($msg_info);
$this->client->send($msg_info);
$i++;
}
}
public function onClose() {
echo "Client close connection".PHP_EOL;
}
public function onError() {
}
}
$client = new Client();
$client->connect();
多埠監聽的使用
上圖,是示例程式碼中的埠監聽:
- 9501 onMessage 處理 WebSocket。
- 9501 onRequest 處理 HTTP。
- 9502 onReceive 處理 TCP。
- 9503 onPacket 處理 UDP。
不多說,看下效果圖:
示例程式碼如下:
server.php
<?php
class Server
{
private $serv;
public function __construct() {
$this->serv = new swoole_websocket_server("0.0.0.0", 9501);
$this->serv->set([
'worker_num' => 2, //開啟2個worker程式
'max_request' => 4, //每個worker程式 max_request設定為4次
'task_worker_num' => 4, //開啟4個task程式
'dispatch_mode' => 4, //資料包分發策略 - IP分配
'daemonize' => false, //守護程式(true/false)
]);
$this->serv->on('Start', [$this, 'onStart']);
$this->serv->on('Open', [$this, 'onOpen']);
$this->serv->on("Message", [$this, 'onMessage']);
$this->serv->on("Request", [$this, 'onRequest']);
$this->serv->on("Close", [$this, 'onClose']);
$this->serv->on("Task", [$this, 'onTask']);
$this->serv->on("Finish", [$this, 'onFinish']);
//監聽 9502 埠
$tcp = $this->serv->listen("0.0.0.0", 9502, SWOOLE_SOCK_TCP);
$tcp->set([
'worker_num' => 2, //開啟2個worker程式
'max_request' => 4, //每個worker程式 max_request設定為4次
'dispatch_mode' => 2, //資料包分發策略 - 固定模式
//固定包頭+包體協議
'open_length_check' => true,
'package_max_length' => '8192',
'package_length_type' => 'N',
'package_length_offset' => '0',
'package_body_offset' => '4',
]);
$tcp->on("Receive", [$this, 'onReceive']);
//監聽 9503 埠
$udp = $this->serv->listen("0.0.0.0", 9503, SWOOLE_SOCK_UDP);
$udp->set([
'worker_num' => 2, //開啟2個worker程式
'max_request' => 4, //每個worker程式 max_request設定為4次
'dispatch_mode' => 2, //資料包分發策略 - 固定模式
]);
$udp->on("Packet", [$this, 'onPacket']);
$this->serv->start();
}
public function onStart($serv) {
echo "#### onStart ####".PHP_EOL;
echo "SWOOLE ".SWOOLE_VERSION . " 服務已啟動".PHP_EOL;
echo "master_pid: {$serv->master_pid}".PHP_EOL;
echo "manager_pid: {$serv->manager_pid}".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onOpen($serv, $request) {
echo "#### onOpen ####".PHP_EOL;
echo "server: handshake success with fd{$request->fd}".PHP_EOL;
$serv->task([
'type' => 'login'
]);
echo "########".PHP_EOL.PHP_EOL;
}
public function onTask($serv, $task_id, $from_id, $data) {
echo "#### onTask ####".PHP_EOL;
echo "#{$serv->worker_id} onTask: [PID={$serv->worker_pid}]: task_id={$task_id}".PHP_EOL;
$msg = '';
switch ($data['type']) {
case 'login':
$msg = '我來了...';
break;
case 'speak':
$msg = $data['msg'];
break;
}
foreach ($serv->connections as $fd) {
$connectionInfo = $serv->connection_info($fd);
if (isset($connectionInfo['websocket_status']) && $connectionInfo['websocket_status'] == 3) {
$serv->push($fd, $msg); //長度最大不得超過2M
}
}
$serv->finish($data);
echo "########".PHP_EOL.PHP_EOL;
}
public function onFinish($serv,$task_id, $data) {
echo "#### onFinish ####".PHP_EOL;
echo "Task {$task_id} 已完成".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onClose($serv, $fd) {
echo "#### onClose ####".PHP_EOL;
echo "client {$fd} closed".PHP_EOL;
echo "########".PHP_EOL.PHP_EOL;
}
public function onMessage($serv, $frame) {
echo "#### onMessage ####".PHP_EOL;
echo "receive from fd{$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}".PHP_EOL;
$serv->task(['type' => 'speak', 'msg' => $frame->data]);
echo "########".PHP_EOL.PHP_EOL;
}
public function onRequest($request, $response) {
echo "#### onRequest ####".PHP_EOL;
$response->header("Content-Type", "text/html; charset=utf-8");
$server = $request->server;
$path_info = $server['path_info'];
$request_uri = $server['request_uri'];
echo "PATH_INFO:".$path_info.PHP_EOL;
if ($path_info == '/favicon.ico' || $request_uri == '/favicon.ico') {
return $response->end();
}
$html = "<h1>你好 Swoole.</h1>";
$response->end($html);
}
public function onReceive($serv, $fd, $from_id, $data) {
echo "#### onReceive ####".PHP_EOL;
$length = unpack('N', $data)[1];
echo "Length:".$length.PHP_EOL;
$msg = substr($data, -$length);
echo "Msg:".$msg.PHP_EOL;
}
public function onPacket($serv, $data, $clientInfo) {
echo "#### onPacket ####".PHP_EOL;
$serv->sendto($clientInfo['address'], $clientInfo['port'], "Server ".$data);
var_dump($clientInfo);
}
}
$server = new Server();
4 個客戶端連線的程式碼分別是:
1、9501 onMessage 處理 WebSocket。可以參考原來文章 Swoole WebSocket 的應用 中的程式碼即可。
2、9501 onRequest 處理 HTTP。可以參考原來文章 Swoole HTTP 的應用 中的程式碼即可。
3、9502 onReceive 處理 TCP。可以參考原來文章 Swoole Task 的應用 中的程式碼即可。
4、9503 onPacket 處理 UDP。
示例程式碼:
netcat -u 10.211.55.4 9503
小結
一、多埠的應用場景是什麼?
比如,開發一個直播網站,直播用一個埠,IM聊天用一個埠。
比如,開發一個RPC服務,資料通訊用一個埠,統計介面用一個埠。
本文歡迎轉發,轉發請註明作者和出處,謝謝!