Skip to content

Repository files navigation

lumnd/plato-workerman

The Workerman driver behind plato\server\driver: the event loop, the protocol and the worker processes PlatoPHP deliberately does not ship.

简体中文

lumnd/platophp owns what happens between an inbound message and a controller — one whole message, one ct / ac dispatch, clean request state, an identity that was established once and belongs to the connection. It owns none of the socket: an event loop, a process manager and a protocol codec are not a framework's business, and a protocol parser is the part of the stack where a mistake is a remote memory exhaustion rather than a wrong page. This package is the other half of that seam, and nothing above plato\server\driver knows it is here.

Requirements

PHP 8.0+
Workerman ^4.1 || ^5.0 — both majors, one code path
Extensions pcntl and posix, which Workerman needs for the master process
Suggested ext-event, for a loop that scales past a thousand connections per worker

Install

composer require lumnd/plato-workerman

The shipped config/server.php already names workerman as its driver, so a default installation starts:

php vendor/bin/plato server:start

after the one line that registers the commands, below.

Configure

Everything the framework does not own in config/server.php is passed to this adapter untouched. Copy the file into your application's config/ and change what you need:

return [
    'default' => 'default',

    'servers' => [
        'default' => [
            // 'workerman' is registered by this package; the class name works just as well and
            // needs no bootstrap file at all
            'driver' => 'workerman',

            'listen'    => 'websocket://127.0.0.1:8282',
            'name'      => 'platophp-server',
            'processes' => 4,

            'heartbeat' => ['interval' => 30, 'timeout' => 120],

            // Read by plato\server\dispatcher, not by this adapter
            'dispatch' => ['max_payload' => 65536],
        ],
    ],
];
Setting Default What it is
listen websocket://127.0.0.1:8282 Where to listen, and in which protocol. See below
name platophp-server Process name in ps, and the stem of the pid, status and log file names
processes 4 Worker processes. One serves one message at a time
user / group '' Drop privileges after binding
pid_file data_path()/server/<name>.pid Where the master keeps its pid
status_file data_path()/server/<name>.status Where server:status collects its report
log_file log_path()/<name>-workerman.log Workerman's own operational log
stdout_file log_path()/<name>-stdout.log Where a daemonized process sends its output
daemonize false Detach from the terminal; --daemon sets it for one run
graceful true Let stop, restart and reload wait for the workers to finish
reuse_port false SO_REUSEPORT, so a restart does not drop the listening socket
protocol '' Workerman protocol class framing a raw tcp:// listener
max_package_size 0 Largest packet assembled off the socket; 0 keeps Workerman's 10 MB
stop_timeout 0 Seconds a worker gets after a graceful stop; 0 keeps Workerman's 2
ssl [] local_cert, local_pk, verify_peer, allow_self_signed. Paths, never key material
context [] Extra stream context for the listening socket
heartbeat [] interval and timeout, in seconds. Either at 0 turns the sweep off
event_loop '' Workerman event loop class; a coroutine loop is refused
on_worker_start null fn(int $index, int $count), in each worker once it knows which it is
on_worker_stop null fn(int $index, int $count), on the way out

TLS is ssl.local_cert plus a websocket:// listener — that combination is wss. Terminating TLS at a reverse proxy and binding 127.0.0.1 is the usual arrangement, and the one to prefer.

Protocols

dispatcher::handle() takes one whole application message. Which protocol delivers it is this adapter's business and not the framework's, so several do:

Listen value
websocket://host:port What most clients speak, and what the shipped configuration defaults to
text://host:port One message per line
frame://host:port Four bytes of total length, big endian, then the payload
tcp://host:port + protocol Any Workerman protocol class, including one you wrote

and some do not:

Refused Why
tcp://, ssl://, unix:// without protocol A byte stream has no message boundaries, and a dispatcher handed half a message cannot tell
udp:// A datagram carries no connection, so there is nothing to authenticate once and nothing to answer on
http:// A different shape of request; serve HTTP through php-fpm or the framework's own entry point
ws://, wss:// Workerman speaks those as a client; a listener is websocket://

Each is refused at start with a message saying which of these it is, rather than with a connection that half works.

Run it

Register the commands once, in plato.config.php or under console.commands in config/config.php:

'commands' => [plato\workerman\console::class],
php vendor/bin/plato server:start                    # foreground, until a signal
php vendor/bin/plato server:start --daemon           # detached
php vendor/bin/plato server:start --server=chat --processes=8
php vendor/bin/plato server:reload                   # new code, same listening socket
php vendor/bin/plato server:stop                     # --force to skip the graceful wait
php vendor/bin/plato server:status
php vendor/bin/plato server:connections

server:start is a foreground process on purpose, and belongs under something that keeps it running:

[Service]
ExecStart=/usr/bin/php /srv/app/vendor/bin/plato server:start
Restart=always
KillSignal=SIGTERM
TimeoutStopSec=40

An application that would rather not use the console calls the facade itself, which is all the command does:

plato\server\server::start();

Write the application

An action reached over a socket is an ordinary action. It reads its input through req, asks who the caller is through plato::$auth, and returns a plato\http\reply:

namespace control;

use plato\http\resp;
use plato\plato;
use plato\server\dispatcher;

class ctl_chat
{
    public function say()
    {
        // The identity was established at open and belongs to the connection
        $user = plato::$auth;

        // Reach this client again from anywhere in the process
        dispatcher::current()->send(['code' => 0, 'msg' => 'delivered']);

        return resp::json(['code' => 0, 'seq' => dispatcher::seq()]);
    }
}

Authentication happens once, at open. On a websocket the only thing a client has to authenticate with is the handshake — the frames that follow carry no headers — so this adapter puts it on the connection under driver::HANDSHAKE:

use plato\server\connection;
use plato\server\dispatcher;
use plato\workerman\driver;

dispatcher::on('open', function (connection $conn)
{
    $handshake = (array) $conn->get(driver::HANDSHAKE, []);
    $user      = my_auth((string) ($handshake['query']['token'] ?? ''));

    if ( $user === null )
    {
        // The driver closes the connection
        return false;
    }

    // Every later message of this client is dispatched as this identity
    $conn->set(connection::AUTH, $user);

    return true;
});

handshake is ['path' => string, 'query' => array, 'headers' => array], with lower case header names, on both Workerman majors. A protocol that has no handshake — a framed tcp:// listener — sets no attribute at all.

Processes

Every worker calls plato\worker::enter() before it serves anything, so an application shards work the same way it does under plato\pool:

'on_worker_start' => function (int $index, int $count)
{
    // Exactly one worker of this listener runs the sweep
    if ( plato\worker::owns() )
    {
        Workerman\Timer::add(60, 'my_sweep');
    }
},

Two things the framework says out loud, and this adapter keeps to:

  • One message at a time per process. Request state lives in static properties, so a coroutine scheduler running two dispatches inside one pid corrupts it. Workerman 5's Fiber, Swoole and Swow loops are refused rather than half supported.
  • send() reaches this process only. Workers do not share memory, and neither driver::connections() nor server::send() pretends otherwise. Fanning out to every worker needs a backend both can see — Redis pub/sub, or another external bus.

Tests

composer test          # Unit + Feature
composer analyse       # phpstan level 5, no baseline
composer style         # phpcs, zero errors

The feature suite starts real listeners in child processes and talks to them over real sockets with a hand written websocket handshake and hand written frames — a client that shared its framing code with the server would prove nothing about the wire.

License

MIT. Security reports go to the address in SECURITY.md.

About

Workerman server adapter for PlatoPHP: the event loop, protocol and worker processes behind plato\server\driver

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages