Usage

Home » Documentation » Usage

Controlling complexity is the essence of computer programming.

Brian Kernighan

Overview

Considerations:

  • Messages must be in JSON format.
  • The message must contain the action key corresponding to the action being triggered. e.g. {action: 'broadcast-action'}.
  • If a simple non-JSON string is sent, it will be considered a “ping/pong”, responding with the same message that was sent. This is the “base-action”.

Following we have 4 examples:

  • Case 1: The simple case: messages happen in ping/ping fashion between client and server and won’t broadcast to other clients. (base-action)
  • Case 2: The channel case: messages get broadcasted to all other clients within the same channel. If the client is not connected to a channel, it will be broadcast to everybody not connected to channels.
  • Case 3: The listener case: where clients can filter which messages (by action) they will receive. This is useful when you have custom actions.
  • Case 4: Associate an application user id with a connection (fd). This will keep in memory the information of which user_id (integer) owns which file descriptor (fd – integer).

Case 1: Simple Case

At this example the user will receive back a real-time message from the server after sending a message.

At this library, there is the presumption that the socket message has a JSON format, if not, the ConveyorActionsBaseAction will be the handler and the text will be added as the “data” parameter of that action. That said, the following standard is expected to be followed by the messages in general, so they can match specific Actions. The minimum format is this:

{
    "action": "base-action",
    "data": "here goes other fields necessary for the Actions processing..."
}

Thats it! Now, to communicate in real-time with this service, on your HTML you can do something like this:

<div>
    <div><button onclick="sendMessage(JSON.stringify({
            'action': 'base-action',
            'data': 'first',
        }))">Base Action</button></div>
    <div><button onclick="sendMessage('second')">Simple Text</button></div>
    <div id="output"></div>
</div>
<script type="text/javascript">
    var websocket = new WebSocket('ws://127.0.0.1:8080');
    websocket.onmessage = function (evt) {
        document.getElementById('output').innerHTML = JSON.parse(evt.data).data;
    };
    function sendMessage(message) {
        websocket.send(message);
    }
</script>

Notice that these 2 buttons result in the same action.

This is what it looks like:

Case 2: Using Channels

At this case it is possible for clients sharing a channel to communicate to each other by broadcasting messages and data through this channel.

The procedure here requires one extra step during the instantiation: the connection action. The connection action will link in a persistent manner the connection FD to a channel.

Notice that if you use the fanout action, it will broadcast to any other connection outside of channels. It will only broadcast messages to other clients within the same channel if you use the broadcast action.

If you are using any Conveyor client package you won’t need to manually connect to a channel at the right moment by yourself as the client already does that by a configuration parameter. To connect manually, you can send the following message:

{
    "action": "channel-connect",
    "channel": "channel-name"
}

After connecting to a channel, all messages sent by that client will be within that channel. You can disconnect from a channel by sending this message:

{
    "action": "channel-disconnect"
}

As an example, we have the following HTML example. When connected, the given connection will participate on a given channel:

<div>
    <form id="message-form" onsubmit="return sendMessage()">
        <div>
            <input id="message-box" autocomplete="off" type="text" placeholder="The message goes here..."/>
        </div>
        <input type="submit" value="Submit"/>
    </form>

    <div>
        <ul id="output"></ul>
    </div>
</div>
<script type="text/javascript">
    var channel = 'actionschannel';
    var websocket = new WebSocket('ws://127.0.0.1:8080');
    websocket.onopen = function(e) {
        // connect to a channel
        websocket.send(JSON.stringify({
            'action': 'channel-connect',
            'channel': channel,
        }));
    };
    websocket.onmessage = function (evt) {
        document.getElementById('output').innerHTML = evt.data;
    };
    function sendMessage() {
        websocket.send(JSON.stringify({
            'action': 'broadcast-action',
            'data': document.getElementById('message-box').value,
        }));
        return false;
    }
</script>

That’s all, with this, you would have the following:

Case 3: Listening to Actions

At this example clients can filter messages that they receive by adding listeners to the ones they want. If there are no listeners registered, they will receive all broadcast actions.

The JavaScript client then starts listening to a specific action by sending a new message at the connection opening:

At this example we assume that we have another broadcast action implemented “secondary-broadcast-action”. That will be used to differentiate between the listened and the not listened broadcasted actions.

<div>
    <div><button onclick="sendMessage('broadcast-action', 'first')">First Action</button></div>
    <div><button onclick="sendMessage('secondary-broadcast-action', 'second')">Second Action</button></div>
    <div id="output"></div>
</div>
<script type="text/javascript">
    var channel = 'actionschannel';
    var websocket = new WebSocket('ws://127.0.0.1:8080');
    websocket.onopen = function(e) {
        websocket.send(JSON.stringify({
            'action': 'channel-connect',
            'channel': channel,
        }));
        
        // This starts the process of listening to actions from the current client.
        websocket.send(JSON.stringify({
            'action': 'add-listener',
            'listen': 'secondary-broadcast-action',
        }));
    };
    websocket.onmessage = function (evt) {
        document.getElementById('output').innerHTML = JSON.parse(evt.data).data;
    };
    function sendMessage(action, message) {
        websocket.send(JSON.stringify({
            'action': action,
            'data': message,
        }));
    }
</script>

You can find out more about how to implement your own actions here: Creating your own Actions.

Case 4: Associate User with Connection

This functionality is for applications that need to have associations between connections (fd) and users. This is useful when you need to execute actions that need to know the user and decide upon that. One good example is a server that serves at the same time multiple users, but won’t answer users the same way depending on the procedure executed. That way, you can have actions that will process some data and broadcast to connections only what each connection needs to receive for that procedure.

For this functionality, you only need one extra action to be dispatched:

{
    "action": "assoc-user-to-fd-action",
    "userId": 1,
}

This code will associate the user “1” with the current connection.

Advice: It is recommended to use some token or secret to identify them before the WebSocket server accepts the association. To see a good example, you can check it here: Authorization.

Case 5: Using Middlewares

The usage of middleware might help to secure your WebSocket server, making sure specific validations and conditions are met in order to proceed. At Socket Conveyor, middlewares are attached to actions at the socket router’s instance. The way to do that is as follows:

<?php

use Conveyor\Conveyor;
use OpenSwoole\WebSocket\Frame;
use OpenSwoole\WebSocket\Server;

$websocket = new Server('0.0.0.0', 8080);

$websocket->on('message', function (Server $server, Frame $frame) use ($persistenceService) {
    Conveyor::init()
        ->server($server)
        ->fd($frame->fd)
        ->addMiddlewareToAction('broadcast-action', function ($payload) {
            echo "Received broadcast: " . $payload['data'];
            return $payload;
        )
        ->closeConnections()
        ->run($frame->data);
});

$websocket->start();

This is a simple example of implementing your OpenSwoole server with Conveyor with a custom middleware that echoes incoming messages.

The Payload is an array with 2 keys:

  • data: The actual message coming in.
  • user: The user id if that was associated. You can learn how to associate here.

Case 6: Fanout Action

This is a global broadcast that goes outside the borders of the channels.

Important: when a client is listening to actions other than the one you send, that client won’t receive it. It happens because listeners are “filters”.

<div>
    <div><button onclick="sendMessage('Hello')">Say Hello</button></div>
    <div id="output"></div>
</div>
<script type="text/javascript">
    var websocket = new WebSocket('ws://127.0.0.1:8080');
    websocket.onmessage = function (evt) {
        document.getElementById('output').innerHTML = JSON.parse(evt.data).data;
    };
    function sendMessage(message) {
        websocket.send(JSON.stringify({
            'action': 'fanout-action',
            'data': message
        }));
    }
</script>

Messages sent with this example will be broadcasted to any client regardless of channels.

Learn more about available actions here: Available Actions.