Swoole 多協議 多埠 的應用

新亮筆記發表於2019-05-07

概述

這是關於 Swoole 學習的第五篇文章:Swoole 多協議 多埠 的應用。

主要參考官方的這兩篇文章,進行實現的 Demo。

希望通過我提供的 Demo,能夠對文件有更加深刻的理解。

網路通訊協議設計

為什麼需要通訊協議?

官方:TCP協議在底層機制上解決了UDP協議的順序和丟包重傳問題。但相比UDP又帶來了新的問題,TCP協議是流式的,資料包沒有邊界。應用程式使用TCP通訊就會面臨這些難題。因為TCP通訊是流式的,在接收1個大資料包時,可能會被拆分成多個資料包傳送。多次Send底層也可能會合併成一次進行傳送。這裡就需要2個操作來解決:分包 和 合包,所以TCP網路通訊時需要設定通訊協議。

Swoole 支援了2種型別的自定義網路通訊協議 :EOF結束符協議、固定包頭+包體協議。

EOF結束符協議

Swoole 多協議 多埠 的應用

先看下,未設定協議的效果:

Swoole 多協議 多埠 的應用

傳送的每條資料長度都是 23,但在 onReceive 接收資料的時候每次接收的長度不一樣,並沒有按照想象的方式進行分包。

再看下,設定了EOF結束符協議的效果:

Swoole 多協議 多埠 的應用

傳送的每條資料長度都是 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();

固定包頭+包體協議

Swoole 多協議 多埠 的應用

先看下,未設定協議的效果:

Swoole 多協議 多埠 的應用

很明顯,在 onReceive 接收到的資料,是少的。

再看下,設定協議的效果:

Swoole 多協議 多埠 的應用

主要設定項如下:

'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();

多埠監聽的使用

Swoole 多協議 多埠 的應用

上圖,是示例程式碼中的埠監聽:

  • 9501 onMessage 處理 WebSocket。
  • 9501 onRequest 處理 HTTP。
  • 9502 onReceive 處理 TCP。
  • 9503 onPacket 處理 UDP。

不多說,看下效果圖:

Swoole 多協議 多埠 的應用

示例程式碼如下:

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服務,資料通訊用一個埠,統計介面用一個埠。

本文歡迎轉發,轉發請註明作者和出處,謝謝!

相關文章