Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the python client not receiving SSE events?

I am have a python client listening to SSE events from a server with node.js API

The flow is I sent an event to the node.js API through call_notification.py and run seevents.py in loop using run.sh(see below)

However I don't see that python client is receiving this SSE event? any guidance on why is that?

call_notification.py

import requests
input_json = {'BATS':'678910','root_version':'12A12'}
url = 'http://company.com/api/root_event_notification?params=%s'%input_json
response = requests.get(url)
print response.text

node.js API

app.get("/api/root_event_notification", (req, res, next) => {
  console.log(req.query.params)
  var events = require('events');
  var eventEmitter = new events.EventEmitter();

  //Create an event handler:
  var myEventHandler = function () {
    console.log('new_root_announced!');
    res.status(200).json({
          message: "New root build released!",
          posts: req.query.params
        });
  }

seevents.py (python client listening to SSE events)

import json
import pprint
import sseclient

def with_urllib3(url):
    """Get a streaming response for the given event feed using urllib3."""
    import urllib3
    http = urllib3.PoolManager()
    return http.request('GET', url, preload_content=False)

def with_requests(url):
    """Get a streaming response for the given event feed using requests."""
    import requests
    return requests.get(url, stream=True)

url = 'http://company.com/api/root_event_notification'
response = with_urllib3(url)  # or with_requests(url)
client = sseclient.SSEClient(response)
#print client.events()
for event in client.events():
    print "inside"
    pprint.pprint(json.loads(event.data))

run.sh

#!/bin/sh

while [ /usr/bin/true ]
do
    echo "Running sseevents.py"
    python sseevents.py  2>&1 | tee -a sseevents.log.txt

    echo "sleeping for 30 sec"
    sleep 30
done

OUTPUT:-

Run call_notification.py on Terminal

node.js API OUTPUT

new_root_announced!
{'root_version': 'ABCD', 'BATS': '143'}

./run.sh --> DON'T SEE ABOVE EVENT below
Running sseevents.py
sleeping for 30 sec
Running sseevents.py
sleeping for 30 sec
Running sseevents.py
sleeping for 30 sec
like image 905
carte blanche Avatar asked Sep 12 '19 19:09

carte blanche


1 Answers

Very short answer to you question:

The server code is not sending a SSE message back to the client.

Why? Because you need to follow the SSE format.

According to JASON BUTZ in Server-Sent Events With Node

You should send a Connection: keep-alive header to ensure the client keeps the connection open as well. A Cache-Control header should be sent with the value no-cache to discourage the data being cached. Finally, the Content-Type needs to be set to text/event-stream.

With all of that done a newline (\n) should be sent to the client and then the events can be sent. Events must be sent as strings, but what is in that string doesn’t matter. JSON strings are perfectly fine.

Event data must be sent in the format "data: <DATA TO SEND HERE>\n".

It’s important to note that at the end of each line should be a newline character. To signify the end of an event an extra newline character needs to be added as well.

Multiple data lines are perfectly fine.

Long answer to your question:

According to Eric Bidelman in html5rocks.com:

When communicating using SSEs, a server can push data to your app whenever it wants, without the need to make an initial request. In other words, updates can be streamed from server to client as they happen.

But, in order for this to happen, the client has to "start" by asking for it AND prepare to receive a stream of messages (when they happen).

  • The "start" is done by calling a SSE API endpoint (in your case, calling the Node.js API code).
  • The preparation is done by preparing to handle a stream of asynchronous messages.

SSEs open a single unidirectional channel between server and client.*

* The emphasis is mine

This means that the server has a "direct" channel to the client. It is not intended to be "started" (opened) by some other process/code that is not "the client" code.

Assuming from OP comments...

Expected behavior (verbose)

  1. A client Alice calls the API endpoint with params {name: "Alice"}, nothing (visible) happens.

  2. ...then a client Bob calls the API endpoint with params {name: "Bob"}, client Alice receives a SSE with payload {name: "Bob", says: "Hi"}.

  3. ...then a client Carol calls the API endpoint with params {name: "Carol"}, clients Alice AND Bob each one receives a SSE with payload {name: "Carol", says: "Hi"}.

  4. ...and so on. Every time a new client calls the API endpoint with params, every other client who has a channel "open" will receive a SSE with the new "Hi" payload.

  5. ...and then client Bob "disconnects" from the server, client Alice, client Carol and all the clients that have a channel "open" will receive a SSE with payload {name: "Bob", says: "Bye"}.

  6. ...and so on. Every time an old client "disconnects" from the server, every other client who has a channel "open" will receive a SSE with the new "Bye" payload.

Abstracted behavior

  • Each new client that asks to "open" a channel sending some params or an old client "disconnects" from the server, they cause and event in the server.
  • Every time such an event happens in the server, the server sends a SSE message with the params and a message as payload to all the "open" channels.

Note on blocking Each client with an "open" channel will be "stuck" in an infinite waiting loop for events to happen. It is client design responsibility to use "threading" code techniques to avoid blocking.

Code

Your Python client should "ask" to start the single unidirectional channel AND keep waiting UNTIL the channel is closed. Should not end and start all over again with a different channel. It should keep the same channel open.

From the network perspective, it will be like a "long" response that does not end (until the SSE messaging is over). The response just "keeps coming and coming".

Your Python client code does that. I noted it is the exact sample code used from sseclient-py library.

Client code for Python 3.4

To include the parameters you want to send to the server, use some code from the Requests library docs/#passing-parameters-in-urls.

So, mixing those samples we end up with the following code as your Python 3.4 client:

import json
import pprint
import requests
import sseclient # sseclient-py

# change the name for each client
input_json = {'name':'Alice'}
#input_json = {'name':'Bob'}
#input_json = {'name':'Carol'}

url = 'http://company.com/api/root_event_notification'
stream_response = requests.get(url, params=input_json, stream=True)

client = sseclient.SSEClient(stream_response)

# Loop forever (while connection "open")
for event in client.events():
    print ("got a new event from server")
    pprint.pprint(event.data)

Client code for Python 2.7

To include the parameters you want to send to the server, encode them in the URL as query parameters using urllib.urlencode() library.

Make the http request with urllib3.PoolManager().request() so you will end up with a stream response.

Note that the sseclient library returns event data as unicode string. To convert back the JSON object to python object (with python strings) use byteify, a recursive custom function ( thanks to Mark Amery ).

Use the following code as your Python 2.7 client:

import json
import pprint
import urllib
import urllib3
import sseclient # sseclient-py

# Function that returns byte strings instead of unicode strings
# Thanks to:
# [Mark Amery](https://stackoverflow.com/users/1709587/mark-amery)
def byteify(input):
    if isinstance(input, dict):
        return {byteify(key): byteify(value)
                for key, value in input.iteritems()}
    elif isinstance(input, list):
        return [byteify(element) for element in input]
    elif isinstance(input, unicode):
        return input.encode('utf-8')
    else:
        return input

# change the name for each client
input_json = {'name':'Alice'}
#input_json = {'name':'Bob'}
#input_json = {'name':'Carol'}

base_url = 'http://localhost:3000/api/root_event_notification'
url = base_url + '?' + urllib.urlencode(input_json)

http = urllib3.PoolManager()
stream_response = http.request('GET', url, preload_content=False)

client = sseclient.SSEClient(stream_response)

# Loop forever (while connection "open")
for event in client.events():
    print ("got a new event from server")
    pprint.pprint(byteify(json.loads(event.data)))

Now, the server code should:

  1. emit an inside-server 'hello' event so other clients listen to the event
  2. "open" the channel
  3. Register to listen for all possible inside-server events to happen (this means, keeping the channel "open" and not sending anything between messages, just keeping the channel "open").
    • This includes to emit an inside-server 'goodbye' event so other clients listen to the event WHEN channel is closed by the client/network (and finally "wrap up").

Use the following Node.js API code:

var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;


function registerEventHandlers(req, res) {
  // Save received parameters
  const myParams = req.query;

  // Define function that adds "Hi" and send a SSE formated message
  const sayHi = function(params) {
      params['says'] = "Hi";
      let payloadString = JSON.stringify(params);
      res.write(`data: ${payloadString}\n\n`);
  }

  // Define function that adds "Bye" and send a SSE formated message
  const sayBye = function(params) {
      params['says'] = "Bye";
      let payloadString = JSON.stringify(params);
      res.write(`data: ${payloadString}\n\n`);
  }

  // Register what to do when inside-server 'hello' event happens
  myEmitter.on('hello', sayHi);

  // Register what to do when inside-server 'goodbye' event happens
  myEmitter.on('goodbye', sayBye);

  // Register what to do when this channel closes
  req.on('close', () => {
      // Emit a server 'goodbye' event with "saved" params
      myEmitter.emit('goodbye', myParams);

      // Unregister this particular client listener functions
      myEmitter.off('hello', sayHi);
      myEmitter.off('goodbye', sayBye);
      console.log("<- close ", req.query);
  });
}


app.get("/api/root_event_notification", (req, res, next) => {
    console.log("open -> ", req.query);

    // Emit a inside-server 'hello' event with the received params
    myEmitter.emit('hello', req.query);

    // SSE Setup
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
    });
    res.write('\n');

    // Register what to do when possible inside-server events happen
    registerEventHandlers(req, res);

    // Code execution ends here but channel stays open
    // Event handlers will use the open channel when inside-server events happen
})

...continue quoting Eric Bidelman in html5rocks.com:

Sending an event stream from the source is a matter of constructing a plaintext response, served with a text/event-stream Content-Type, that follows the SSE format. In its basic form, the response should contain a "data:" line, followed by your message, followed by two "\n" characters to end the stream

In the client code, the sseclient-py library takes care of interpreting the SSE format so every time the two "\n" characters arrive, the library "iterates" a new "iterable" object (a new event) that has the data property with the message sent from the server.

This is how I tested the code

  1. Started server with Node.js API code
  2. Run a client with only the "Alice" line uncommented (Nothing is seen on this client console yet).
  3. Run a second client with only "Bob" line uncommented. The console of the first client "Alice" shows: Bob saying "Hi" (Nothing is seen on Bob's client console yet).
  4. Run a third client with only "Carol" line uncommented. Alice's and Bob's consoles show: Carol saying "Hi" (Nothing is seen on Carol's client console yet).
  5. Stop/kill Bob's client. Alice's and Carol's consoles show: Bob saying "Bye".

So, code works OK :)

like image 84
elingerojo Avatar answered Oct 13 '22 23:10

elingerojo