UDP数据包是有界的,不存在数据包边界确定的问题,但TCP数据包是无界的,如果使用的自定义格式数据包,就需要自定义协议,一般来说,自定义协议主要有两个工作:

Parser

完成数据包的解析

Handler

网络事件处理

以一个简单的echo server为例子说明整个过程:

  1. 客户端连接后,即可发送文本内容,文本内容以CRLF结尾,服务器收到数据包后,向所有用户广播当前用户输入的内容。
  2. 客户端发送字符串quit表示断开连接,服务器将通知事件发送给其它客户端。
  3. 单个数据包如果超过64KB, 给客户端发送错误信息。

这部分代码已经包含在源码包中:

vendor/beyoio/beyoio/protocol/text/Parser.php中的内容:

<?php
namespace beyod\protocol\text;

use beyod\Connection;

class Parser extends \beyod\Parser 
{  
    //数据包结尾定界符
    public $delimiter = "\r\n";
    
    public $max_packet_size = 65536; //设置数据包最大长度
    
    /** 
     * @param string $buffer
     * @param Connection $connection 当前连接,对udp协议此值为null
     * @return int
     */
    public function input($buffer, $connection){
        
        //调用父类方法,检测数据包是否过大
        $len = parent::input($buffer, $connection);
        
        //如果数据中没有CRLF, 返回0,说明完整数据未收到,继续等待下一次数据段到达。
        $pos = strpos($buffer, $this->delimiter);
        if ($pos === false) {
            return 0;
        }
        
        //包含CRLF, 说明已经收到完整数据包,返回整个数据包的长度。
        return $pos + 2;
    }
    
    /**
    * 一旦收到数据包后,可以解码处理,因为对我们来说,是不必要关心的,所以清理后返回给handler
    */
    public function decode($buffer, $connection){
        return trim($buffer);
    }
    
    //向客户端发送文本时,自动追加边界符。
    public function encode($buffer, $connection){
        return $buffer.$this->delimiter;
    }
}

vendor/beyoio/beyoio/protocol/text/Handler.php

<?php

namespace beyod\protocol\text;

use Yii;
use yii\base\Event;
use beyod\IOEvent;
use beyod\MessageEvent;
use beyod\CloseEvent;
use beyod\ErrorEvent;

class Handler extends \beyoio\Handler
{
    public $tag = null;
    
    public function init()
    {
        $this->tag = "Beyod Text Echo Server 1.0.1"
            ."\r\nServer:\t".Yii::$app->server->server_token
            ."\r\nServer Start At:\t".date('Y-m-d H:i:s')
            ."\r\nGPID:\t".Yii::$app->server->getGPID()
            . "\r\n";
    }
    
    
    /**
     * @param  IOEvent $event
     * {@inheritDoc}
     * @see \beyod\Handler::onConnect()
     */
    public function onConnect(IOEvent $event)
    {
        //连接后向当前客户端发送提示信息
        $resp = $this->tag."your connection id ".$event->sender."\r\nPlease input message, quit to disconnect:\r\n";
        $event->sender->send($resp);
        
        //向其它连接正常的客户端发送上线通知
        foreach($event->sender->listenner->connections as $id => $conn){
            if($id == $event->sender->id || $conn->isClosed() ) continue;
            $conn->send($conn." connected");
        }
    }
    
    /**
     * 当收到数据包时的处理
     * @param  MessageEvent $event
     */
    public function onMessage(MessageEvent $event)
    {
        //如果收到quit,向其它客户端发送上线通知
        if($event->message == 'quit'){            
            foreach($event->sender->listenner->connections as $id => $conn){
                if($id == $event->sender->id || $conn->isClosed() ) continue;
                $conn->send($conn.' quit !');
            }
            
            //向当前客户端发送消息并断开连接
            return $event->sender->close('bye bye !');
        }
        
        //向其它客户端发送消息内容,实现消息群聊
        foreach($event->sender->listenner->connections as $id => $conn){
            if($id == $event->sender->id || $conn->isClosed() ) continue;
            $conn->send($conn.": ".$event->message);
        }
        
        //当前客户端的提示
        $event->sender->send("you said: ".$event->message);
    }
    
    //当连接断开时,向其它客户端发送下线通知
    public function onClose(CloseEvent $event){
        foreach($event->sender->listenner->connections as $id => $conn){
            if($id == $event->sender->id || $conn->isClosed() ) continue;
            $conn->send($conn." disconnected");
        }
    }
    
    //当数据包解析错误时,向客户端发送错误消息(并未断开连接)
    public function onBadPacket(ErrorEvent $event)
    {
        $event->sender->send($event->errstr);
    }
}

config/main.php中的配置此服务功能:

'components' =>[
    'server' => [
        //...
        'listeners' => [
            'text' => [
                    'class' => 'beyod\Listener',
                    'listen' => 'tcp://0.0.0.0:7723',
                    'parser' =>  'beyod\protocol\text\Parser',
                    'handler' => 'beyod\protocol\text\Handler'
                ],
            //其它服务协议
        ]
    ]
]

同时打开多个telnet,连接至7723端口即可测试。