Module - WebSocket

The CWebSocket module provides a real-time WebSocket server built on top of ReactPHP and Ratchet. It uses the Pusher protocol, allowing you to use the Pusher JavaScript client or CF's built-in CSocket for client-side connections.


Architecture

[Browser]  <--WebSocket-->  [CWebSocket Server (ReactPHP)]  <--Broadcast-->  [PHP App]
              Pusher JS           Port 6001/6002                CBroadcast

The WebSocket server runs as a CDaemon service. Your PHP application pushes events via CBroadcast, and connected clients receive them in real-time.


Configuration

Create config/websocket.php in your application:

<?php
return [
    'enable' => true,
    'dashboard' => [
        'port' => 6002,
        'domain' => c::env('WEBSOCKET_HOST', 'myapp.example.com'),
        'path' => 'cwebsocket',
    ],
    'apps' => [
        'myapp' => [
            'id' => 'myapp',
            'name' => 'myapp',
            'host' => c::env('WEBSOCKET_HOST', 'myapp.example.com'),
            'key' => 'myapp-key',
            'secret' => 'myapp-secret',
            'path' => null,
            'capacity' => null,
            'enable_client_messages' => true,
            'enable_statistics' => false,
            'allowed_origins' => [],
        ],
    ],
    'replication' => [
        'mode' => 'local',
        'modes' => [
            'local' => [
                'channel_manager' => \CWebSocket_ChannelManager_LocalChannelManager::class,
                'collector' => \CWebSocket_Statistic_Collector_MemoryCollector::class,
            ],
        ],
    ],
    'statistics' => [
        'store' => \CWebSocket_Statistic_Store_DatabaseStore::class,
        'interval_in_seconds' => 3600,
        'delete_statistics_older_than_days' => 30,
    ],
    'max_request_size_in_kb' => 250,
    'ssl' => [
        'local_cert' => null,
        'local_pk' => null,
        'passphrase' => null,
        'verify_peer' => false,
        'allow_self_signed' => true,
    ],
    'handlers' => [
        'websocket' => \CWebSocket_Handler_WebSocketHandler::class,
        'health' => \CWebSocket_Handler_HealthHandler::class,
        'trigger_event' => \CWebSocket_Handler_ApiHandler_TriggerEvent::class,
        'fetch_channels' => \CWebSocket_Handler_ApiHandler_FetchChannels::class,
        'fetch_channel' => \CWebSocket_Handler_ApiHandler_FetchChannel::class,
        'fetch_users' => \CWebSocket_Handler_ApiHandler_FetchUsers::class,
    ],
    'promise_resolver' => \React\Promise\FulfilledPromise::class,
];

For Redis-based replication (multi-server), change replication.mode to 'redis' and configure the Redis modes section.


Creating the Daemon Service

Extend CWebSocket_Daemon_WebSocketService:

<?php
class MyDaemon_Service_WebSocketService extends CWebSocket_Daemon_WebSocketService {
    protected $loopInterval = 60;

    public function setup() {
        c::db()->disableBenchmark();
        $options = [];
        $options['host'] = '0.0.0.0';
        $options['port'] = '6002';
        $options['disableStatistics'] = true;
        $options['debug'] = true;
        $options['loop'] = null;
        $this->websocketOptions = $options;
        $process = new CWebSocket_Process_StartServer($this->websocketOptions, new CDaemon_Output());
        $process->start();
    }

    public function execute() {
    }
}

Register in bootstrap.php:

CManager::registerDaemon(MyDaemon_Service_WebSocketService::class, null, 'WebSocket');

Broadcasting Events

Use CBroadcast to push events from server-side PHP to connected WebSocket clients:

// Direct broadcast
CBroadcast::broadcast(['presence-chat.global'], 'CHAT', [
    'message' => 'Hello World',
    'user' => 'System',
    'time' => date('H:i'),
]);

Create a broadcast helper class for your application:

