I'm trying to wrap my head around the channel features of Google App Engine since they don't (easily) provide websockets.
My current situation is that I have a long work (file processing) that is being executed asynchronously via a worker.
This worker update the state of the file processing in the database at every lines in order to inform the customer.
From that current perspective, a F5 will indicate the evolution of the processing.
Now I'd like to implement a live update system. Of course I could do an XHR request every 5 seconds but a live connection seems better... introducing Channels since Websockets seems out of the possibilities.
From what I understood, I can channel.send_message to one client only, not to a "room". The issue here, is that the worker that process the file does not have any information which customer is currently connected (could be one, could be ten).
I could loop over all the customer and post to each client_id, suspecting that at least one of them will get the message, but this is awfully useless and too resourceful.
I was hoping there was a better way to achieve this ? Maybe a nice alternative to Google Channels feature without having to reconfigure my whole App Engine system (like for Websockets)?
One solution I can think of, which is not the absolute ideal but would be more suited, is to manage dedicated database tables (could also be implemented in Memcache) with :
A table that contains a list of rooms
A table that contains a list of client_id connected to the room
e.g. :
Rooms (id, name)
Clients (id, room_id, client_id)
Now, instead of posting to channel.send_message(client_id, Message), one would make a wrapper like this :
def send_to_room(room, message):
# Queries are SQLAlchemy like :
room = Rooms.query.filter(Rooms.name === room).first()
if not room:
raise Something
clients = Clients.query.filter(Rooms.room_id === room.id).all()
for client in clients:
channel.send_message(client.client_id, message)
And voilà, you have a Room like implementation in Google App Engine.
The drawback of this solution is to add two tables (or equivalent) in your database.
Does someone has better?
I am assuming that the long running task is being kicked off by the client.
So before you kick off the task make a ajax request from the client to a handler similar to this one. This handler has two things returned to the client. The token param which is used by the javascript api to create a channel, and a cid param which is used to determine which client created the channel.
from google.appengine.api import channel
#ae.route("/channel")
class CreateChannel(webapp2.RequestHandler):
def get(self):
cid = str(uuid.uuid4())
token = channel.create_channel(cid)
data = {
"cid":cid,
"token":token
}
self.response.write(json.dumps(data))
Now use the channel javascript api to create a new channel
https://cloud.google.com/appengine/docs/python/channel/javascript
var onClosed = function(resp){
console.log("channel closed");
};
var onOpened = function(resp){
console.log("channel created");
};
var onmessage = function(resp){
console.log("The client received a message from the backend task");
console.log(resp);
};
var channel_promise = $.ajax({
url: "/channel",
method: "GET"
});
channel_promise.then(function(resp){
//this channel id is important you need to get it to the backend process so it knows which client to send the message to.
var client_id = resp.data.cid;
var channel = new goog.appengine.Channel(resp.data.token);
handler = {
'onopen': $scope.onOpened,
'onmessage': $scope.onMessage,
'onerror': function () {
},
'onclose': function () {
alert("channel closed.")
}
};
socket = channel.open(handler);
//onOpened is the callback function to call after channel has been created
socket.onopen = onOpened;
//onClose is the callback function to call after channel has been closed
socket.onclose = onClosed;
//onmessage is the callback function to call when receiving messages from your task queue
socket.onmessage = onMessage;
});
Now we are all set up to listen for channel messages.
So when the user clicks the button we need to kickoff the backend task.
var letsDoSomeWorkOnClick = function(){
//make sure you pass the client id with every ajax request
$.ajax({
url: "/kickoff",
method: "POST",
params: {"cid":client_id}
});
}
Now the app engine handler to start the backend task queue. I use the deffered library to do this. https://cloud.google.com/appengine/articles/deferred
#ae.route("/kickoff")
KickOffHandler(webapp2.RequestHandler):
def post(self):
cid = self.request.get("cid")
req = {}
req['cid'] = cid
task = MyLongRunningTask()
deferred.defer(task.long_runner_1, req, _queue="my-queue")
example task:
class MyLongRunningTask:
def long_runner_1(self,req):
# do a whole bunch of stuff
channel.send_message(req["cid"], json.dumps({"test":"letting client know task is done"})
Related
I'm new to webrtc,i liked to make simple application where client send video and audio stream to server using webrtc and from server i will use the video frame for detecting object in the video using opencv, i have implemented simple server side code using docs of aiortc package,but i was stuck,because the on_track never called i dont know whats wrong with my code
Client code
const { RTCPeerConnection, RTCSessionDescription } = window;
socket = io()
const peerConnection = new RTCPeerConnection()
const offerAsync = peerConnection.createOffer();
offerAsync.then(async offer=>{
await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
socket.emit("connect_to_server", {
offer
});
})
socket.on("connect_to_server_response",async function(data){
navigator.mediaDevices.getUserMedia({
video:true,
audio:true
}).then(async stream=>{
const video = document.getElementById("video")
await peerConnection.setRemoteDescription(
data
);
console.log(data)
stream.getTracks().forEach(track => {
console.log("this is track")
peerConnection.addTrack(track, stream)
});
console.log(video)
video.srcObject = stream
video.play()
}).catch(err=>{
console.log(err)
console.log("something went wrong please connect administrator, error code = [100]")
})
})
console.log("working");
Server code
async def res(data):
offer = RTCSessionDescription(sdp=data["offer"]["sdp"], type=data["offer"]["type"])
pc = RTCPeerConnection()
#pc.on("track")
def on_track(track):
print("on Track")
print(track.kind)
await pc.setRemoteDescription(offer)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
emit("connect_to_server_response", {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type})
#socketio.on("connect_to_server")
def connect_to_server(data):
asyncio.set_event_loop(asyncio.SelectorEventLoop())
asyncio.get_event_loop().run_until_complete(res(data))
print(data)
print(request.sid)
print("new user joint")
It looks like you are exchanging the SDP messages fine and in the right order.
Your issue is because you are never exchanging ICE candidates. In order to establish a connection, you will need to emit ice candidate messages from the js client to the python client as well as from the python client to the js client.
You need to define an event handler for RTCPeerConnection.onicecandidate (this will start getting fired once you call createOffer) on both sides and emit these candidates that you're receiving. Then you have to RTCPeerConnection.addIceCandidate once you receive the candidates.
Once you successfully do that, you should be able to get the on_track event.
Check out these links for reference:
What are ICE Candidates and how do the peer connection choose between them?
RTCIceCandidate
Simple webrtc workflow sample code
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
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)
A client Alice calls the API endpoint with params {name: "Alice"}, nothing (visible) happens.
...then a client Bob calls the API endpoint with params {name: "Bob"}, client Alice receives a SSE with payload {name: "Bob", says: "Hi"}.
...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"}.
...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.
...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"}.
...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:
emit an inside-server 'hello' event so other clients listen to the event
"open" the channel
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
Started server with Node.js API code
Run a client with only the "Alice" line uncommented (Nothing is seen on this client console yet).
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).
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).
Stop/kill Bob's client. Alice's and Carol's consoles show: Bob saying "Bye".
So, code works OK :)
I am trying to create a grpc python server that can keep track of all clients connected.
I am referencing a talk/demo that Ray Tsang did where he kept a collection of StreamObservers and just iterated through them to send to all the clients. Here is a video of that for reference.
Now my question is how do you get a StreamObserver in python? I only see self, request and context as being available to me in the definition.
This is my first python project so there might be something obvious I am missing here.
Here is my proto, its basically the sample proto
syntax = "proto3";
package hellostreamingworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends another greeting
rpc SayHelloAgain (HelloRequest) returns (stream HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
If I have understand you need to create a class ClientInterceptor that extends grpc.UnaryUnaryClientInterceptor(https://grpc.io/grpc/python/grpc.html?highlight=unaryunaryclientinterceptor#grpc.UnaryUnaryClientInterceptor)
and then assign it with the intercept_channel method in this way
self.channel = grpc.insecure_channel(address, options)
self.channel = grpc.intercept_channel(
self.channel,
ClientInterceptor()
)
You can use the intercept_unary_unary method for receive infos about the various clients.
If you want to have infos server side extends the ServerInterceptor(https://grpc.io/grpc/python/grpc.html?highlight=serverinterceptor#grpc.ServerInterceptor) and assign it on the server init
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=1000),
options=(('grpc.max_send_message_length', 1000 * 1024 * 1024,),
('grpc.max_receive_message_length', 1000 * 1024 * 1024,),
),
interceptors=(MyServerInterceptor())
)
And the use the intercept_service method for receive infos.
What you probably need is a bidirectional stream service. That is both the client and server are continuously sending data over. In this case gRPC + python will keep track of your client!
Check out this example code and corresponding explanation.
I'm new to Flask and working on creating a web application that will listen and monitor for status updates with callback urls from our API. If a particular event is performed, the application will download the files in a pdf format.
The application will use the user's integration key to access the management console. The app will route the user to a selection screen where the user will get to select between downloading options.
#app.route('/tool', methods=['GET', 'POST'])
def tool():
# Empty dict
agreement_list = {}
# Iterate through session cookies to extract out the name & agreement ID
for agreement in session['agreementID']['userAgreementList']:
if agreement['esign'] == True and agreement['status'] != 'SIGNED':
agreement_list[agreement['name']] = agreement['agreementId']
# If the user clicks the download button
if request.method == 'POST':
# extract the data from the selection field
session['config_selected'] = request.form.get('select_name')
# Store into session
session['agreements'] = agreement_list
return redirect(url_for('scan'))
return render_template('tool.html', agreements=agreement_list)
Once the button is clicked, the app will route the user to a page where it will monitor for updates and download the file when there's an update.
#app.route('/scan', methods=['Get'])
def scan():
def createPDF(file_selected, agreementID, config_selected):
"""
This sub function will create a pdf requested by the user. It will use an API call
depending on which file is selected. It does both single and multiple pdf creations.
:param page_id: A list of selected downloading option the user selected
:param agreementID: Agreement ID that the user selected
:param select: name of the selected file use to help name pdf
:return: N/A
"""
# Iterate through the list of selected options that the user selected
for file in file_selected:
# API request
r = requests.get('Some request url from API' + agreementID + file,
headers=session['header'])
file_name = config_selected + ' - ' + id[1:] + '.pdf'
file_name = file_name.replace(' ', '_')
# Write file to pdf
with open(file_name, 'wb') as f:
f.write(r.content)
f.close()
# call createPDF here
return render_template('scan.html')
Attempts
After researching around, I found that I should use sockets for communication between the server and the client. I tried to implement Flask-SocketIO and got it running.
#socketio.on('message')
def handleMessage(msg):
print('Message: ' + msg)
send(msg, broadcast=True)
Currently the application is running on my local machine and it's currently being able to do polling on that particular page.
$(document).ready(function () {
var socket = io.connect('http://127.0.0.1:5000');
socket.on('connect', function () {
socket.send('User has connected!');
});
// socket.on('message', function () {
// socket.send('Scanning')
// });
});
Question:
How would I get the live callback updates when the end user performs the task that I am looking out for? Do I have to connect the socket to the API server for the callbacks? Or is there another way I can possibly get the callback updates using the integration key since I have access to the management UI using HTTP request.
I finally figured out what was wrong and why I wasn't getting any event pings from the API server. It turns out that my localhost needed to be publicly accessible in order to receive event changes from the server. My solution was to us ngrok to create a secure tunnel from my localhost out to the public endpoint. Once that was achieved, I added the callback url, which in turn allowed the server to ping me with event changes.
I am creating a small application to test how GAE Channel API works. I think I have done all as it's described in the documentation but when I launch it, it shows an error in FireFox error log about syntax in the beginning and then another repeating error that an element wasn't found that.
Here is the first error info:
Source: http://127.0.0.1:8080/_ah/channel/dev?command=connect&channel=channel-773698929-185804764220139124118
Line 1, symbol 1
Here is the url where my javascript code tries to connect repeatedly and it raises the second error:
http://127.0.0.1:8080/_ah/channel/dev?command=poll&channel=channel-2071442473-185804764220139124118&client=1
I get the token through a JSON request with jQuery $.get. Then I run this code to get the token and open the channel. The error begins to show just when I run socket = channel.open(handler):
var response = JSON.parse(data);
var token = response.token.toString();
channel = new goog.appengine.Channel(token);
var handler = {
'onopen': onOpened,
'onmessage': onMessage,
'onerror': function() {
},
'onclose': function() {
}
};
socket = channel.open(handler);
Here is the server side code in Python to open the channel:
class OpenChannel(webapp.RequestHandler):
def get(self):
user = users.get_current_user()
token = channel.create_channel(user.user_id())
serialized = json.dumps({'token': token})
self.response.headers['Content-Type'] = "application/json"
self.response.out.write(serialized)
What's my error and what can I do? Thanks!
It seems that Channel API works on localhost different way than on GAE hosting. I uploaded it to the cloud and it works well now. Though it looks like it working fine on the local computer, it shows permanent JS error repeating in the error log.
You could try removing the handler argument and adding the handlers as methods of the socket object i.e. socket.onopen = function() {}; etc. That worked for me. But you are right. According to this, you should be able to get this working by using the handler argument. Hmm.