Swoole协议

TCP是一个流式的协议,客户端向服务器发送一段数据后,可能并不会被服务器一次就完整的接收到。客户端向服务器发送多段数据,可能服务器一次就接收到了全部。在实际应用中,希望在服务器上能够一次接收一段完整的数据,不多也不少。

传统的TCP服务器中,往往需要由程序员维护一个缓存区,先将读取到数据写入缓存区,然后再通过预先设定好的协议内容来区分一段完整数据的开头、结尾和长度,并将一段完整的数据交给逻辑部分处理,这就是自定义协议的功能。

在Swoole中已经在底层实现了一个数据缓存区,并内置了几种常用的协议类型,并直接在底层做好了数据的拆分,以保证在onReceive回调函数中,一定能够收到一个或数个完整的数据段。

数据缓存区的大小可以通过配置pakcage_max_length来控制。

$configs = [];
$configs["package_max_length"] = 8192;
$server->set($configs);

Swoole目前支持两种通讯协议:EOF结束符协议、固定包头加包体协议

package_max_length

package_max_length用于设置最大数据包尺寸,当开启open_length_checkopen_eof_checkopen_http_protocol等协议解析后,Swoole底层会进程数据包拼接,此时在数据包未收取完整时,所有数据都将保存在内存中。所以需要设置package_max_length一个数据包最大允许占用的内存尺寸。

如果同时有1万个TCP连接在发送数据,每个数据包2MB,在最极端的情况下会占用20GB的内存空间。所以此参数不宜设置过大,否则会占用很大的内存。

相关配置选项

  • open_length_check

当发现数据包长度超过package_max_length时会直接丢弃此数据并关闭连接,因此不会占用任何内存,适用于websocketmqtthttp2协议。

  • open_eof_check

由于无法事先得知数据包的长度,所以接收到的数据还是会保存在内存中持续增长。当发现内存占用已经超过package_max_length时,将直接丢地此数据包并关闭连接。

  • open_http_protocol

HTTP的GET请求最大允许8KB数据且无法修改此配置,POST请求会检测Content-Type,如果发现超过package_max_length则直接丢地此数据,并发送HTTP 400错误并关闭连接。

EOF协议

  • 使用一组固定的、不会在正常数据内出现的字符串/r/n作为分割协议的标记,称之为EOF协议。

什么是EOF协议呢?

EOF全称 End of File,使用\r\n作为结束标记。

在逐个读取数据流中的数据时,如果发现读到EOF标记,就表示已经读到数据末尾。

在TCP的数据流中,使用EOF协议的数据流的特征是|数据|EOF|数据|EOF|

EOF协议处理的原理是在每串正常数据的末尾会添加一个预先规定的且绝对不会再数据中出现的字符串作为结束标记,这样接收到的数据就可以根据EOF标记来切分数据。

典型的memcached、ftp、stmp都是使用/r/n作为结束符。当发送数据时只要在数据包的末尾添加/r/n即可。

使用EOF协议处理一定要确保数据包中间不会出现EOF,否则将会造成分包错误。

使用EOF协议的数据流的格式

如何开启EOF协议支持呢?

Swoole中可使用配置选项来开启EOF功能

$configs = [];
// 开启EOF检测
$configs["open_eof_split"] = true;
// 设置EOF标记
$configs["package_eof"] = "/r/n";
// 设置服务器运行时参数
$server->set($config);
  • open_eof_split

open_eof_split会启用EOF自动分包,当设置open_eof_check选项后Swoole底层会检测数据是否以特定的字符串结尾来进行数据缓冲,默认只会截取接收到的数据末尾部分做对比,此时可能会产生多条数据合并在一个数据中。

EOF切割需要遍历整个数据包的内容查询\r\n结束标记,因此会消耗大量CPU资源。假设每个数据包为2MB,每秒10000个请求,则可能产生20GB条CPU字符匹配指令。

