实战swoole【聊天室】

栏目: PHP · 发布时间: 6年前

内容简介:前言:了解概念之后就应该练练手啦,不然就是巨婴有收获的话请官方示例

前言:了解概念之后就应该练练手啦,不然就是巨婴

有收获的话请 加颗小星星 ,没有收获的话可以 反对 没有帮助 举报 三连

准备工作

  • 需要先看初识swoole【上】,了解基本的服务端WebSocket使用
  • js WebSocket客户端简单使用

使用

# 命令行1
php src/websocket/run.php
# 命令行2
cd public && php -S localhost:8000
# 客户端,多开几个查看效果
访问http://localhost:8000/

复制代码

WebSocket

官方示例

$server = new swoole_websocket_server("0.0.0.0", 9501);
$server->on('open', function (swoole_websocket_server $server, $request) {
        echo "server: handshake success with fd{$request->fd}\n";
    });
$server->on('message', function (swoole_websocket_server $server, $frame) {
        echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
        $server->push($frame->fd, "this is server");
    });
$server->on('close', function ($ser, $fd) {
        echo "client {$fd} closed\n";
    });
$server->on('request', function (swoole_http_request $request, swoole_http_response $response) {
        global $server;//调用外部的server
        // $server->connections 遍历所有websocket连接用户的fd,给所有用户推送
        foreach ($server->connections as $fd) {
            $server->push($fd, $request->get['message']);
        }
    });
$server->start();
复制代码

详解:

  • swoole_websocket_server 继承自 swoole_http_server

    • 设置了onRequest回调,websocket服务器也可以同时作为http服务器
    • 未设置onRequest回调,websocket服务器收到http请求后会返回http 400错误页面
    • 如果想通过接收http触发所有websocket的推送,需要注意作用域的问题,面向过程请使用global对swoole_websocket_server进行引用,面向对象可以把swoole_websocket_server设置成一个成员属性
  • function onOpen(swoole_websocket_server $svr , swoole_http_request $req );

    • 当WebSocket客户端与服务器建立连接并完成握手后会回调此函数。
    • $req 是一个Http请求对象,包含了客户端发来的握手请求信息
    • onOpen事件函数中可以调用push向客户端发送数据或者调用close关闭连接
    • onOpen事件回调是可选的
  • function onMessage(swoole_websocket_server $server , swoole_websocket_frame $frame )

    • 当服务器收到来自客户端的数据帧时会回调此函数。
    • $frame 是swoole_websocket_frame对象,包含了客户端发来的数据帧信息
    • onMessage回调 必须被设置 ,未设置服务器将无法启动
    • 客户端发送的ping帧不会触发onMessage,底层会自动回复pong包
  • swoole_websocket_frame 属性

    • $frame->fd ,客户端的socket id,使用 $server->push 推送数据时需要用到
    • $frame->data ,数据内容,可以是文本内容也可以是二进制数据,可以通过opcode的值来判断
    • $frame->opcode ,WebSocket的OpCode类型,可以参考WebSocket协议标准文档
    • $frame->finish , 表示数据帧是否完整,一个WebSocket请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整)

聊天室服务端示例

目录结构:

  • config
    • socket.php
  • src
    • websocket
      • Config.php
      • run.php
      • WebSocketServer.php 内存表版本
      • WsRedisServer.php redis版本

WebSocketServer.php 内存表版本

<?php
namespace App\WebSocket;

class WebSocketServer
{
    private $config;
    private $table;
    private $server;

    public function __construct()
    {
        // 内存表 实现进程间共享数据,也可以使用 redis 替代
        $this->createTable();
        // 实例化配置
        $this->config = Config::getInstance();
    }

