1

I have a twig template and I am trying to send message having same token in url Meaning message send from http://url/{token1} from should only be recived by another url with same token.

But I am not being able to pass any message from twig to node to symfony.

index.html.twig

<div id="chat">

</div>
<div>
    <div class="form-group">
        <label for="name">Name:</label> <input type="text" id="name">
    </div>
    <div class="form-group">
        <label for="message">Message:</label> <input type="text" id="message">
    </div>
    <button type="button" id="sendBtn" class="btn-primary">Send</button>
</div>

<script src="/bin/ws-broker.js"></script>
<script>
    const socket = new WebSocket("ws://localhost:8080");
    document.getElementById("sendBtn").addEventListener("click", function() {
        const message = {
            name: document.getElementById("name").value,
            message: document.getElementById("message").value
        };
        socket.send(JSON.stringify(message));
    });
</script>

/bin/ws-broker.js

const WebSocket = require('ws');
const qs = require('querystring');
const wss = new WebSocket.Server({ port: 8080 });
const http = require('http');

    wss.on('connection', function connection(ws, req)
    {
        console.log('Connection Received from IP: ' + req.socket.remoteAddress);
        ws.on('message', function incoming(message) {
            if ('' === message) {
                //empty message
                return;
            }
            try {
                //process message as JSON object
                message = JSON.parse(message);
            } catch (e) {
                //failed parsing the message as JSON
                return;
            }
    
        });
    });
    
    console.log('Listening on port 8080');

Controller

    /**
     * @Route("/{token}", name="home")
     */
    public function index(): Response
    {
        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
        ]);
    }
Will B.
  • 17,883
  • 4
  • 67
  • 69
S S
  • 1,443
  • 2
  • 12
  • 42
  • 1
    The `/bin/ws-broker.js` that I provided is not meant to go on the frontend at all. It has to run as a server through nodejs on the backend. eg: `node bin/ws-broker.js` to replace `symfony bin/console --env=prod app:command` – Will B. Jun 17 '21 at 04:31
  • There are lots of things that could potentially block websockets. This page alone lists a dozen or so: https://support.grammarly.com/hc/en-us/articles/360000934011-WebSockets-Configuration-Troubleshooting. SUGGESTION: 1) Focus on your dev environment first. Write a pair of simple "hello world" apps just to verify you can send a message from a client to a server. 2) Then expand your scope from there. 3) Once you're comfortable end::end communication works in your dev environment, test in a more "realistic" environment. – paulsm4 Jun 17 '21 at 04:40
  • @Will B. Yes I have already started node – S S Jun 17 '21 at 05:27
  • Where is the `token` coming from? It will need to be sent from the client-side javascript to the node websocket listener `bin/ws-broker.js`. Also how should the responses be handled by the client? – Will B. Jun 17 '21 at 21:07
  • This is an identical question and not a follow-up question: [How to arguments into the Websocket handler?](https://stackoverflow.com/questions/67836478/how-to-arguments-into-the-websocket-handler) – Martin Zeitler Jun 17 '21 at 22:48
  • @MartinZeitler This is a follow-up question, since the original was related to Symfony dependency injection. I suggested not using Ratchet in my answer which spawned this question. – Will B. Jun 17 '21 at 22:52

1 Answers1

1

There appears to be some confusion surrounding the NodeJS WebSocket broker service and the Browser WebSocket client.

For clarification, the WebSocket broker runs on the backend as a server alongside Apache, in order to listen for messages being sent from browser WebSocket clients. In the same way that Apache listens for HTTP Requests from browser clients.

The WebSocket broker server receives a message then forwards the data as an HTTP Request so that Symfony can handle the request and send back a response that the broker passes back to the WebSocket client.

The process is identical to how Ratchet listens but without the overhead of Symfony and Doctrine being loaded in a perpetually running state by using php bin/console app:command. With the exception that instead of being in the Symfony environment already, bin/ws-broker.js creates an HTTP request to send to Symfony.

The bin/ws-broker.js process breakdown goes like this.

Browser HTTP Request -> /path/to/index -> 
  Symfony AppController::index() -> 
  return Response(Twig::render('index.html.twig')) -> 
    WebSocket Client - socket.send(message) -> 
      [node bin/ws-broker.js WebSocket Server - ws.on('message')] ->
      [node bin/ws-broker.js http.request()] -> /path/to/score-handler -> 
        Symfony AppController::scoreHandler() -> 
        return JsonResponse(data) ->  
      [node bin/ws-broker.js WebSocket Server - ws.send(response) to WebSocket Client] ->
    WebSocket Client - socket.onmessage()

The bin/console app:command Ratchet process breakdown goes like this.

Browser HTTP Request -> /path/to/index -> 
  Symfony AppController::index() -> 
  return Response(Twig::render('index.html.twig')) -> 
    WebSocket Client - socket.send(message) -> 
      [php bin/console app:command - Ratchet\MessageComponentInterface::onMessage()] ->
      [php bin/console app:command - $client->send(response) to WebSocket Client] ->
    WebSocket Client - socket.onmessage()

Add the Symfony HTTP Request and Response handler to the NodeJS WebSocket Listener Service

// bin/ws-broker.js

const WebSocket = require('ws');
const qs = require('querystring');
const http = require('http');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws, req) {
    console.log('Connection Received from IP: ' + req.socket.remoteAddress);
    ws.on('message', function incoming(message) {
        if ('' === message) {
            //empty message
            return;
        }

        try {
            //process message as JSON object
            message = JSON.parse(message);
        } catch (e) {
            //failed parsing the message as JSON
            return;
        }

        //convert the WS message to a query string for the Symfony Request object
        let postData = qs.stringify({
            "name": message.name, 
            "message": message.message
        });
        let http_options = {
            host: 'localhost',
            path: '/' + message.token,
            port: 8000,
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Content-Length': postData.length
            }
        };

        //forward the message to Symfony using an HTTP Request
        let req = http.request(http_options, function(res) {
            res.setEncoding('utf8');
            //process the Symfony response data
            let data = '';
            res.on('data', (chunk) => {
                data += chunk
            });

            //send the Symfony Response back to the WebSocket client
            res.on('end', function() {
                ws.send(data);
            });
        });

        //send the requested message to Symfony
        req.write(postData);
        req.end();
    });
});

console.log('Listening on port 8080');

Run the WebSocket Broker Server

node bin/ws-broker.js &

Add a route to handle the Broker Request and send a Response back to the Broker. Add the token to the home/index.html.twig context.

class AppController extends AbstractController
{
    /**
     * @Route("/{token}", name="score_handler", requirements={ "token":"\w+" } methods={ "POST" })
     */
    public function scoreHandler(string $token): JsonResponse
    {
        //called by bin/ws-broker.js

        //do things here...

        return $this->json([
            'data' => 'Message Received!'
        ]);
    }

    /**
     * @Route("/{token}", name="home", requirements={ "token":"\w+" } methods={ "GET" })
     */
    public function index(string $token): Response
    {
        //called by Apache/Browser

        return $this->render('home/index.html.twig', [
            'controller_name' => 'HomeController',
            'token' => $token
        ]);
    }
}

Remove the bin/ws-broker.js from the frontend, send the token to the Broker and add a response handler

<!-- home/index.html.twig -->

<div id="chat"></div>
<div>
    <div class="form-group">
        <label for="name">Name:</label> <input type="text" id="name">
    </div>
    <div class="form-group">
        <label for="message">Message:</label> <input type="text" id="message">
    </div>
    <button type="button" id="sendBtn" class="btn-primary">Send</button>
</div>

<script type="text/javascript">
    const socket = new WebSocket("ws://localhost:8080");
    socket.onmessage = function(evt) {
        window.console.log(evt.data);
        //handle WebSocket Server Response...
    };
    document.getElementById("sendBtn").addEventListener("click", function() {
        const message = {
            name: document.getElementById("name").value,
            message: document.getElementById("message").value,
            token: "{{ token }}" //<------ SEND TOKEN to WebSocket Server
        };
        socket.send(JSON.stringify(message));
    });
</script>
Will B.
  • 17,883
  • 4
  • 67
  • 69
  • Many thanks for your explanation. Token is coming from the URL. Eg. `http://localhost:8000/unique_token` With this URL hit then my `index.html.twig` page is rendered And on send button click next person with same token url should get the message – S S Jun 18 '21 at 06:14
  • Updated for `home` route to support token in the URL during GET requests. – Will B. Jun 18 '21 at 12:46
  • I have opened 2 tabs with same URL and token. But message from one tab is not received in another. – S S Jun 18 '21 at 16:01
  • If you are wanting to broadcast the message to all of the clients, you have to change the `ws.send()` to iterate over all the clients. [Server Broadcast](https://github.com/websockets/ws#server-broadcast) Next question would be, are you trying to make channels for each of the messages based on the `token`? If so that changes the scope of your new issue significantly, since it would require setting conditions as to which clients to send. – Will B. Jun 18 '21 at 16:40
  • @SS for clarification, my recommendation was only on how to replace Ratchet for nodejs to alleviate the overhead of Symfony+Doctrine. Which was not implemented correctly in this question which this answer fixed. Please update your question with the new code you have now, so we can walk through the `token` channels issue. – Will B. Jun 18 '21 at 16:45
  • I will update this question tomorrow. I went through other nodejs tutorials. I thought `res.on('end', function() { ws.send(data); });` is same as ws.send() – S S Jun 18 '21 at 17:07
  • 1
    Here is a good example of how to implement a token/UserID channel system. [Sending message to a specific connected users using webSocket?](https://stackoverflow.com/a/19469286/1144627]) Adapt the code there with the Symfony process in my example `res.on('end', function() { toUserWebSocket.send(data); })` – Will B. Jun 18 '21 at 17:47
  • I have more problem with node. So I have created a new question `https://stackoverflow.com/questions/68052855/sending-message-to-users-with-same-token-using-websocket` For this question, I have removed code for connecting with Symfony . Thanks. – S S Jun 20 '21 at 05:11