初始化

This commit is contained in:
2026-01-06 13:37:07 +08:00
parent c3435595fe
commit 00d7a381aa
70 changed files with 3913 additions and 1 deletions

15
src/websocket/Event.php Normal file
View 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
View 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
View 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
View 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
View 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}";
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}