open_eof_split选项会开启Swoole底层对接收到的数据从头开始依次扫描检查,当找到第一个EOF标记时,将已经扫描过的数据作为一个完整的数据包通过onReceive回调函数发送给PHP层处理,这里需要注意的是package_eof只允许设置长度不超过8的字符串。

open_eof_split选项是依次扫描数据中的EOF标记的,虽然每次回调都只会收到一个完整的数据包,但性能较差。因此Swoole提供了另一种不同的选项open_eof_check

$configs = [];
// 开启EOF检测
$configs["open_eof_check"] = true;
$server->set($configs);

open_eof_check选项的结果为布尔值,必须为truefalse,传入其他类型的数值会被强制转换为布尔值。当启用open_eof_split参数后,Swoole底层会从数据包中间查找EOF结束标记并拆分数据包。服务器onReceive每次仅仅会收到一个以EOF字符串结尾的数据包。当启用open_eof_split参数后,无论open_eof_check是否启用都会生效。

  • package_eof

package_eof需要与open_eof_checkopen_eof_split配合使用,用来设置EOF字符串,常见如\r\n。需要注意的是package_eof最大只允许传入8个字节的字符串。

  • open_eof_check
$configs = [];
$configs["open_eof_check"] = true;
$server->set($configs);

open_eof_check用于打开EOF检测,开启后将检测客户端连接发送过来的数据,当数据包结尾是package_eof指定的字符串时才会投递给Worker工作进程,否则会一直拼接数据包,直到超过packge_max_length缓冲区或超时时才会中止。另外当出错时Swoole底层会认为是恶意连接丢弃数据并强制关闭连接。

open_eof_check同样会开启EOF检测,不同的是open_eof_check只会检查接收数据的末尾是否为EOF标记。相比较open_eof_split而言此种方式性能最好,几乎没有损耗。

但是如果同时收到多条带有EOF标记的数据,这种方式会同时将多条数据包合并为一个回调给PHP层处理,因此需要PHP层通过EOF标记对数据做二次拆分。

open_eof_check选项值是布尔型的truefalse,当传入其他类型的数值时会被强制转换为布尔值。此配置仅仅对STREAM类型的Socket有效,如TCP、UnixSocketStream。例如常见的Memcache\SMTP\POP等协议都是以\r\n作为结束标记,因此可以使用此配置。当配置开启后可以保证Worker工作进程一次性收到一个或多个完整的数据包。

open_eof_checkEOF检测不会从数据中查找EOF结束标记字符串,所以Worker工作进程可能会同时收到多个数据包,因此需要在应用层代码中自行拆包。

$recv = $client->recv();
if(!empty($recv)){
  $arr = explode("\r\n", $recv);
}

Swoole1.7.15+版本中新增open_eof_split配置项,支持从数据中查找EOF结束标记字符串并切分数据。

open_eof_splitopen_eof_check之间的差异是什么呢?

  • open_eof_check只会检查接收数据的末尾是否为EOF,因此性能最好几乎没有小号。
  • open_eof_check无法解决多个数据包合并的问题,比如同时发送两条带有EOF的数据,Swoole底层可能会一次性全部返回。
  • open_eof_split会从左到右对数据进行逐个字节比对,查找数据中的EOF并进行分包,因此性能较差,而且每次只会返回一个数据包。

实例:面向过程方式

服务器

$ vim server.php
<?php
//创建异步服务器对象,并监听对应主机的端口。
$host = "0.0.0.0";
$port = 9501;
$mode = SWOOLE_PROCESS;
$type = SWOOLE_SOCK_TCP;
$server = new swoole_server($host, $port, $mode, $type);
//设置服务器运行时参数
$configs = [];
$configs["worker_num"] = 1;
$configs["daemonize"] = false;

$configs["package_max_length"] = 8192;
$configs["open_eof_check"] = true;
$configs["open_eof_split"] = true;
$configs["package_eof"] = "\r\n";
$server->set($configs);


