Write the Code. Change the World.

4月 02

想做聊天这样的app或页面,就会使用到长连接。websocket 是一种方式。服务方可以用 nodejs 这些。对于 php,还是使用 workman 或 swoole 比较方便。这里就记录下 laravel 框架使用 swoole 的过程。

编译安装 swoole 扩展

http://pecl.php.net/package/swoole
当前,最新版本是 4.31。所有一切都是新的,服务器是新的,装的环境也是最新的。所以没有心理压力,都用最新的。

cd /usr/local/src

wget http://pecl.php.net/get/swoole-4.3.1.tgz

tar -xzvf swoole-4.3.1.tgz

cd swoole-4.3.1

phpize

# 对 ssl 的支持。这个还是比较重要的
./configure --enable-openssl 

make && make install

make test

vim /alidata/service/php/etc/php.ini

# 尾部追加
extension=swoole.so

# 重启 php
/etc/init.d/php-fpm restart

# 查看扩展
# php -m|grep swoole


上边步骤,仅仅针对环境中只装有一个 php 的。如果多 php, configure 的时候 ,还得带上 phpconfig 参数。

laravel 中的操作

  1. 创建 swoole.php 配置。 vim config/swoole.php
<?php

return [
    'host' => '0.0.0.0',
    'port' => 9508,
    'config' => [
        'worker_num' => 4,
        'daemonize' => 1,
        'max_request' => 1000,
        'dispatch_mode' => 2,
        'pid_file' => storage_path('swoole/socket.pid'),
        'ssl_cert_file' => storage_path('swoole/ssl/chat.kwva.cn.pem'),
        'ssl_key_file' => storage_path('swoole/ssl/chat.kwva.cn.key'),
        'log_file' => storage_path('swoole/swoole.log'),
    ]
];
  1. 创建 command。
php artisan make:command Swoole
  1. 再创建一个 handler 。
vim app\Qiubg\Handlers\SwooleWebsocketHandler.php

编写代码

服务端先要 new 一个 swoole_websocket_server 。

new swoole_websocket_server(config('swoole.host'), config('swoole.port'), SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);

连接信息存储,可以使用 swoole_table ,也可以使用 redis。具体怎么做,看你哈。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

use swoole_websocket_server;

class Swoole extends Command
{
    protected $signature = 'swoole:action {action}';

    protected $description = 'start or stop swoole';

    private $server;

    private $pid;

    public function __construct()
    {
        parent::__construct();

        $this->pid = storage_path('swoole/socket.pid');
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $arg = $this->argument('action');

        switch ($arg) {
            case 'start':
                $this->start();
            break;
            case 'stop':
                $this->stop();
            break;
            case 'reload':
                $this->reload();
            break;
            default:
                $this->info('no this command');
            break;
        }
    }

