laravel使用swoole的websocket

使用laravel的command来创建swoole进程

使用laravel的artisan命令生成swoole.php类

php artisan make:command swoole

修改swoole.php类的内容如下:



namespace App\Console\Commands;

use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument;
use App\Handlers\SwooleHandler;
class swoole extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'swoole:action {action}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'test swoole socket';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->fire();
    }

    public function fire(){
        $arg=$this->argument('action');
        switch($arg){
            case 'start':
                $this->info('swoole observer started');
                $this->start();
                break;
            case 'stop':
                $this->info('stoped');
                break;
            case 'restart':
                $this->info('restarted');
                break;
        }
    }

    public function start(){
        $this->serv=new \swoole_websocket_server('0.0.0.0',9501);
        $this->serv->set(
            array(
                'worker_num'=>4,
                'daemonize'=>1,
                'log_file'=>'/home/log/swoole.log',
                'max_request'=>100,
                'dispatch_mode'=>2,
                'debug_mode'=>1
            )
        );
        $handler=SwooleHandler::getInstance();
        $this->serv->on('open',array($handler,'onOpen'));
        $this->serv->on('workerstart',array($handler,'onWorkStart'));
        //$this->serv->on('Start',array($handler,'onStart'));
        //$this->serv->on('Connect',array($handler,'onConnect'));
        $this->serv->on('message',array($handler,'onMessage'));
        //$this->serv->on('Receive',array($handler,'onReceive'));
        $this->serv->on('close',array($handler,'onClose'));

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

    protected function getArguments(){
        return array(
            'action',InputArgument::REQUIRED,'start|stop|restart'
        );
    }
}

这里可以看到调用的swoole进程时候设置了几个方法,用来监听对应的事件。监听事件的方法使用了一个handler类来实现:

 $handler=SwooleHandler::getInstance();

这里先把handler类列出来


/**
 * Created by PhpStorm.
 * User: sunnier
 * Email:[email protected]
 * Date: 2018/7/18
 * Time: 10:37
 */

namespace App\Handlers;

use App\Models\OrderGoods;
use Illuminate\Support\Facades\Log;
use App\Models\StoreUser;
use Illuminate\Support\Facades\Redis;

class SwooleHandler
{

    private static $_instance;    //保存类实例的私有静态成员变量

    private $redis;

    public function __construct()
    {

    }

    //定义私有的__clone()方法,确保单例类不能被复制或克隆
    private function __clone() {}

    //对外提供获取唯一实例的方法
    public static function getInstance()
    {
        //检测类是否被实例化
        if ( ! (self::$_instance instanceof self) ) {
            self::$_instance = new SwooleHandler();
        }
        return self::$_instance;
    }

    public function onWorkStart($serv, $rq)
    {
        echo "Swoole onWorkStart ";
        $redis = Redis::connection();
        $serv->redis = $redis;
    }

    public function onOpen($serv, $rq)
    {
        echo "Swoole http server is started at http://0.0.0.0:9501\n";
    }

    public function onStart($serv)
    {
        Log::info('swoole start');
    }

    public function onConnect($serv, $fd, $from_id)
    {
        Log::info('swoole connect' . $from_id);
    }

    public function onMessage($serv, $frame)
    {
        //获取token和操作
        $this->handleByAction($serv, $frame);
        Log::info('swoole message' . json_encode($frame));
    }

    /**
     * 根据传来的字符串处理相应的内容
     * create by sunnier
     * Email:[email protected]
     * @param $server
     * @param $str
     */
    private function handleByAction($server, $frame)
    {
        //根据,拆分所传的字符串,判断用户行为
        $str=$this->decode($frame->data);
        $arr=explode('||:||',$str);
        if(is_array($arr)) {
            //第一个是token
            $token = addslashes(strval($arr[0]));
            if(!empty($arr[1])) {
                //第二个是行为
                $action = strval($arr[1]);
                switch ($action) {
                    case 'login':
                        $this->login($token, $server, $frame);
                        break;
                    case 'delete':
                        $this->delete($token, $arr[2], $server, $frame);
                        break;
                    case 'push':
                        $this->push($token, $arr[2], $server, $frame);
                        break;
                    default:
                }
                if (!empty($arr[2])) {
                    $server->push($frame->fd, $this->encode($arr[2]));
                }
            }else{
                $server->push($frame->fd, $this->encode($arr[0]));
            }
        }else{
            $server->push($frame->fd, $this->encode($frame->data));
        }
    }
    //ascii码转换为字符串
    private function decode($M){
        $bytes=explode(',',$M);
        $str = '';
        foreach($bytes as $ch) {
            $str .= chr($ch);
        }
        return $str;
    }

    /**
     * 字符串转换为ascii码
     * create by sunnier
     * Email:[email protected]
     * @param $message
     * @return string
     */
    private function encode($message){
        $bytes = array();
        for($i = 0; $i < strlen($message); $i++){
            $bytes[] = ord($message[$i]);
        }
        return implode(',',$bytes);
    }

    /**
     * 登录行为
     * create by sunnier
     * Email:[email protected]
     * @param $token
     * @param $server
     * @param $frame
     */
    private function login($token,$server,$frame){
        $user=StoreUser::where('token',$token)->first();
        if(!empty($user)) {
            $server->redis->set($token, $frame->fd);
            $server->push($frame->fd, $this->encode(json_encode(["连接成功!"])));
        }else{
            $server->push($frame->fd, $this->encode(json_encode(["连接失败,未找到用户!"])));
        }
    }

