初始化
This commit is contained in:
15
src/websocket/Event.php
Normal file
15
src/websocket/Event.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
class Event
|
||||
{
|
||||
public $type;
|
||||
public $data;
|
||||
|
||||
public function __construct($type, $data = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
10
src/websocket/Frame.php
Normal file
10
src/websocket/Frame.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
class Frame
|
||||
{
|
||||
public function __construct(public int $fd, public string $data)
|
||||
{
|
||||
}
|
||||
}
|
||||
71
src/websocket/Handler.php
Normal file
71
src/websocket/Handler.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
use think\Event;
|
||||
use think\Request;
|
||||
use think\worker\contract\websocket\HandlerInterface;
|
||||
use think\worker\websocket\Event as WsEvent;
|
||||
|
||||
class Handler implements HandlerInterface
|
||||
{
|
||||
protected $event;
|
||||
|
||||
public function __construct(Event $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* "onOpen" listener.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onOpen(Request $request)
|
||||
{
|
||||
$this->event->trigger('worker.websocket.Open', $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* "onMessage" listener.
|
||||
*
|
||||
* @param Frame $frame
|
||||
*/
|
||||
public function onMessage(Frame $frame)
|
||||
{
|
||||
$this->event->trigger('worker.websocket.Message', $frame);
|
||||
|
||||
$event = $this->decode($frame->data);
|
||||
if ($event) {
|
||||
$this->event->trigger('worker.websocket.Event', $event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "onClose" listener.
|
||||
*/
|
||||
public function onClose()
|
||||
{
|
||||
$this->event->trigger('worker.websocket.Close');
|
||||
}
|
||||
|
||||
protected function decode($payload)
|
||||
{
|
||||
$data = json_decode($payload, true);
|
||||
if (!empty($data['type'])) {
|
||||
return new WsEvent($data['type'], $data['data'] ?? null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function encodeMessage($message)
|
||||
{
|
||||
if ($message instanceof WsEvent) {
|
||||
return json_encode([
|
||||
'type' => $message->type,
|
||||
'data' => $message->data,
|
||||
]);
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
67
src/websocket/Pusher.php
Normal file
67
src/websocket/Pusher.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
use think\worker\Manager;
|
||||
use think\worker\message\PushMessage;
|
||||
|
||||
/**
|
||||
* Class Pusher
|
||||
*/
|
||||
class Pusher
|
||||
{
|
||||
|
||||
/** @var Room */
|
||||
protected $room;
|
||||
|
||||
/** @var Manager */
|
||||
protected $manager;
|
||||
|
||||
protected $to = [];
|
||||
|
||||
public function __construct(Manager $manager, Room $room)
|
||||
{
|
||||
$this->manager = $manager;
|
||||
$this->room = $room;
|
||||
}
|
||||
|
||||
public function to(...$values)
|
||||
{
|
||||
foreach ($values as $value) {
|
||||
if (is_array($value)) {
|
||||
$this->to(...$value);
|
||||
} elseif (!in_array($value, $this->to)) {
|
||||
$this->to[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push message to related descriptors
|
||||
* @param $data
|
||||
* @return void
|
||||
*/
|
||||
public function push($data): void
|
||||
{
|
||||
$fds = [];
|
||||
|
||||
foreach ($this->to as $room) {
|
||||
$clients = $this->room->getClients($room);
|
||||
if (!empty($clients)) {
|
||||
$fds = array_merge($fds, $clients);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_unique($fds) as $fd) {
|
||||
[$workerId, $fd] = explode('.', $fd);
|
||||
$this->manager->sendMessage((int) $workerId, new PushMessage((int) $fd, $data));
|
||||
}
|
||||
}
|
||||
|
||||
public function emit(string $event, ...$data): void
|
||||
{
|
||||
$this->push(new Event($event, $data));
|
||||
}
|
||||
}
|
||||
54
src/websocket/Room.php
Normal file
54
src/websocket/Room.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
use think\worker\Conduit;
|
||||
|
||||
class Room
|
||||
{
|
||||
|
||||
public function __construct(protected Conduit $conduit)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function add($fd, ...$rooms)
|
||||
{
|
||||
$this->conduit->sAdd($this->getClientKey($fd), ...$rooms);
|
||||
|
||||
foreach ($rooms as $room) {
|
||||
$this->conduit->sAdd($this->getRoomKey($room), $fd);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($fd, ...$rooms)
|
||||
{
|
||||
$rooms = count($rooms) ? $rooms : $this->getRooms($fd);
|
||||
|
||||
$this->conduit->sRem($this->getClientKey($fd), ...$rooms);
|
||||
|
||||
foreach ($rooms as $room) {
|
||||
$this->conduit->sRem($this->getRoomKey($room), $fd);
|
||||
}
|
||||
}
|
||||
|
||||
public function getClients(string $room)
|
||||
{
|
||||
return $this->conduit->sMembers($this->getRoomKey($room)) ?: [];
|
||||
}
|
||||
|
||||
public function getRooms(string $fd)
|
||||
{
|
||||
return $this->conduit->sMembers($this->getClientKey($fd)) ?: [];
|
||||
}
|
||||
|
||||
protected function getClientKey(string $key)
|
||||
{
|
||||
return "ws:client:{$key}";
|
||||
}
|
||||
|
||||
protected function getRoomKey($room)
|
||||
{
|
||||
return "ws:room:{$room}";
|
||||
}
|
||||
}
|
||||
81
src/websocket/socketio/EnginePacket.php
Normal file
81
src/websocket/socketio/EnginePacket.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket\socketio;
|
||||
|
||||
class EnginePacket
|
||||
{
|
||||
/**
|
||||
* Engine.io packet type `open`.
|
||||
*/
|
||||
const OPEN = 0;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `close`.
|
||||
*/
|
||||
const CLOSE = 1;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `ping`.
|
||||
*/
|
||||
const PING = 2;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `pong`.
|
||||
*/
|
||||
const PONG = 3;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `message`.
|
||||
*/
|
||||
const MESSAGE = 4;
|
||||
|
||||
/**
|
||||
* Engine.io packet type 'upgrade'
|
||||
*/
|
||||
const UPGRADE = 5;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `noop`.
|
||||
*/
|
||||
const NOOP = 6;
|
||||
|
||||
public $type;
|
||||
|
||||
public $data = '';
|
||||
|
||||
public function __construct($type, $data = '')
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public static function open($payload)
|
||||
{
|
||||
return new self(self::OPEN, $payload);
|
||||
}
|
||||
|
||||
public static function pong($payload = '')
|
||||
{
|
||||
return new self(self::PONG, $payload);
|
||||
}
|
||||
|
||||
public static function ping()
|
||||
{
|
||||
return new self(self::PING);
|
||||
}
|
||||
|
||||
public static function message($payload)
|
||||
{
|
||||
return new self(self::MESSAGE, $payload);
|
||||
}
|
||||
|
||||
public static function fromString(string $packet)
|
||||
{
|
||||
return new self(substr($packet, 0, 1), substr($packet, 1) ?: '');
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
return $this->type . $this->data;
|
||||
}
|
||||
}
|
||||
198
src/websocket/socketio/Handler.php
Normal file
198
src/websocket/socketio/Handler.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket\socketio;
|
||||
|
||||
use Exception;
|
||||
|
||||
use think\Config;
|
||||
use think\Event;
|
||||
use think\Request;
|
||||
use think\worker\contract\websocket\HandlerInterface;
|
||||
use think\worker\Websocket;
|
||||
use think\worker\websocket\Event as WsEvent;
|
||||
use think\worker\websocket\Frame;
|
||||
use Workerman\Timer;
|
||||
|
||||
class Handler implements HandlerInterface
|
||||
{
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
protected $event;
|
||||
|
||||
protected $websocket;
|
||||
|
||||
protected $eio;
|
||||
|
||||
protected $pingTimeoutTimer = 0;
|
||||
protected $pingIntervalTimer = 0;
|
||||
|
||||
protected $pingInterval;
|
||||
protected $pingTimeout;
|
||||
|
||||
public function __construct(Event $event, Config $config, Websocket $websocket)
|
||||
{
|
||||
$this->event = $event;
|
||||
$this->config = $config;
|
||||
$this->websocket = $websocket;
|
||||
$this->pingInterval = $this->config->get('worker.websocket.ping_interval', 25000);
|
||||
$this->pingTimeout = $this->config->get('worker.websocket.ping_timeout', 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* "onOpen" listener.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onOpen(Request $request)
|
||||
{
|
||||
$this->eio = $request->param('EIO');
|
||||
|
||||
$payload = json_encode(
|
||||
[
|
||||
'sid' => base64_encode(uniqid()),
|
||||
'upgrades' => [],
|
||||
'pingInterval' => $this->pingInterval,
|
||||
'pingTimeout' => $this->pingTimeout,
|
||||
]
|
||||
);
|
||||
|
||||
$this->push(EnginePacket::open($payload));
|
||||
|
||||
$this->event->trigger('worker.websocket.Open', $request);
|
||||
|
||||
if ($this->eio < 4) {
|
||||
$this->resetPingTimeout($this->pingInterval + $this->pingTimeout);
|
||||
$this->onConnect();
|
||||
} else {
|
||||
$this->schedulePing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "onMessage" listener.
|
||||
*
|
||||
* @param Frame $frame
|
||||
*/
|
||||
public function onMessage(Frame $frame)
|
||||
{
|
||||
$enginePacket = EnginePacket::fromString($frame->data);
|
||||
|
||||
$this->event->trigger('worker.websocket.Message', $enginePacket);
|
||||
|
||||
$this->resetPingTimeout($this->pingInterval + $this->pingTimeout);
|
||||
|
||||
switch ($enginePacket->type) {
|
||||
case EnginePacket::MESSAGE:
|
||||
$packet = Packet::fromString($enginePacket->data);
|
||||
switch ($packet->type) {
|
||||
case Packet::CONNECT:
|
||||
$this->onConnect($packet->data);
|
||||
break;
|
||||
case Packet::EVENT:
|
||||
$type = array_shift($packet->data);
|
||||
$data = $packet->data;
|
||||
$result = $this->event->trigger('worker.websocket.Event', new WsEvent($type, $data));
|
||||
|
||||
if ($packet->id !== null) {
|
||||
$responsePacket = Packet::create(Packet::ACK, [
|
||||
'id' => $packet->id,
|
||||
'nsp' => $packet->nsp,
|
||||
'data' => $result,
|
||||
]);
|
||||
|
||||
$this->push($responsePacket);
|
||||
}
|
||||
break;
|
||||
case Packet::DISCONNECT:
|
||||
$this->event->trigger('worker.websocket.Disconnect');
|
||||
$this->websocket->close();
|
||||
break;
|
||||
default:
|
||||
$this->websocket->close();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EnginePacket::PING:
|
||||
$this->event->trigger('worker.websocket.Ping');
|
||||
$this->push(EnginePacket::pong($enginePacket->data));
|
||||
break;
|
||||
case EnginePacket::PONG:
|
||||
$this->event->trigger('worker.websocket.Pong');
|
||||
$this->schedulePing();
|
||||
break;
|
||||
default:
|
||||
$this->websocket->close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "onClose" listener.
|
||||
*/
|
||||
public function onClose()
|
||||
{
|
||||
Timer::del($this->pingTimeoutTimer);
|
||||
Timer::del($this->pingIntervalTimer);
|
||||
$this->event->trigger('worker.websocket.Close');
|
||||
}
|
||||
|
||||
protected function onConnect($data = null)
|
||||
{
|
||||
try {
|
||||
$this->event->trigger('worker.websocket.Connect', $data);
|
||||
$packet = Packet::create(Packet::CONNECT);
|
||||
if ($this->eio >= 4) {
|
||||
$packet->data = ['sid' => base64_encode(uniqid())];
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$packet = Packet::create(Packet::CONNECT_ERROR, [
|
||||
'data' => ['message' => $exception->getMessage()],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->push($packet);
|
||||
}
|
||||
|
||||
protected function resetPingTimeout($timeout)
|
||||
{
|
||||
Timer::del($this->pingTimeoutTimer);
|
||||
$this->pingTimeoutTimer = Timer::delay($timeout, function () {
|
||||
$this->websocket->close();
|
||||
});
|
||||
}
|
||||
|
||||
protected function schedulePing()
|
||||
{
|
||||
Timer::del($this->pingIntervalTimer);
|
||||
$this->pingIntervalTimer = Timer::delay($this->pingInterval, function () {
|
||||
$this->push(EnginePacket::ping());
|
||||
$this->resetPingTimeout($this->pingTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
public function encodeMessage($message)
|
||||
{
|
||||
if ($message instanceof WsEvent) {
|
||||
$message = Packet::create(Packet::EVENT, [
|
||||
'data' => array_merge([$message->type], $message->data),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($message instanceof Packet) {
|
||||
$message = EnginePacket::message($message->toString());
|
||||
}
|
||||
|
||||
if ($message instanceof EnginePacket) {
|
||||
$message = $message->toString();
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
protected function push($data)
|
||||
{
|
||||
$this->websocket->push($data);
|
||||
}
|
||||
|
||||
}
|
||||
134
src/websocket/socketio/Packet.php
Normal file
134
src/websocket/socketio/Packet.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket\socketio;
|
||||
|
||||
/**
|
||||
* Class Packet
|
||||
*/
|
||||
class Packet
|
||||
{
|
||||
/**
|
||||
* Socket.io packet type `connect`.
|
||||
*/
|
||||
const CONNECT = 0;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `disconnect`.
|
||||
*/
|
||||
const DISCONNECT = 1;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `event`.
|
||||
*/
|
||||
const EVENT = 2;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `ack`.
|
||||
*/
|
||||
const ACK = 3;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `connect_error`.
|
||||
*/
|
||||
const CONNECT_ERROR = 4;
|
||||
|
||||
/**
|
||||
* Socket.io packet type 'binary event'
|
||||
*/
|
||||
const BINARY_EVENT = 5;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `binary ack`. For acks with binary arguments.
|
||||
*/
|
||||
const BINARY_ACK = 6;
|
||||
|
||||
public $type;
|
||||
public $nsp = '/';
|
||||
public $data = null;
|
||||
public $id = null;
|
||||
|
||||
public function __construct(int $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public static function create($type, array $decoded = [])
|
||||
{
|
||||
$new = new self($type);
|
||||
$new->id = $decoded['id'] ?? null;
|
||||
if (isset($decoded['nsp'])) {
|
||||
$new->nsp = $decoded['nsp'] ?: '/';
|
||||
} else {
|
||||
$new->nsp = '/';
|
||||
}
|
||||
$new->data = $decoded['data'] ?? null;
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
$str = '' . $this->type;
|
||||
if ($this->nsp && '/' !== $this->nsp) {
|
||||
$str .= $this->nsp . ',';
|
||||
}
|
||||
|
||||
if ($this->id !== null) {
|
||||
$str .= $this->id;
|
||||
}
|
||||
|
||||
if (null !== $this->data) {
|
||||
$str .= json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
public static function fromString(string $str)
|
||||
{
|
||||
$i = 0;
|
||||
|
||||
$packet = new Packet((int) substr($str, 0, 1));
|
||||
|
||||
// look up namespace (if any)
|
||||
if ('/' === substr($str, $i + 1, 1)) {
|
||||
$nsp = '';
|
||||
while (++$i) {
|
||||
$c = substr($str, $i, 1);
|
||||
if (',' === $c) {
|
||||
break;
|
||||
}
|
||||
$nsp .= $c;
|
||||
if ($i === strlen($str)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$packet->nsp = $nsp;
|
||||
} else {
|
||||
$packet->nsp = '/';
|
||||
}
|
||||
|
||||
// look up id
|
||||
$next = substr($str, $i + 1, 1);
|
||||
if ('' !== $next && is_numeric($next)) {
|
||||
$id = '';
|
||||
while (++$i) {
|
||||
$c = substr($str, $i, 1);
|
||||
if (null == $c || !is_numeric($c)) {
|
||||
--$i;
|
||||
break;
|
||||
}
|
||||
$id .= substr($str, $i, 1);
|
||||
if ($i === strlen($str)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$packet->id = intval($id);
|
||||
}
|
||||
|
||||
// look up json data
|
||||
if (substr($str, ++$i, 1)) {
|
||||
$packet->data = json_decode(substr($str, $i), true);
|
||||
}
|
||||
|
||||
return $packet;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user