    public function run()
    {
        $this->server = new \swoole_websocket_server(
            $this->config['socket']['host'],
            $this->config['socket']['port']
        );

        $this->server->on('open', [$this, 'open']);
        $this->server->on('message', [$this, 'message']);
        $this->server->on('close', [$this, 'close']);

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

    public function open(\swoole_websocket_server $server, \swoole_http_request $request)
    {
        $user = [
            'fd' => $request->fd,
            'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd,
            'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])]
        ];
        // 放入内存表
        $this->table->set($request->fd, $user);

        $server->push($request->fd, json_encode(
                array_merge(['user' => $user], ['all' => $this->allUser()], ['type' => 'openSuccess'])
            )
        );
    }

    private function allUser()
    {
        $users = [];
        foreach ($this->table as $row) {
            $users[] = $row;
        }
        return $users;
    }

    public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
    {
        $this->pushMessage($server, $frame->data, 'message', $frame->fd);
    }

    /**
     * 推送消息
     *
     * @param \swoole_websocket_server $server
     * @param string $message
     * @param string $type
     * @param int $fd
     */
    private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd)
    {
        $message = htmlspecialchars($message);
        $datetime = date('Y-m-d H:i:s', time());
        $user = $this->table->get($fd);

        foreach ($this->table as $item) {
            // 自己不用发送
            if ($item['fd'] == $fd) {
                continue;
            }

            $server->push($item['fd'], json_encode([
                'type' => $type,
                'message' => $message,
                'datetime' => $datetime,
                'user' => $user
            ]));
        }
    }

    /**
     * 客户端关闭的时候
     *
     * @param \swoole_websocket_server $server
     * @param int $fd
     */
    public function close(\swoole_websocket_server $server, int $fd)
    {
        $user = $this->table->get($fd);
        $this->pushMessage($server, "{$user['name']}离开聊天室", 'close', $fd);
        $this->table->del($fd);
    }

    /**
     * 创建内存表
     */
    private function createTable()
    {
        $this->table = new \swoole_table(1024);
        $this->table->column('fd', \swoole_table::TYPE_INT);
        $this->table->column('name', \swoole_table::TYPE_STRING, 255);
        $this->table->column('avatar', \swoole_table::TYPE_STRING, 255);
        $this->table->create();
    }
}

复制代码

WsRedisServer.php redis版本

<?php
namespace App\WebSocket;

use Predis\Client;

/**
 * 使用redis代替table,并存储历史聊天记录
 *
 * Class WsRedisServer
 * @package App\WebSocket
 */
class WsRedisServer
{
    private $config;
    private $server;
    private $client;
    private $key = "socket:user";

    public function __construct()
    {
        // 实例化配置
        $this->config = Config::getInstance();
        // redis
        $this->initRedis();
        // 初始化,主要是服务端自己关闭不会清空redis
        foreach ($this->allUser() as $item) {
            $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']);
        }
    }

