乐趣区

实战 swoole【聊天室】

前言:了解概念之后就应该练练手啦,不然就是巨婴
有收获的话请加颗小星星,没有收获的话可以 反对 没有帮助 举报三连

代码仓库
实战 swoole【聊天室】
在线体验

准备工作

需要先看初识 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
PHP + Swoole 实现的简单聊天室

退出移动版