<?php
class MyBroadcast {
    public static function broadcast(array $channels, $event = 'BROADCAST', array $payload = []) {
        try {
            CBroadcast::manager()->driver('pusher')->broadcast($channels, $event, $payload);
        } catch (\Exception $ex) {
            // Silently fail if WebSocket is not running
        }
    }

    public static function chatGlobal($message, $username) {
        static::broadcast(['presence-chat.global'], 'CHAT', [
            'message' => $message,
            'username' => $username,
            'time' => date('H:i'),
        ]);
    }

    public static function notifyUser($userId, $title, $message) {
        static::broadcast(['private-user.' . $userId], 'NOTIFICATION', [
            'title' => $title,
            'message' => $message,
        ]);
    }
}

Registering Channels

Register broadcast channels in bootstrap.php for authorization:

CBroadcast::registerChannel('user', MyBroadcast_UserChannel::class);
CBroadcast::registerChannel('user.{user}', MyBroadcast_UserChannel::class);
CBroadcast::registerChannel('chat.{room}', MyBroadcast_ChatChannel::class);

Channel Types

Prefix Type Description
(none) Public Anyone can subscribe
private- Private Requires authentication
presence- Presence Private + member tracking (who is online)

Client-Side Connection

Use Pusher JS to connect to the WebSocket server:

<script src="https://js.pusher.com/7.0/pusher.min.js"></script>
<script>
var pusher = new Pusher('myapp-key', {
    wsHost: 'myapp.example.com',
    wsPort: 6002,
    forceTLS: false,
    disableStats: true,
    enabledTransports: ['ws']
});

var channel = pusher.subscribe('presence-chat.global');
channel.bind('CHAT', function(data) {
    console.log(data.message);
});
</script>

Or use CF's built-in CSocket (available via cres.js):

var socket = new CSocket({
    broadcaster: 'pusher',
    key: 'myapp-key',
    wsHost: 'myapp.example.com',
    wsPort: 6002,
    forceTLS: false,
    disableStats: true
});

socket.channel('presence-chat.global')
    .listen('CHAT', function(data) {
        console.log(data.message);
    });

Auto-Restart with Cron

Ensure the WebSocket daemon stays running by creating a cron task:

<?php
class MyCron_Cron_Server_RestartPrimaryService extends MyCron_Cron {
    protected $schedule = '*/5 * * * *';

    public function handler() {
        MyTaskQueue_Server_RestartPrimaryService::dispatchNow();
    }
}
<?php
class MyTaskQueue_Server_RestartPrimaryService {
    use CQueue_Trait_DispatchableTrait;
    use CQueue_Trait_QueueableTrait;
    use CQueue_Trait_InteractsWithQueue;
    use CQueue_Trait_SerializesModels;

    public function execute() {
        $this->restartServiceWhenNotRunning(
            CDaemon::createRunner(MyDaemon_Service_WebSocketService::class)
        );
    }

    private function restartServiceWhenNotRunning(CDaemon_Runner $service) {
        if (!$service->isRunning()) {
            $service->rotateLog();
            $service->start();
            CCron::log('successfully restart ' . $service->getServiceClass());
        }
    }
}

Register in bootstrap.php:

MyCron::registerCron();

Managing from Admin

Add daemon management to your admin panel using the built-in trait:

<?php
class Controller_Admin_Sysadmin_Daemon extends MyAdmin_Controller {
    use CTrait_Controller_Application_Manager_Daemon;

    protected function getTitle() {
        return 'Daemon';
    }
}

This provides a UI to start, stop, view logs, and check status of all registered daemons.


Checking WebSocket Status Programmatically

// Check if WebSocket port is reachable
$fp = @fsockopen('127.0.0.1', 6002, $errno, $errstr, 1);
$isRunning = ($fp !== false);
if ($fp) {
    fclose($fp);
}

// Or use CDaemon runner
$runner = CDaemon::createRunner(MyDaemon_Service_WebSocketService::class);
$isRunning = $runner->isRunning();