    public function run()
    {
        $this->server = new \swoole_websocket_server(
            $this->config['socket']['host'],
            $this->config['socket']['port']
        );

        $this->server->on('open', [$this, 'open']);
        $this->server->on('message', [$this, 'message']);
        $this->server->on('close', [$this, 'close']);

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

    public function open(\swoole_websocket_server $server, \swoole_http_request $request)
    {
        $user = [
            'fd' => $request->fd,
            'name' => $this->config['socket']['name'][array_rand($this->config['socket']['name'])] . $request->fd,
            'avatar' => $this->config['socket']['avatar'][array_rand($this->config['socket']['avatar'])]
        ];
        // 放入redis
        $this->client->hmset("{$this->key}:{$user['fd']}", $user);

        // 给每个人推送,包括自己
        foreach ($this->allUser() as $item) {
            $server->push($item['fd'], json_encode([
                'user' => $user,
                'all' => $this->allUser(),
                'type' => 'openSuccess'
            ]));
        }
    }

    private function allUser()
    {
        $users = [];
        $keys = $this->client->keys("{$this->key}:*");
        // 所有的key
        foreach ($keys as $k => $item) {
            $users[$k]['fd'] = $this->client->hget($item, 'fd');
            $users[$k]['name'] = $this->client->hget($item, 'name');
            $users[$k]['avatar'] = $this->client->hget($item, 'avatar');
        }
        return $users;
    }

    public function message(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
    {
        $this->pushMessage($server, $frame->data, 'message', $frame->fd);
    }

    /**
     * 推送消息
     *
     * @param \swoole_websocket_server $server
     * @param string $message
     * @param string $type
     * @param int $fd
     */
    private function pushMessage(\swoole_websocket_server $server, string $message, string $type, int $fd)
    {
        $message = htmlspecialchars($message);
        $datetime = date('Y-m-d H:i:s', time());
        $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd');
        $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name');
        $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar');

        foreach ($this->allUser() as $item) {
            // 自己不用发送
            if ($item['fd'] == $fd) {
                continue;
            }

            $is_push = $server->push($item['fd'], json_encode([
                'type' => $type,
                'message' => $message,
                'datetime' => $datetime,
                'user' => $user
            ]));
            // 删除失败的推送
            if (!$is_push) {
                $this->client->hdel("{$this->key}:{$item['fd']}", ['fd', 'name', 'avatar']);
            }
        }
    }

    /**
     * 客户端关闭的时候
     *
     * @param \swoole_websocket_server $server
     * @param int $fd
     */
    public function close(\swoole_websocket_server $server, int $fd)
    {
        $user['fd'] = $this->client->hget("{$this->key}:{$fd}", 'fd');
        $user['name'] = $this->client->hget("{$this->key}:{$fd}", 'name');
        $user['avatar'] = $this->client->hget("{$this->key}:{$fd}", 'avatar');
        $this->pushMessage($server, "{$user['name']}离开聊天室", 'close', $fd);
        $this->client->hdel("{$this->key}:{$fd}", ['fd', 'name', 'avatar']);
    }

    /**
     * 初始化redis
     */
    private function initRedis()
    {
        $this->client = new Client([
            'scheme' => $this->config['socket']['redis']['scheme'],
            'host' => $this->config['socket']['redis']['host'],
            'port' => $this->config['socket']['redis']['port'],
        ]);
    }
}
复制代码

config.php

<?php
namespace App\WebSocket;

class Config implements \ArrayAccess
{
    private $path;
    private $config;
    private static $instance;

    public function __construct()
    {
        $this->path = __DIR__ . '/../../config/';
    }

    // 单例模式
    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function offsetSet($offset, $value)
    {
        // 阉割
    }

    public function offsetGet($offset)
    {
        if (empty($this->config)) {
            $this->config[$offset] = require $this->path . $offset . ".php";
        }
        return $this->config[$offset];
    }

    public function offsetExists($offset)
    {
        return isset($this->config[$offset]);
    }

    public function offsetUnset($offset)
    {
        // 阉割
    }

    // 禁止克隆
    final private function __clone(){}
}
复制代码

config/socket.php

<?php
return [
    'host' => '0.0.0.0',
    'port' => 9501,

    'redis' => [
        'scheme' => 'tcp',
        'host' => '0.0.0.0',
        'port' => 6380
    ],

    'avatar' => [
        './images/avatar/1.jpg',
        './images/avatar/2.jpg',
        './images/avatar/3.jpg',
        './images/avatar/4.jpg',
        './images/avatar/5.jpg',
        './images/avatar/6.jpg'
    ],

    'name' => [
        '科比',
        '库里',
        'KD',
        'KG',
        '乔丹',
        '邓肯',
        '格林',
        '汤普森',
        '伊戈达拉',
        '麦迪',
        '艾弗森',
        '卡哇伊',
        '保罗'
    ]
];
复制代码

run.php

<?php
require __DIR__ . '/../bootstrap.php';

$server = new App\WebSocket\WebSocketServer();

$server->run();
复制代码

总结

完整示例: 聊天室

学完后发现生活中所谓的聊天室其实也不过如此,当然这只是简单的demo,很多功能都没有实现,想进一步学习的话可以去github上找完整的项目进行深入学习

参考


以上所述就是小编给大家介绍的《实战swoole【聊天室】》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

正则表达式必知必会

正则表达式必知必会

Ben Forta / 杨涛、王建桥、杨晓云 / 人民邮电出版社 / 2007 / 29.00元

正则表达式是一种威力无比强大的武器,几乎在所有的程序设计语言里和计算机平台上都可以用它来完成各种复杂的文本处理工作。本书从简单的文本匹配开始,循序渐进地介绍了很多复杂内容,其中包括回溯引用、条件性求值和前后查找,等等。每章都为读者准备了许多简明又实用的示例,有助于全面、系统、快速掌握正则表达式,并运用它们去解决实际问题。 本书适合各种语言和平台的开发人员。一起来看看 《正则表达式必知必会》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换