    private function start()
    {
        $pid = $this->getPid();
        if ($pid && \Swoole\Process::kill($pid,0)) {
            $this->info('progress already exist!');
            exit;
        }

        $this->server = new swoole_websocket_server(config('swoole.host'), config('swoole.port'),SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
        $this->server->set([
            'worker_num'=> 4, // 进程数
            'daemonize' => 1, // 启用守护进程
            'max_request'=> 1000, // work 进程最大任务数
            'dispatch_mode'=> 2, // 数据包分发策略,默认 2,固定模式
            'log_file' => storage_path('swoole/swoole.log'),
            'pid_file' => $this->pid,
            'ssl_cert_file' => storage_path('swoole/ssl/chat.kwva.cn.pem'),
            'ssl_key_file' => storage_path('swoole/ssl/chat.kwva.cn.key')
        ]);

        $handler = \App::make('Qiubg\Handlers\SwooleWebsocketHandler');

        $this->server->on('open', array($handler, 'onOpen'));

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

        $this->server->on('close', array($handler, 'onClose'));

        $this->info('progress created!');

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

    private function stop()
    {
        $pid = $this->getPid();
        if (!$pid) {
            $this->info('progress not exist!');
            exit;
        }

        $result = \Swoole\Process::kill((int)$pid);
        if ($result) {
            $this->info('process stop successfull!');
            exit;
        }
        $this->info('process stop failed!');
    }

    private function reload()
    {
        $pid = $this->getPid();
        if (!$pid) {
            $this->info('progress not exist!');
            exit;
        }

        $result = \Swoole\Process::kill((int)$pid);
        if ($result) {
            $this->info('process stop successfull!');
        }

        $this->start();
    }

    private function getPid()
    {
        return file_exists($this->pid) ? file_get_contents($this->pid) : false;
    }
}

handler

<?php

namespace Qiubg\Handlers;

use Log;
use swoole_table;

class SwooleWebsocketHandler
{
    private $table;

    public function __construct()
    {
        $this->table =  new \swoole_table(1024);
        $this->table->column('room_id', swoole_table::TYPE_INT);
        $this->table->column('fd', swoole_table::TYPE_INT);
        $this->table->column('user_id', swoole_table::TYPE_INT);
        $this->table->column('nickname', swoole_table::TYPE_STRING, 64);
        $this->table->column('avatar', swoole_table::TYPE_STRING, 255);
        $this->table->create();
    }

    public function onOpen($ws, $request)
    {

    }

    public function allUsers($room_id)
    {
        $users = [];
        foreach($this->table as $item) {
            if ($item['room_id'] == $room_id) {
                $users[] = $item;
            }
        }
        return $users;
    }

    public function onMessage($ws, $frame)
    {
        $data = json_decode($frame->data, true);
        switch($data['type']) {
            case 'connect':
                $user = [
                    'room_id' => $data['room_id'],
                    'fd' => $frame->fd,
                    'user_id' => $data['user_id'],
                    'nickname' => $data['nickname'],
                    'avatar' => $data['avatar']
                ];

                $this->table->set($frame->fd, $user);
                unset($user['fd']);

                $users = $this->allUsers($user['room_id']);
                foreach($this->table as $item) {
                    if ($item['user_id'] == $user['user_id']) {
                        $out = [
                            'users' => $users,
                            'user' => $user,
                            'type' => 'connect'
                        ];
                        $ws->push($item['fd'], json_encode($out));
                    } else {
                        if ($item['room_id'] == $data['room_id']) {
                            $out = [
                                'user' => $user,
                                'type' => 'connect'
                            ];
                            $ws->push($item['fd'], json_encode($out));
                        }
                    }
                }
                break;
            case 'message':
                $message = json_encode([
                    'type' => 'message',
                    'from' => [
                        'user_id' => $data['user_id'],
                        'nickname' => $data['nickname'],
                        'avatar' => $data['avatar']
                    ],
                    'message' => is_string($data['message']) ? nl2br($data['message']) : $data['message']
                ]);
                foreach($this->table as $item) {
                    if ($item['room_id'] == $data['room_id']) {
                        $ws->push($item['fd'], $message);
                    }
                }
                break;
            case 'single':
                $data = json_encode([
                    'type' => 'single',
                    'from' => [

                    ],
                    'to' => [

                    ],
                    'message' => is_string($data['message']) ? nl2br($data['message']) : $data['message']
                ]);
                break;
            case 'close':

                break;
        }
    }

    public function onClose($ws, $fd)
    {
        $user = $this->table->get($fd);

        if($user) {
            $this->table->del($fd);
            foreach($this->table as $item) {
                if ($item['room_id'] == $user['room_id']) {
                    $ws->push($item['fd'], json_encode(['type' => 'close', 'user' => $user]));
                }
            }
        }
    }
}

这里是服务端的 code 。

前端

ws = new WebSocket("wss://xxx.xxx.com:9508");

//发送房间号相关信息,以识别connect id
ws.onopen = function (evt) {
    var data = {
        room_id: room_id,
        user_id: user_id,
        nickname: nickname,
        avatar: avatar,
        type: 'connect'
    };
    console.log(data);
    ws.send(JSON.stringify(data));
};

// 当有消息时根据消息类型显示不同信息
ws.onmessage = function (evt) {
    var data = JSON.parse(evt.data);

    console.log(data.type, data, data.message);
    //判断是join还是message
    if (data.type == 'message') {

    }
}

ws.onclose = function () {

};

ws.onerror = function () {

};

启动

php artisan swoole:action start

php artisan swoole:action stop

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注