Broadcast to all connected clients except sender with python flask socketio - python

I am following Alex Hadik's Flask Socketio tutorial which builds a very simple flask chat app.
http://www.alexhadik.com/blog/2015/1/29/using-socketio-with-python-and-flask-on-heroku
I would like to broadcast a message to all connected users except the sender. I have gone through the flasksocketio init.py but I'm still not sure how to do this.
Here's the server code.
from flask import Flask, render_template,request
from flask.ext.socketio import SocketIO,emit,send
import json,sys
app = Flask(__name__)
socketio = SocketIO(app)
clients = {}
#app.route("/")
def index():
return render_template('index.html',)
#socketio.on('send_message')
def handle_source(json_data):
text = json_data['message'].encode('ascii', 'ignore')
current_client = request.namespace
current_client_id = request.namespace.socket.sessid
update_client_list(current_client,current_client_id)
if clients.keys():
for client in clients.keys():
if not current_client_id in client:
clients[client].socketio.emit('echo', {'echo': 'Server Says: '+text})
def update_client_list(current_client,current_client_id):
if not current_client_id in clients: clients[current_client_id] = current_client
return
if __name__ == "__main__":
socketio.run(app,debug = False)
It's currently just broadcasting to all connected clients. I created a connected clients dict (clients) which stores the request.namespace indexed by the client id.
Calling clients[client].socketio.emit() for all clients except the sending client still results in the message being broadcast to call users.
Does anyone have any thoughts on how I can broadcast messages to all connected users except the sender?

You don't have to save users ids and manage individual emissions, you can specify a broadcast without the sender with emit('my_event', {data:my_data}, broadcast=True, include_self=False). In your case it would be something like this:
#socketio.on('send_message')
def handle_source(json_data):
text = json_data['message'].encode('ascii', 'ignore')
emit('echo', {'echo': 'Server Says: '+text}, broadcast=True, include_self=False)
If you have to send messages for a specific group of clients you can create rooms and then use emit('my_event', {data:my_data}, room=my_room, include_self=False) for sending messages to clients who joined my_room. You can check the reference of flask_socketio.emit for more details.

I can't comment on #Alex's response because I don't have enough reputation, but if you want to emit a broadcast message, this is how it is done in Python:
emit('echo', {'data':'what ever you are trying to send'}, broadcast=True)

https://flask-socketio.readthedocs.io/en/latest/#flask_socketio.SocketIO.emit
You're looking for the skip_sid parameter for socketio.emit().
skip_sid – The session id of a client to ignore when broadcasting or
addressing a room. This is typically set to the originator of the
message, so that everyone except that client receive the message. To
skip multiple sids pass a list.

You can try socket.broadcast.emit instead of socket.emit. I'm not sure if this works in the Python library, but it is the syntax for what you're looking for in Socket.io for Node.js.

Related

How to broadcast a message to all geventwebsocket clients

I'm setting up a geventwebscoket app in python using gevent-websocket.
In on of the examples (chat-app) that is pretty much the same as my application, I define an app that handles websocket connections and messages like this:
import json
from gevent import monkey
monkey.patch_all()
from flask import Flask, render_template
from werkzeug.debug import DebuggedApplication
from geventwebsocket import WebSocketServer, WebSocketApplication, Resource
flask_app = Flask(__name__)
flask_app.debug = True
class ChatApplication(WebSocketApplication):
def on_open(self):
print("Some client connected!")
def on_message(self, message):
if message is None:
return
message = json.loads(message)
if message['msg_type'] == 'message':
self.broadcast(message)
elif message['msg_type'] == 'update_clients':
self.send_client_list(message)
def send_client_list(self, message):
current_client = self.ws.handler.active_client
current_client.nickname = message['nickname']
self.ws.send(json.dumps({
'msg_type': 'update_clients',
'clients': [
getattr(client, 'nickname', 'anonymous')
for client in self.ws.handler.server.clients.values()
]
}))
def broadcast(self, message):
for client in self.ws.handler.server.clients.values():
client.ws.send(json.dumps({
'msg_type': 'message',
'nickname': message['nickname'],
'message': message['message']
}))
def on_close(self, reason):
print("Connection closed!")
#flask_app.route('/')
def index():
return render_template('index.html')
WebSocketServer(
('0.0.0.0', 8000),
Resource([
('^/chat', ChatApplication),
('^/.*', DebuggedApplication(flask_app))
]),
debug=False
).serve_forever()
I want to have some scheduled processes in my code that send a message to every client connected to the websocket.
In the examples and the limited documentation I find no way of calling the broadcast method from somewhere else in the project. Every message/broadcast has to be sent as a reaction to a received message (as I understand it).
I tried to figure it so I tried broadcasting a message every time someone visits the index page:
#flask_app.route('/')
def index():
chat_application = ChatApplication()
chat_application.broadcast("A new user on the page!")
return render_template('index.html')
This throws an error:
chat_application = ChatApplication()
TypeError: __init__() missing 1 required positional argument: 'ws'
Long story short:
I do not know how to send a message to every client on the websocket since I need the ChatApplication instance to access the broadcast function and I can't seem to figure out how to create a ChatApplication object to let me call that function.
I figured it out.
By starting a server like this
server = WebSocketServer(
('0.0.0.0', 8000),
Resource([
('^/chat', ChatApplication),
('^/.*', DebuggedApplication(flask_app))
]),
debug=False
)
server.serve_forever()
you can access all the clients and send them a message like this
for client in server.clients.values():
client.ws.send("whatever you want to send")