$server->on("Start", function(swoole_server $server){
    echo "[start] master {$server->master_pid} manager {$server->manager_pid}".PHP_EOL;
});
$server->on("Shutdown", function(swoole_server $server){
    echo "[shutdown]".PHP_EOL;
});
//注册监听客户端连接进入事件
$server->on("Connect", function(swoole_server $server, $fd){
    echo "[connect] client {$fd}".PHP_EOL;
});
//注册监听接收客户端消息事件
$server->on("Receive", function(swoole_server $server, $fd, $reactor_id, $data){
    echo "[receive] {$data}".PHP_EOL;

    $message = "success\r\n";
    $server->send($fd, $message);
});
//注册监听客户端连接关闭事件
$server->on("Close", function(swoole_server $server, $fd){
    echo "[close] client {$fd}".PHP_EOL;
});
// 启动服务器
$server->start();

客户端

$ vim client.php
<?php
//创建同步阻塞客户端对象
$client = new swoole_client(SWOOLE_SOCK_TCP);
//设置客户端运行参数
$configs = [];
$configs["open_eof_check"] = true;
$configs["open_eof_split"] = true;
$configs["package_eof"] = "\r\n";
$client->set($configs);
//连接服务器
$host = "127.0.0.1";
$port = 9501;
$timeout = 1;
$result = $client->connect($host, $port, $timeout);
if(!$result){
    die("connect failed".PHP_EOL);
}
//向服务器发送数据,注意发送数据必须具有EOF标记,否则不会响应。
$message = "hello";
//$message = "world\r\n";
$length = $client->send($message);//发送成功则返回消息长度
echo $length.PHP_EOL;
if(!$length){
    die("send failed".PHP_EOL);
}
//接收从服务器发送的数据
$result = $client->recv();
if(!$result){
    die("recv failed".PHP_EOL);
}
echo $result;

//关闭客户端连接
$client->close();

运行测试

$ php server.php
[start] master 4109 manager 4110
[connect] client 1
[receive] world

$ php client.php
5
PHP Warning:  Swoole\Client::recv(): recv() failed. Error: Resource temporarily unavailable [11] in /home/jc/projects/swoole/eof/client.php on line 27
recv failed

注意:在客户端send消息时并没有添加EOF结束标记\r\n导致客户端报错

PHP Warning:  Swoole\Client::recv(): recv() failed. Error: Resource temporarily unavailable [11] in /home/jc/projects/swoole/eof/client.php on line 27
recv failed

实例:面向对象方式

服务器

$ vim server.php
<?php
/**服务器 */
class Server
{
    private $server;
    private $eof = "\r\n";
    /**构造方法 */
    public function __construct($host = "0.0.0.0", $port = 9501, $configs = [])
    {
        try{
            //创建异步非阻塞多进程的TCP服务器
            $mode = SWOOLE_PROCESS;//多进程
            $type = SWOOLE_SOCK_TCP;//TCP服务器
            $this->server = new swoole_server($host, $port, $mode, $type);
        }catch(Exception $ex){
            $message = $ex->getMessage();
            //监听端口失败会抛出异常
            $this->debug("[constructor] listen port {$port} failed, {$message}");
        }
        $this->server->set($configs);

        $this->server->on("Start", [$this, "onStart"]);
        $this->server->on("Connect", [$this, "onConnect"]);
        $this->server->on("Receive", [$this, "onReceive"]);
        $this->server->on("Close", [$this, "onClose"]);
        $this->server->on("Task", [$this, "onTask"]);
        $this->server->on("Finish", [$this, "onFinish"]);

        $this->server->start();
    }