    /**
     * 删除内容行为
     * create by sunnier
     * Email:[email protected]
     * @param $token
     * @param $str
     * @param $server
     */
    private function delete($token,$str,$server,$frame){
        $user=StoreUser::where('token',$token)->first();
        if(!empty($user)) {
            //删除对应的内容
            $order_goods_id=intval($str);
            if(OrderGoods::where('id',$order_goods_id)->update(['has_finish' => 1])) {
                $server->push($frame->fd, $this->encode(json_encode(['action'=>'delete','id'=>$order_goods_id])));
            }else{
                $server->push($frame->fd, $this->encode(json_encode(["删除失败!"])));
            }
        }else{
            $server->push($frame->fd, $this->encode(json_encode(["删除失败,未找到用户!"])));
        }
    }

    /**
     * 推送行为
     * create by sunnier
     * Email:[email protected]
     * @param $token
     * @param $str
     * @param $server
     * @param $frame
     */
    private function push($token,$str,$server,$frame){
        $user=StoreUser::where('token',$token)->first();
        //查询是否存在对应用户
        if(!empty($user)) {
            $fd =  $server->redis->get($token);
            $server->push($fd,$this->encode($str));
        }else{
            $server->push($frame->fd, $this->encode(json_encode(["推送失败,未找到用户!"])));
        }
    }

    public function onClose($serv, $fd, $from_id)
    {
        Log::info('swoole close');
    }

    public function onReceive($serv, $fd, $from_id, $data)
    {
        Log::info(json_encode($data));
    }
}

着重介绍几个方法

1、workerstart方法

具体如下:

        $this->serv->on('workerstart',array($handler,'onWorkStart'));

实际上就是在workstart时调用了handler的onWorkStart方法,代码如下:

public function onWorkStart($serv, $rq)
    {
        echo "Swoole onWorkStart ";
        $redis = Redis::connection();
        $serv->redis = $redis;
    }

这里就是给建立的每个swoole进程设置单独的redis连接,因为官方有这段说明:
是否可以共用1个redis或mysql连接

2、onmessage方法(用来接收客户端发送的信息的方法)

        $this->serv->on('message',array($handler,'onMessage'));

这里对应了handler类中的onMessage方法,如上所示:

public function onMessage($serv, $frame)
    {
        //对接收的信息进行处理
        $this->handleByAction($serv, $frame);
        Log::info('swoole message' . json_encode($frame));
    }

3、说明:

swoole会给每个连接websocket的客户端一个id存放在$frame(如onMessge方法中的$frame)中,即:$frame->fd;这样就可以向对应的$frame->fd发送消息。

4、为了使得socket传输信息更加安全稳定(socket协议规定返回数据帧头部必须为数据大小的ascii码),所有数据传输都是用ascii码,如后端传输的两个转码解码方法:

//ascii码转换为字符串
private function decode($M){
        $bytes=explode(',',$M);
        $str = '';
        foreach($bytes as $ch) {
            $str .= chr($ch);
        }
        return $str;
    }

    /**
     * 字符串转换为ascii码
     * create by sunnier
     * Email:[email protected]
     * @param $message
     * @return string
     */
    private function encode($message){
        $bytes = array();
        for($i = 0; $i < strlen($message); $i++){
            $bytes[] = ord($message[$i]);
        }
        return implode(',',$bytes);
    }

前端js代码:

<script>
        //string转换ascii码
        function stringToByte(str) {
            var bytes = new Array();
            var len, c;
            len = str.length;
            for (var i = 0; i < len; i++) {
                c = str.charCodeAt(i);
                if (c >= 0x010000 && c <= 0x10FFFF) {
                    bytes.push(((c >> 18) & 0x07) | 0xF0);
                    bytes.push(((c >> 12) & 0x3F) | 0x80);
                    bytes.push(((c >> 6) & 0x3F) | 0x80);
                    bytes.push((c & 0x3F) | 0x80);
                } else if (c >= 0x000800 && c <= 0x00FFFF) {
                    bytes.push(((c >> 12) & 0x0F) | 0xE0);
                    bytes.push(((c >> 6) & 0x3F) | 0x80);
                    bytes.push((c & 0x3F) | 0x80);
                } else if (c >= 0x000080 && c <= 0x0007FF) {
                    bytes.push(((c >> 6) & 0x1F) | 0xC0);
                    bytes.push((c & 0x3F) | 0x80);
                } else {
                    bytes.push(c & 0xFF);
                }
            }
            return bytes;
        }
        //ascii码转换string
        function byteToString(arr) {
            if (typeof arr === 'string') {
                return arr;
            }
            var str = '',
                _arr = arr;
            for (var i = 0; i < _arr.length; i++) {
                var one = _arr[i].toString(2),
                    v = one.match(/^1+?(?=0)/);
                if (v && one.length == 8) {
                    var bytesLength = v[0].length;
                    var store = _arr[i].toString(2).slice(7 - bytesLength);
                    for (var st = 1; st < bytesLength; st++) {
                        store += _arr[st + i].toString(2).slice(2);
                    }
                    str += String.fromCharCode(parseInt(store, 2));
                    i += bytesLength - 1;
                } else {
                    str += String.fromCharCode(_arr[i]);
                }
            }
            return str;
        }
        $(function () {
            var exampleSocket = new WebSocket("ws://192.168.10.10:9501");
            //连接调用方法
            exampleSocket.onopen = function (event) {
                var str = stringToByte("login");
                //发送消息
                exampleSocket.send(str);
            };
            //接受消息
            exampleSocket.onmessage = function (event) {
                var toStr = byteToString(event.data.split(','));
                var res = $.parseJSON(toStr);
                console.log(res);
            }
        });
    script>

参考链接

你可能感兴趣的:(PHP,swoole,laravel)