(flask + socket.IO) Result of emit callback is the response of my REST endpoint

Just to give a context here, I'm a node.JS developer, but I'm on a project that I need to work with Python using Flask framework.
The problem is, when a client request to an endpoint of my rest flask app, I need to emit an event using socket.IO, and get some data from the socket server, then this data is the response of the endpoint. But I didn't figured out how to send this, because flask needs a "return" statement saying what is the response, and my callback is in another context.
Sample of what I'm trying to do: (There's some comments explaining)
import socketio
import eventlet
from flask import Flask, request
sio = socketio.Server()
app = Flask(__name__)
#app.route('/test/<param>')
def get(param):
def ack(data):
print (data) #Should be the response
sio.emit('event', param, callback=ack) # Socket server call my ack function
#Without a return statement, the endpoint return 500
if __name__ == '__main__':
app = socketio.Middleware(sio, app)
eventlet.wsgi.server(eventlet.listen(('', 8000)), app)
Maybe, the right question here is: Is this possible?
I'm going to give you one way to implement what you want specifically, but I believe you have an important design flaw in this, as I explain in a comment above. In the way you have this coded, your socketio.Server() object will broadcast to all your clients, so will not be able to get a callback. If you want to emit to one client (hopefully not the same one that sent the HTTP request), then you need to add a room=client_sid argument to the emit. Or, if you are contacting a Socket.IO server, then you need to use a Socket.IO client here, not a server.
In any case, to block your HTTP route until the callback function is invoked, you can use an Event object. Something like this:
from threading import Event
from flask import jsonify
#app.route('/test/<param>')
def get(param):
ev = threading.Event()
result = None
def ack(data):
nonlocal result
nonlocal ev
result = {'data': data}
ev.set() # unblock HTTP route
sio.emit('event', param, room=some_client_sid, callback=ack)
ev.wait() # blocks until ev.set() is called
return jsonify(result)
I had a similar problem using FastAPI + socketIO (async version) and I was stuck at the exact same point. No eventlet so could not try out the monkey patching option.
After a lot of head bangings it turns out that, for some reason, adding asyncio.sleep(.1) just before ev.wait() made everything work smoothly. Without that, emitted event actually never reach the other side (socketio client, in my scenario)

Custom handshake data with Flask-SocketIO

I have a Python server using Flask, that has a websocket connection using Flask-SocketIO . There is a similar question :
Send custom data along with handshakeData in socket.io? The idea is to do the same thing but instead of using Node, using Flask. I want the client to send some data in the connect event, for example:
var socket = io("http://127.0.0.1:3000/", { query: "foo=bar" });
I haven't been able to get that custom data, and I can't rely on cookies due to the client's framework. A working solution is to have the connect event as usual, and then, in a custom event, get that information as a payload. But what we would want is to only have to do a connect event. Thanks!
As Miguel suggested in his comment you can simply do
from flask import Flask
from flask import request
app = Flask(__name__)
socketio = SocketIO(app)
#socketio.on('connect')
def connect():
foo = request.args.get('foo')
# foo will be 'bar' when a client connects
socketio.run(app)
on the server side.
The global connect method gets called on server side. But not my connect method for a specific namespace. Are namespaces supporting query parameter?
Client Code
const socket = io.connect('http://127.0.0.1:5000/ds', { query: {name: "Test"} });
Server Code
#socketio.on('connect', namespace='/ds')
def test_connect():
foo = request.args.get('foo')
print("CONNECT DS")
as of this time in 2021 you can send Auth data from python-socketio client with
sio.connect('http://localhost:6000/',auth={"authToken":"some auth token"})

