workerman/gateway-worker
GatewayWorker is a PHP distributed framework built on Workerman for long-lived TCP, UDP, and WebSocket connections. It separates gateway, business worker, and register processes to scale real-time apps across servers, with process management and messaging for chat, IoT, push, and game backends.
workerman/gateway-worker is not a Laravel package with a service provider — it is a runnable scaffold you deploy as a separate daemon alongside your Laravel app. The most important mental shift: Laravel handles HTTP and enqueues work; GatewayWorker holds TCP/WebSocket connections and pushes.
Install and inspect
# In a service directory, NOT inside your Laravel app
composer create-project workerman/gateway-worker gateway
cd gateway
Look at these files first, in this order:
start_register.php — service discovery registry (0.0.0.0:1236).start_gateway.php — the public WebSocket/TCP listener (0.0.0.0:8282). Set $gateway->count here to scale connections.start_businessworker.php — where your logic runs. Set $worker->count here to scale CPU.Applications/YourApp/Events.php — your actual code: onConnect, onMessage, onClose, onWorkerStart.All three start_*.php files must point at the same register address:
$gateway->registerAddress = '127.0.0.1:1236';
$businessworker->registerAddress = '127.0.0.1:1236';
Run it
php start.php start # foreground, prints logs, use during dev
php start.php start -d # daemonized
php start.php status # live process/connection counts
php start.php stop
First use case — an authenticated chat channel
On the client, connect to ws://your-host:8282. In Events.php:
public static function onMessage($clientId, $message)
{
$data = json_decode($message, true);
if ($data['type'] === 'login') {
$uid = /* validate $data['token'] via Laravel/Redis/JWT */;
Gateway::bindUid($clientId, $uid); // stable identity
Gateway::joinGroup($clientId, "user:$uid"); // personal channel
}
}
From Laravel, push to that user with the separate client package:
composer require workerman/gatewayclient
use GatewayClient\Gateway;
Gateway::$registerAddress = config('services.gateway.register'); // 127.0.0.1:1236
Gateway::sendToUid($user->id, json_encode(['event' => 'ping']));
That round trip — Laravel request writes → GatewayClient → Gateway process → socket — is the core day-to-day loop.
1. Three-tier topology is the design, not an accident.
Register is a lightweight service registry. Gateway does nothing but hold sockets and speak the internal protocol. BusinessWorker runs Events.php. Scale Gateway->count for connection volume (10k+ per process is normal), scale BusinessWorker->count for message throughput. All three can be on separate hosts as long as registerAddress is reachable from Gateway and BusinessWorker.
2. Auth in the BusinessWorker, not the Gateway.
The Gateway process should never touch your database. Accept the socket on connect, then require a login frame within N seconds and close unauth sockets from a Timer. Rejecting at the WebSocket handshake is possible (onWebSocketConnect), but you can't cheaply hit Eloquent there.
3. Bootstrap Laravel inside onWorkerStart so Events.php can use Eloquent, Config, Redis.
public static function onWorkerStart($worker)
{
require __DIR__.'/../../../laravel/vendor/autoload.php';
$app = require __DIR__.'/../../../laravel/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
// now config(), DB, Cache, Redis are available
}
Once bootstrapped, Events.php can validate tokens (Auth::guard('api')->setToken(...)->user()), write to models, dispatch jobs — but see the gotchas about connection staleness.
4. Use bindUid for identity, joinGroup for channels.
Client IDs change on every reconnect; never persist them. bindUid($clientId, $uid) gives you sendToUid, isUidOnline, getClientIdByUid. Groups are the natural abstraction for rooms/subscriptions (joinGroup, leaveGroup, sendToGroup($group, $data, $excludeClientId)).
5. Wire GatewayClient into Laravel broadcasting.
Implement a custom broadcaster so broadcast(new MessageSent($m)) reaches the socket:
Broadcast::extend('gateway', fn ($app, $config) => new GatewayBroadcaster(
$config['register'], $config['app_key'] // if you use Gateway::$appKey scoping
));
Or, simpler: a queued event listener that calls Gateway::sendToUid() in handle(). Keep sends in queue workers so HTTP latency never depends on the socket layer.
6. Push from the "other direction" with AsyncTcpConnection.
BusinessWorker is itself a Workerman client. When a client sends a message, you can either enqueue it to Laravel (via Redis list) or open a non-blocking AsyncTcpConnection to a Laravel internal endpoint. Never use blocking file_get_contents/curl in onMessage.
7. Periodic work via Timer.
Heartbeats, presence sweeps, draining a Redis queue of outbound messages:
Timer::add(2, function () {
while ($job = Redis::lpop('outbound')) {
[$uid, $payload] = unserialize($job);
Gateway::sendToUid($uid, $payload);
}
});
8. Presence and online lists.
Gateway::isUidOnline($uid) and Gateway::getClientIdByUid work across BusinessWorker processes via the Gateway tier. But your own static $clients = [] in Events.php is per-worker — put anything that must be globally visible in Redis (SADD online:$room, SREM on onClose).
9. Scaling out.
Run two Registers for HA (Gateway/BusinessWorker round-robin between them). Add Gateway hosts behind a load balancer (sticky not required — GatewayClient and sendToUid route via the register). All hosts share one register address.
require this into Laravel. The package is a project skeleton. composer require workerman/gateway-worker in your Laravel app won't give you a facade — Events.php lives in its own directory, deployed by its own supervisor program.workerman/gateway-worker is the server; workerman/gatewayclient is what Laravel calls. They share GatewayProtocol — keep them roughly in sync when upgrading.$registerAddress must be set on every participant. Gateway, BusinessWorker, and Laravel's GatewayClient each need it. If sendToUid silently returns false, 90% of the time it's a missing/wrong register address or port 1236 blocked.static map of client IDs in onWorkerStart is only visible in that one worker. If you bindUid on worker A and check getClientIdByUid on worker B, it works because the Gateway tier answers — but your local arrays won't. Use Redis for anything global.Gateway::closeClient($clientId) if a Timer sweep finds it older than N seconds. Otherwise unauthenticated sockets pile up.unset large payloads, cap any in-memory caches, and let supervisor autorestart periodically (or Timer a graceful Worker::stopAll() after a request count).onWorkerStart will time out. On a Timer (say, every 30s while idle) call DB::connection()->reconnect() or wrap message handling in a try/catch that calls DB::purge() and retries once.onMessage terminates the process and supervisor restarts it, dropping all its connections. Wrap handlers and log via Worker::log() or Log::error() after Laravel bootstrap.curl or sleep(3) stalls everyone on that worker. Use AsyncTcpConnection, Redis, and queues.Gateway::sendToAll and sendToUid fan out internally across Gateway workers, but they still traverse each socket at send time. For fan-out to large groups, batch your writes, prefer sendToGroup, and throttle server-side. sendToGroup returns the count of recipients — useful for metrics.$gateway->transport = 'ssl' with ssl_cert_file and ssl_key_file on the Gateway, or terminate TLS at nginx with a WebSocket upgrade proxy (proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";).-d daemonization. Always run Linux in production.Gateway::sendTo* call opens a socket to the register, sends, and closes. In a hot loop set Gateway::$persistentConnection = true and register a register_shutdown_function that Gateway::close()s — otherwise you'll exhaust PHP sockets under load.php start.php start streams everything to your terminal. For daemon mode configure $worker->logFile per process, and log-rotate it with logrotate or a size cap.php start.php status. It shows per-process connection counts, memory, and timers — the fastest way to spot a busy vs. idle worker. php start.php restart is safer than stop + start for zero-downtime-ish reloads.Gateway::$appKey scoping. If you run multiple apps behind one register, set a shared $appKey on Gateway, BusinessWorker, and GatewayClient. Mismatched keys produce "connection not found"-style failures with no obvious error.How can I help you explore Laravel packages today?