    public function onStart(swoole_server $server)
    {
        $this->debug("[start] master {$server->master_pid} manager {$server->manager_pid}");
    }
    public function onConnect(swoole_server $server, $fd, $reactor_id)
    {
        $this->debug("[connect] reactor {$reactor_id} client {$fd}");
    }
    public function onReceive(swoole_server $server, $fd, $reactor_id, $data)
    {
        $this->debug(PHP_EOL."[receive] client {$fd} message : {$data}");

        $params = [];
        $params["fd"] = $fd;
        $params["data"] = $data;
        $message = json_encode($params);
        $this->server->task($message);

        $this->debug("[receive] contine handle worker");
    }
    public function onTask(swoole_server $server, $task_id, $worker_id, $message)
    {
        $this->debug("[task] worker {$worker_id} task {$task_id} message: {$message}");

        $params = json_decode($message, true);
        $fd = $params["fd"];
        $data = $params["data"];

        if(strpos($this->eof, $data) > 0){
            $list = explode($this->eof, $data);
            foreach($list as $item){
                if(empty($item)){
                    continue;
                }
                $this->send($fd, $item.$this->eof);
            }
        }

        return "success";
    }
    public function onFinish(swoole_server $server, $task_id, $data)
    {
        $this->debug("[finish] task {$task_id} data:{$data}");
    }
    public function send($fd, $data, $server_socket = -1)
    {
        return $this->server->send($fd, $data, $server_socket);
    }
    public function onClose(swoole_server $server, $fd, $reactor_id)
    {
        $this->debug("[close] reactor {$reactor_id} client {$fd}");
    }
    public function debug($message)
    {
        echo $message.PHP_EOL;
    }
}

$host = "0.0.0.0";
$port = 9501;

$configs = [];
$configs["worker_num"] = 1;
$configs["task_worker_num"] = 2;
$configs["daemonize"] = false;
$configs["max_request"] = 10000;
$configs["dispatch_mode"] = 2;
$configs["open_eof_check"] = true;
$configs["open_eof_split"] = true;
$configs["package_max_length"] = 8192;
$configs["package_eof"] = "\r\n";

$server = new Server($host, $port, $configs);

客户端

$ vim client.php
<?php
/**客户端 */
class Client
{
    private $client;
    private $eof = "\r\n";
    /**构造方法 */
    public function __construct($host = "127.0.0.1", $port = 9501, $configs = [])
    {
        //创建异步非阻塞TCP客户端
        $socket_type = SWOOLE_SOCK_TCP;
        $is_sync = SWOOLE_SOCK_ASYNC;
        $this->client = new swoole_client($socket_type, $is_sync);
        //设置客户端参数,必须在connect前执行。
        $this->client->set($configs);
        //注册异步事件回调函数
        $this->client->on("Connect", [$this, "onConnect"]);
        $this->client->on("Receive", [$this, "onReceive"]);
        $this->client->on("Close", [$this, "onClose"]);
        $this->client->on("Error", [$this, "onError"]);
        //连接远程服务器并监听指定主机的端口
        $this->connect($host, $port);
    }
    public function debug($message)
    {
        echo $message.PHP_EOL;
    }
    public function connect($host, $port)
    {
        //异步模式下connect会立即返回true但实际连接并未建立,因此不能在connect之后使用send方法。
        //当连接创建成功后系统会自动调用onConnect方法,此时才可以使用send向服务器发送消息。
        $fp = $this->client->connect($host, $port);
        if(!$fp){
            $this->debug("Error {$fp->errCode}: {$fp->errMsg}");
            return;
        }
    }
    public function onConnect(swoole_client $client)
    {
        if($this->isConnected()){
            $this->debug("[connect]");
            $message = "hello".$this->eof."world".$this->eof;
            $count = 0;
            $max = 10;
            while(true){
                if($count >= $max){
                    break;
                }
                $this->debug("[send] {$count}:$message");
                $this->send($message);
                $count++;
            }
        }
    }

    public function onReceive(swoole_client $client, $data)
    {
        $data = $this->client->recv();
        echo "[receive1] {$data}".PHP_EOL;
        $this->debug("[receive] {$data}");
    }

    public function onClose(swoole_client $client)
    {
        $this->debug("[close]");
    }

    public function onError(swoole_client $client)
    {
        $this->debug("[error]");
    }

    public function send($message)
    {
        $this->client->send($message);
    }

    public function isConnected()
    {
        return $this->client->isConnected();
    }
}

$host = "127.0.0.1";
$port = 9501;
$configs = [];
$configs["open_eof_check"] = true;
$configs["open_eof_split"] = true;
$configs["package_eof"] = "\r\n";
$configs["package_max_length"] = 8192;
$client = new Client($host, $port);

运行测试

$ php server.php
[start] master 5702 manager 5703
[connect] reactor 0 client 1

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 0 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 1 message: {"fd":1,"data":"world\r\n"}
[finish] task 0 data:success
[finish] task 1 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 2 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 3 message: {"fd":1,"data":"world\r\n"}
[finish] task 2 data:success
[finish] task 3 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 4 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 5 message: {"fd":1,"data":"world\r\n"}
[finish] task 4 data:success
[finish] task 5 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 6 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 7 message: {"fd":1,"data":"world\r\n"}
[finish] task 6 data:success
[finish] task 7 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 8 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 9 message: {"fd":1,"data":"world\r\n"}
[finish] task 8 data:success
[finish] task 9 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 10 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 11 message: {"fd":1,"data":"world\r\n"}
[finish] task 10 data:success
[finish] task 11 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 12 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 13 message: {"fd":1,"data":"world\r\n"}
[finish] task 12 data:success
[finish] task 13 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 14 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 15 message: {"fd":1,"data":"world\r\n"}
[finish] task 14 data:success
[finish] task 15 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 16 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 17 message: {"fd":1,"data":"world\r\n"}
[finish] task 16 data:success
[finish] task 17 data:success

[receive] client 1 message : hello

[receive] contine handle worker
[task] worker 0 task 18 message: {"fd":1,"data":"hello\r\n"}

[receive] client 1 message : world

[receive] contine handle worker
[task] worker 0 task 19 message: {"fd":1,"data":"world\r\n"}
[finish] task 18 data:success
[finish] task 19 data:success


$ php client.php
[connect]
[send] 0:hello
world

[send] 1:hello
world

[send] 2:hello
world

[send] 3:hello
world

[send] 4:hello
world

[send] 5:hello
world

[send] 6:hello
world

[send] 7:hello
world

[send] 8:hello
world

[send] 9:hello
world

固定包头协议

  • 在收据首部添加一组固定格式的数据作为协议头,称之为固定包头协议。
  • 协议头的格式必须固定,并且其中需要标明后续数据的长度Length
  • 长度字段Length的格式只支持S,L,N,Vs,l,n,v
固定包头协议

网络通信过程中可能会出现分包与合包的情况,这时就需要使用到固定包头协议。固定包头协议是在实际应用中最为常见的协议,协议规定一个固定长度的包头,在包头的固定位置有一个指定好的字段用于存放后续数据的实际长度。这样服务器可以先读取固定长度的数据,并从中提取出长度,然后再读取指定长度的数据,即可获得一段完整的数据。在Swoole中提供了固定包头的协议格式,不过需要注意的是,Swoole之允许二进制形式的包头,因此需要使用PHP的packunpack用来打包和解包。

//解包封包
function packdata($data, $package_length_type)
{
    return pack($package_length_type, strlen($data)).$data;
}
function unpackdata($data, $package_length_type)
{
    $length = $package_length_type=="N" ? 4 : 2;
    return substr($data, $length);
}

固定包头协议非常通用,在BAT的服务器程序中经常能够看到,这种协议的特点是一个数据包总是由包头和包体两部分组成,包头由一个字段指定包体或整个包的长度,长度一般是使用2字节或4字节来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据后得到完整数据包。Swoole的配置可以很好的支持这种协议,也可以灵活地配置参数来应对所有情况。

$package_length_type = "N";
$package_body_offset = $package_length_type=="N" ? 4 : 2;