Websocket connection between socket.io client and tornado python server

I'm trying to get websockets to work between two machines. One pc and one raspberry pi to be exact.
On the PC I'm using socket.io as a client to connect to the server on the raspberry pi.
With the following code I iniated the connection and try to send predefined data.
var socket = io.connect(ip + ':8080');
socket.send('volumes', { data: data });
On the raspberry pi, the websocket server looks like this:
from tornado import web, ioloop
from sockjs.tornado import SockJSRouter, SockJSConnection
class EchoConnection(SockJSConnection):
def on_message(self, msg):
self.send(msg)
def check_origin(self, origin):
return True
if __name__ == '__main__':
EchoRouter = SockJSRouter(EchoConnection, '/echo')
app = web.Application(EchoRouter.urls)
app.listen(8080)
ioloop.IOLoop.instance().start()
But the connection is never established. And I don't know why. In the server log I get:
WARNING:tornado.access:404 GET /socket.io/1/?t=1412865634790
(192.168.0.16) 9.01ms
And in the Inspector on the pc there is this error message:
XMLHttpRequest cannot load http://192.168.0.10:8080/socket.io/1/?t=1412865634790. Origin sp://793b6d4588ead99e1780e35b71d24d1b285328f8.hue is not allowed by Access-Control-Allow-Origin.
I am out of ideas and don't know what to do. Can you help me?
Thank you!
Well, the solution for your problem has to do with the internal design of the sockjs-tornado library more than with the socket.io library.
Basically, your problem has to do with cross origin request i.e. the html that is generating the request to the websocket server is not at the same origin as the websocket server. I can see from your code that you already identified the problem ( and you tried to solve it by redefining the method "check_origin") but you didn´t find the proper way to do it, basically because within this library is not the SockJSConnection class the one that extends tornado WebSocketHandler and so redefining its "check_origin" is useless. If you dig a little bit into the code, you will see that there exists one class defined, namely SockJSWebSocketHandler that has a redefinition of such method itself, which relies on the tornado implementation if it returns true, but that also allows you to avoid that check using a setting parameter :
class SockJSWebSocketHandler(websocket.WebSocketHandler):
def check_origin(self, origin):
***
allow_origin = self.server.settings.get("websocket_allow_origin", "*")
if allow_origin == "*":
return True
So, to summarize, you just need to include the setting "websocket_allow_origin"="*" in the server settings and everything should work properly =D
if __name__ == '__main__':
EchoRouter = SockJSRouter(EchoConnection, '/echo', user_settings={"websocket_allow_origin":"*"})

using redis in python flask to live send messages

Below is a simple app to send mesg to the browser. if there is a new mesg from the redis channel it will be sent other wise send the last know value in a non-blocking way.
But i am doing something wrong. can someone please help me understand
from gevent import monkey, Greenlet
monkey.patch_all()
from flask import Flask,render_template,request,redirect,url_for,abort,session,Response,jsonify
app = Flask(__name__)
myglobaldict = {'somedata':''}
class RedisLiveData:
def __init__(self, channel_name):
self.channel_name = channel_name
self.redis_conn = redis.Redis(host='localhost', port=6379, db=0)
pubsub = self.redis_conn.pubsub()
gevent.spawn(self.sub, pubsub)
def sub(self,pubsub):
pubsub.subscribe(self.channel_name)
for message in pubsub.listen():
gevent.spawn(process_rcvd_mesg, message['data'])
def process_rcvd_mesg(mesg):
print "Received new message %s " % mesg
myglobaldict['somedata'] = mesg
g = RedisLiveData("test_channel")
#app.route('/latestmessage')
def latestmessage():
return Response(myglobaldict,mimetype="application/json")
if __name__ == '__main__':
app.run()
on the javascript side i am just using a simple $.ajax get to view the messages.
but the client http://localhost:5000/latestmessage has the old message even after the redis update.
It should be the cache issue.
You can add a timestamp or a random number to the request http://localhost:5000/latestmessage?t=timestamp sent from the ajax.
I suggest you to use POST instead of GET as http method, you eliminate the caching problem and some annoying behaviour from browsers like chrome where the requests after the first will wait for the first to complete before being sent to the webserver.
If you want to keep the GET method then you can ask jquery to make the request non cache-able by the browser with the setting parameter cache
$.ajax(..., {cache:false})

Categories