Introduction
Usage
"Controlling complexity is the essence of computer programming." Brian Kernighan
Usage
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”.
Table of Contents
- Case 1: Simple Case
- Case 2: Using Channels
- Case 3: Associate User With Connection
- Case 4: Use Middlewares
- Case 5: Fanout Action
- Case 6: Broadcasting Messages through Http
- Case 7: Create your own Actions
- Case 8: Listen to Server Events
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:
1{
2 "action": "base-action",
3 "data": "here goes other fields necessary for the Actions processing..."
4}
Thats it! Now, to communicate in real-time with this service, on your HTML you can do something like this:
1<div>
2 <div><button onclick="sendMessage(JSON.stringify({
3 'action': 'base-action',
4 'data': 'first',
5 }))">Base Action</button></div>
6 <div><button onclick="sendMessage('second')">Simple Text</button></div>
7 <div id="output"></div>
8</div>
9<script type="text/javascript">
10 var websocket = new WebSocket('ws://127.0.0.1:8080');
11 websocket.onmessage = function (evt) {
12 document.getElementById('output').innerHTML = JSON.parse(evt.data).data;
13 };
14 function sendMessage(message) {
15 websocket.send(message);
16 }
17</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:
1{
2 "action": "channel-connect",
3 "channel": "channel-name"
4}
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:
1{
2 "action": "channel-disconnect"
3}
As an example, we have the following HTML example. When connected, the given connection will participate on a given channel:
1<div>
2 <form id="message-form" onsubmit="return sendMessage()">
3 <div>
4 <input id="message-box" autocomplete="off" type="text" placeholder="The message goes here..."/>
5 </div>
6 <input type="submit" value="Submit"/>
7 </form>
8
9 <div>
10 <ul id="output"></ul>
11 </div>
12</div>
13<script type="text/javascript">
14 var channel = 'actionschannel';
15 var websocket = new WebSocket('ws://127.0.0.1:8080');
16 websocket.onopen = function(e) {
17 // connect to a channel
18 websocket.send(JSON.stringify({
19 'action': 'channel-connect',
20 'channel': channel,
21 }));
22 };
23 websocket.onmessage = function (evt) {
24 document.getElementById('output').innerHTML = evt.data;
25 };
26 function sendMessage() {
27 websocket.send(JSON.stringify({
28 'action': 'broadcast-action',
29 'data': document.getElementById('message-box').value,
30 }));
31 return false;
32 }
33</script>
That’s all, with this, you would have the following:
Case 3: 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:
1{
2 "action": "assoc-user-to-fd-action",
3 "userId": 1,
4}
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 4: Use 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:
1<?php
2
3use Conveyor\Conveyor;
4use OpenSwoole\WebSocket\Frame;
5use OpenSwoole\WebSocket\Server;
6
7$websocket = new Server('0.0.0.0', 8080);
8
9$websocket->on('message', function (Server $server, Frame $frame) use ($persistenceService) {
10 Conveyor::init()
11 ->server($server)
12 ->fd($frame->fd)
13 ->addMiddlewareToAction('broadcast-action', function ($payload) {
14 echo "Received broadcast: " . $payload['data'];
15 return $payload;
16 )
17 ->closeConnections()
18 ->run($frame->data);
19});
20
21$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.
Case 5: 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”.
1<div>
2 <div><button onclick="sendMessage('Hello')">Say Hello</button></div>
3 <div id="output"></div>
4</div>
5<script type="text/javascript">
6 var websocket = new WebSocket('ws://127.0.0.1:8080');
7 websocket.onmessage = function (evt) {
8 document.getElementById('output').innerHTML = JSON.parse(evt.data).data;
9 };
10 function sendMessage(message) {
11 websocket.send(JSON.stringify({
12 'action': 'fanout-action',
13 'data': message
14 }));
15 }
16</script>
Messages sent with this example will be broadcasted to any client regardless of channels.
Case 6: Broadcasting Messages through Http
Conveyor provides the ability to broadcast messages to specific channels using HTTP requests through a reserved special endpoint: /conveyor/message
. This endpoint will broadcast messages to the specified channel as defined in the payload.
To broadcast a message via HTTP request, the following is a CURL example of the request:
1curl -X POST \
2 {url}/conveyor/message \
3 -H "Host: {your_host}:{your_port}" \
4 -H "User-Agent: Jacked Server HTTP Proxy" \
5 -H "Content-Type: application/json" \
6 -d '{
7 "channel": "{channel-name}",
8 "message": "{message}"
9 }'
Assuming your Conveyor server is running on http://localhost:8080
, and you want to broadcast a message to a channel named MONITOR_CHANNEL
with the content Hello World
, the CURL command would look like this:
1curl -X POST \
2 http://localhost:8080/conveyor/message \
3 -H "Host: localhost:8080" \
4 -H "User-Agent: Jacked Server HTTP Proxy" \
5 -H "Content-Type: application/json" \
6 -d '{
7 "channel": "MONITOR_CHANNEL",
8 "message": "Hello World"
9 }'
This command will send a POST request to the /conveyor/message
endpoint, broadcasting the specified message to the MONITOR_CHANNEL
.
By following this specification, you can easily broadcast messages to any channel supported by your Conveyor server through HTTP requests.
Case 7: Create your own Actions
Creating your own actions need 2 steps. (1) is about creating your action’s class. The other is injecting that action in Socket Conveyor’s workflow.
Action's Class
You can create your own actions by extending any action, or by extending the action abstraction Conveyor\Actions\Abstractions\AbstractAction
.
To get up to speed with this, let’s look at the BaseAction’s code:
1<?php
2
3namespace Conveyor\Actions;
4
5use Conveyor\Actions\Abstractions\AbstractAction;
6use InvalidArgumentException;
7
8class BaseAction extends AbstractAction
9{
10 public const NAME = 'base-action';
11
12 protected string $name = self::NAME;
13
14 public function validateData(array $data): void
15 {
16 if (!isset($data['data'])) {
17 throw new InvalidArgumentException('BaseAction required \'data\' field to be created!');
18}
19 }
20
21 public function execute(array $data): mixed
22 {
23 $this->send($data['data'], $this->fd);
24 return null;
25 }
26}
In this, you can see the 2 abstract functions implementations: validateData
and execute
.
Almost any logic can be implemented by looking at the AbstractAction
code and customizing it as well.
Adding an Action to Socket Conveyor
You can add extra actions to Socket Conveyor in 2 different ways: (1) at the Conveyor class, (2) at the Conveyor Server.
- Conveyor:
During the declaration of Socket Conveyor’s code, you can add your own actions like follows:
1<?php
2
3use Conveyor\Conveyor;
4use OpenSwoole\WebSocket\Frame;
5use OpenSwoole\WebSocket\Server;
6use MyNamespace\MyCustomAction;
7
8$websocket = new Server('0.0.0.0', 8080);
9
10$websocket->on('message', function (Server $server, Frame $frame) use ($persistenceService) {
11 Conveyor::init()
12 ->server($server)
13 ->fd($frame->fd)
14 ->addActions([
15 new MyCustomAction,
16 ])
17 ->closeConnections()
18 ->run($frame->data);
19});
20
21$websocket->start();
- Conveyor Server:
During the declaration of Conveyor Server, you can add your actions as a “conveyorOptions” parameter:
1<?php
2
3use Conveyor\ConveyorServer;
4use Conveyor\ConveyorWorker;
5use Conveyor\Actions\Interfaces\ActionInterface;
6
7/** @var array<array-key, ActionInterface> $actions */
8$actions = [new MyCustomAction];
9
10(new ConveyorServer())
11 ->conveyorOptions([
12 ConveyorWorker::ACTIONS => $actions,
13 ])
14 ->start();
Case 8: Listen to Server Events
Conveyor have several events that you can listen to for customization, debugging, etc.
To add a listener to an event, you can add callbacks to the events at the eventListeners
parameter at the static (new Conveyor\ConveyorServer())->start
method.
1<?php
2
3include __DIR__ . '/vendor/autoload.php';
4
5use Conveyor\ConveyorServer;
6use Conveyor\Events\PreServerStartEvent;
7
8(new ConveyorServer())
9 ->eventListeners([
10 ConveyorConstants::EVENT_PRE_SERVER_START => function (PreServerStartEvent $event) {
11 // do something at the start of the server
12 },
13 ])
14 ->start();
You can find the list of events and their respective format at this page.