Swoole的服务器和异步客户端都是在onReceive回调函数中处理数据包,当设置了协议处理之后,只有当收到一个完整的数据包时才会触发onReceive事件。

$server->on("receive", function(swoole_server $server, $fd, $reactor_id, $data) use($package_length_type){
    $worker_id = $server->worker_id;
    $length = strlen($data);
    debug("[receive] worker:{$worker_id} length:{$length} data:{$data}");

    $recv = unpackdata($data, $package_length_type);
    debug("[receive] recv:{$recv}");

    $send = packdata($recv, $package_length_type);
    debug("[receive] send:{$send}");
    $server->send($fd, $send);
});

当同步客户端在设置了协议处理后,调用$client->recv()不再需要传入长度,recv函数在收到完整的数据包或发生错误后返回。

$result = $client->recv();
if(!$result)
{
    exit("recv failed");
}
$data = unpackdata($result, $package_length_type);
debug("[receive] {$data}");

固定包头协议是在需要发送的数据前添加一段预先约定好的长度和格式的数据体,在该数据体中存放着有序数据的相应信息,一般情况下后续数据的长度字段是必填项,这样一段数据体称之为协议包头。在TCP的数据流中使用固定包头协议的数据流特征是|length长度|数据|length长度|数据|

开启协议检测

在Swoole中使用配置选项来开启固定包头的协议功能,通过设置open_length_check选项可打开固定包头协议解析功能,可使用package_length_offsetpackage_body_offsetpackage_length_type三个配置控制解析功能。package_length_offset规定了包头中第几个字节开始是长度字段,package_body_offset规定了包头的长度,package_length_type规定了长度字段的类型。

$configs = [];
//设置是否开启协议解析
$configs["open_length_check"] = true;
// 设置数据包缓存区大小,若缓存数据超过该值则引发错误,具体错误处理由开启的协议解析类型所决定。
$configs["package_max_length"]  = 81920;
// 指定包长字段的类型
$configs["package_length_type"] = 'N';
// 设置从包头中第几个字节开始存放长度字段
$configs["package_length_offset"] = 0;
// 设置从第几个字节开始计算长度
$configs["package_body_offset"] = 4;
$server->set($configs)

package_length_type中规定了length长度字段的类型,这个类型等价于使用PHP的pack函数打包数据时所使用的类型,具体类型如下:

  • c 1字节范围从-128到127,表示有符号的Char字符。
  • C 1字节范围从0到255,表示无符号的Char字符。
  • s 2字节范围从-32768到32767,表示有符号的机器字节序。
  • S 2字节范围从0到65535,表示无符号的机器字节序。
  • n 2字节范围从0到65535,表示无符号的网络字节序。
  • N 4字节范围从0到4294967295,表示无符号的网络字节序。
  • l 4字节范围从-2147483648到2147483648,表示有符号的机器字节序。
  • L 4字节范围从0到4294967295,表示无符号的机器字节序。
  • v 2字节范围从0到65535,表示无符号的小端字节序。
  • V4字节范围从0到4294967295,表示无符号的小端字节序。

字节序

在内存中一个整数由四个连续的字节序列表示,例如一个int类型的变量i其内存地址为0x100,此变量的四个字节会被存储在0x1000x1010x1020x103
字节序决定了变量在内存中四个地址的存放顺序,通过会根据字节排序顺序的不同,将字节序区分为大端字节序和小端字节序。
假设变量i的16进制表示为0x12345678,在大端字节序中变量将会以下形式存放在内存。

... 0x100   0x101   0x102   0x103   ...
... 12  34  56  78  ...

在小端字节序中,则会以以下形式存放在内存中。

... 0x100   0x101   0x102   0x103   ...
... 78  56  34  12  ...

可以发现,以内存地址由小到大代表从低到高,将变量从左到右设定为从低到高,大端序列是高位字节存放在高位,低位字节存放在低位,小端则反之。

在机器中使用大端还是小端完全取决于硬件本身,因此不同机器之间通信时,需要使用一种统一地字节序来进行传递,由此诞生了网络字节序的概念。网络字节序一般采用大端序由IP协议所指定,而机器字节序则由机器本身所决定。两者之间需要借助于系统函数调用进行转换。

实例:面向过程

服务器

$ vim server.php
<?php
//解包封包
function packdata($data, $package_length_type)
{
    return pack($package_length_type, strlen($data)).$data;
}
function unpackdata($data, $package_length_type)
{
    $length = $package_length_type=="N" ? 4 : 2;
    return substr($data, $length);
}
//日志输出
function debug($message)
{
    echo $message.PHP_EOL;
}

$host = "0.0.0.0";
$port = 9501;
$server = new swoole_server($host, $port);

$package_length_type = "N";
$package_body_offset = $package_length_type=="N"?4:2;

$configs = [];
$configs["open_length_check"] = true;
$configs["package_max_length"] = 2000000;
$configs["package_length_offset"] = 0;
$configs["pakcage_length_type"] = $package_length_type;
$configs["package_body_offset"] = $package_body_offset;
$server->set($configs);

$server->on("connect", function(swoole_server $server, $fd){
    debug("[connect] client {$fd}");
});

$server->on("close", function(swoole_server $server, $fd){
    debug("[close] client {$fd}");
});

$server->on("receive", function(swoole_server $server, $fd, $reactor_id, $data) use($package_length_type){
    $worker_id = $server->worker_id;
    $length = strlen($data);
    debug("[receive] worker:{$worker_id} length:{$length} data:{$data}");

    $recv = unpackdata($data, $package_length_type);
    debug("[receive] recv:{$recv}");

    $send = packdata($recv, $package_length_type);
    debug("[receive] send:{$send}");
    $server->send($fd, $send);
});

$server->start();

客户端

$ vim client.php
<?php
//解包封包
function packdata($data, $package_length_type)
{
    return pack($package_length_type, strlen($data)).$data;
}
function unpackdata($data, $package_length_type)
{
    $length = $package_length_type=="N" ? 4 : 2;
    return substr($data, $length);
}
//日志输出
function debug($message)
{
    echo $message.PHP_EOL;
}

$client = new swoole_client(SWOOLE_SOCK_TCP);

$package_length_type = "N";
$package_body_offset = $package_length_type=="N" ? 4 : 2;

$configs = [];
$configs["open_length_check"] = true;
$configs["package_length_type"] = $package_length_type;
$configs["package_length_offset"] = 0;
$configs["package_body_offset"] = $package_body_offset;
$configs["package_max_length"] = 2000000;
$client->set($configs);

$host = "127.0.0.1";
$port = 9501;
$bool = $client->connect($host, $port);
if(!$bool)
{
    exit("connect failed");
}

$data = "hello world";
$data = packdata($data, $package_length_type);
$length = $client->send($data);
if(!$length)
{
    $errcode = $client->errCode;
    exit("send failed");
}

$result = $client->recv();
if(!$result)
{
    exit("recv failed");
}
$data = unpackdata($result, $package_length_type);
debug("[receive] {$data}");

$client->close();

运行测试

$ php server.php
[connect] client 1
[receive] worker:0 length:15 data:
                                  hello world
[receive] recv:hello world
[receive] send:
               hello world
[close] client 1

$ php client.php
[receive] hello world
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 4,863评论 0 9
  • PHP常用socket创建TCP连接,使用CURL创建HTTP连接,为了简化操作,Swoole提供了Client类...
    JunChow520阅读 3,773评论 0 3
  • date: 2019-05-02 20:33:01title: swoole| swoole wiki 笔记 初心...
    daydaygo阅读 3,830评论 2 12
  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    JasonShi6306421阅读 1,204评论 0 1
  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    保川阅读 5,903评论 1 13