Broadcasting message from grpc server to all/some connected clients in python - client-server

i am learning how to use grpc streams to exchange messages between clients and server in python. I found a base example that enables the simple message sending between server and client. I am trying to modify it so that i could keep track of all the clients connected to the grpc server (on the server side) and could do two things: 1) broadcast from server to all clients, 2) send message to a particular connected client.
Here is the .proto file
syntax = 'proto3';
service Scenario {
rpc Chat(stream DPong) returns (stream DPong) {}
}
message DPong {
string name = 1;
}
And here is the client.py that creates a daemon process to listen for incoming messages and waits for stdin for any outgoing messages
import threading
import grpc
import time
import scenario_pb2_grpc, scenario_pb2
# new changes
msgQueue = queue.Queue()
def run():
channel = grpc.insecure_channel('localhost:50052')
stub = scenario_pb2_grpc.ScenarioStub(channel)
print('client connected')
global queue
def inputStream():
while 1:
msg = input('>>Enter message\n>>')
yield scenario_pb2.DPong(name=msg)
input_stream = stub.Chat(inputStream())
def read_incoming():
while 1:
print('receivedFromServer: {}\n>>'.format(next(input_stream).name))
thread = threading.Thread(target=read_incoming)
thread.daemon = True
thread.start()
while 1:
time.sleep(1)
if __name__ == '__main__':
print('client starting ...')
run()
Below is the server.py
import random
import string
import threading
import grpc
import scenario_pb2_grpc
import scenario_pb2
import time
from concurrent import futures
clientList = []
class Scenario(scenario_pb2_grpc.ScenarioServicer):
def Chat(self, request_iterator, context):
clients = []
def stream():
while 1:
time.sleep(1)
msg = input('>>Enter message\n>>')
for i in clientList:
yield msg
output_stream = stream()
def read_incoming():
while 1:
received = next(request_iterator).name
if (context,request_iterator) not in clientList:
clientList.append((context, request_iterator))
print('receivedFromClient: {}'.format(received), len(clientList))
thread = threading.Thread(target=read_incoming)
thread.daemon = True
thread.start()
while 1:
msg = output_stream
yield scenario_pb2.DPong(name=next(msg))
if __name__ == '__main__':
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
scenario_pb2_grpc.add_ScenarioServicer_to_server(
Scenario(), server)
server.add_insecure_port('[::]:50052')
server.start()
print('listening ...')
while 1:
time.sleep(1)
So far, i have tried to maintain a list object clientList that contains the context & request_iterator object of the client, and is updated every time a new client joins the server. But how do i set these object from the clientList before sending out an outgoing message? I have tried to iterate the list but the server sends the message to the same client (the last client heard from) a number of times instead of sending it to all the clients once.
Any help is highly appreciated!

This is certainly possible. The problem that you're running into here is that each call to Scenario.Chat on the server side corresponds to a single client connection. That is, this function is called when the streaming RPC starts and as soon as the function exits, the RPC ends.
So if you want n connected clients, you'll need n instances of Scenario.Chat running concurrently, each on its own thread. This does mean that the number of concurrently connected clients is limited by the size of the threadpool with which you instantiate your server.
So, let's say you have n threads in your server process dedicated to maintaining client connections. Then you need another n+1th thread (perhaps the main thread) determining when the server will broadcast a message to all clients (maybe by looking for input from STDIN?). When this extra thread determines that a message should be broadcast, it needs to communicate this intent to all of the threads maintaining connections to a client. There are many ways to make this happen. A threading.Condition and a global collections.deque, or a collections.deque per client connection (somewhat like channels between goroutines) would be two ways. The tricky bit here is ensuring that each client connection will receive the message regardless of how long the client connection thread takes to wake up and how many messages the n+1th thread decides to send in the interim.
If this is still unclear, I can follow up with some actual code demonstrating the idea.

You can spin up multiple ports in one application.
gRPC can be running in port 50011 and flask with socket.io can be running in port 8080
with python, you can use the flask framework and flask_socketio library in your server.py
eg server.py
from flask import Flask
from flask_socketio import SocketIO, emit
app = Flask(__name__)
socketio = SocketIO(app)
#app.route('/')
def index():
return "Hello, World!"
if __name__ == '__main__':
app.run(port=8080)
app.run(debug=True)
socketio.run(app)
instead of using gRPC streaming API, use WebSocket to broadcast to all connected clients and specific/selected clients using rooms.
eg
#socketio.on('message')
def handle_message(data):
// logic to send large data in chunks the logic should call the
// emit function in socket.io and emit an event that send the large
// data in chunks eg emit('my response', chunkData)
gRPC is primarily built for one client request and response and WebSocket is for multiple clients.

Related

Scaling FastApi and Redis Pub/Sub for chat

trying to build a scalable chat app with FastApi and Redis pub/sub.
Suppose we have 10 processes running FastApi app. Each process will create 1 connection pool to Redis at startup. Redis instance allows max 10 connections. Each user has its own redis channel where all notifications (chat messages, app notifications etc) are coming. When the user connects to a websocket 2 tasks are launched, 1 that listens to websocket, and 1 that listens to redis user channel. Below is a simplified thing we have now.
resources.py
redis = None
async def startup_event():
global redis
redis = aioredis.from_url(url=REDIS_URL, password=REDIS_PASSWORD, encoding='utf-8', decode_responses=True)
async def get_redis() -> Redis:
return redis
views.py
import orjson as json
channel = 'user:channel'
async def listen_socket(
websocket: WebSocket,
redis: Redis, ):
while True:
try:
data = await websocket.receive_bytes()
except:
await redis.publish(channel, json.dumps({'type': 'disconnect_user'}))
return None
async def listen_redis(
websocket: WebSocket,
redis: Redis, ):
ps = redis.pubsub()
await ps.psubscribe(channel)
async for data in ps.listen():
if data['type'] == 'pmessage':
data = json.loads(data['data'])
event_type = data.get('type')
if event_type == 'disconnect_user':
return None
elif event_type == 'echo':
await websocket.send_bytes(json.dumps(data))
#router.websocket('/', name='ws', )
async def process_ws(
websocket: WebSocket,
redis: Redis = Depends(get_redis), ):
await websocket.accept()
await asyncio.gather(
listen_redis(
websocket=websocket,
redis=redis, ),
listen_socket(
websocket=websocket,
redis=redis, ),
)
This line async for data in ps.listen(): blocks the connection and this particular connection cannot serve clients on different threads of the same process, not even the current client. Is this true? If yes then this approach is absolutely not scalable, because we cannot afford 1 Redis connection per user.
What would solve the above issue? 2 Redis connections per process? 1 connection pool and 1 connection dedicated to consume redis pub/sub channel? In this case publishing will be done to a process channel not to a specific user channel. We would need a thread that consumes the pub/sub channel and routes to the user websocket connected to that process. Is this correct?
Am I overthinking?
Are there better approaches?
Thank you so much for help!
I recommend that you don't try to implement the functionality you describe manually just by using a fastapi and redis. Is a path of pain and suffering that is unjustified and highly ineffective.
Just use centrifugo and you'll be happy.
I recommend using queues to scale your real time application.
e.g. RabbitMQ or even rpush und lpop with redis lists - if you want stay with redis. this approch is much easier to implement as pub/sub and scales great.
Handling and sharing events bidirectional with Pub/Sub & WebSockets is a pain in most languages.

How to check for a websocket bottleneck in a python project

I currently have a script that connects to a server, makes a websocket connection and receives high frequency messages.
I am quite sure that the processing on my client end cannot keep up with the messages and thus i am getting behind after small periods of time.
My understanding is the messages are queued in both the servers sending buffer and in my clients receive buffer too, and if i do not process them quick enough evenutally the buffer will fill up and i will lose messages which will cause an out of sequence issue, is my assumption correct?
My question is, what is the best way (tools) to go about tracing possible bottle necks and track down if the issue is the server or the client? I am working with python in Visual Studio and have the single process running for now using PM2.
I am looking for advice on way to trace low level bottlenecks even if it means using tools like wireshark etc.
thanks.
My advice is to use gevent and gevent-websocket so that all the connections are async. Then you can do multiple connections asynchonously.
With GIPC, you could launch an instance per cpu core and load balance between ports.
example:
from gevent import monkey, socket, Timeout, sleep
monkey.patch_all()
import sys
pyver = sys.version_info[0]
if pyver == 3:
import signal
from gevent import signal_handler as sig
else:
from gevent import signal
import bottle
from bottle import route, request, response, abort
import ujson as json
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
from geventwebsocket import WebSocketError
import traceback
#route('/ws/app')
def handle_websocket():
global ws_users
ws = request.environ.get('wsgi.websocket')
if not ws:
abort(400, 'Expected WebSocket request.')
while 1:
message = None
try:
with Timeout(2, False) as timeout:
message = ws.receive()
if message:
message = json.loads(message)
# process message, report back with ws.send()
except WebSocketError:
break
except Exception as exc:
traceback.print_exc()
sleep(1)
if __name__ == '__main__':
print(socket.gethostname())
print('Started...')
botapp = bottle.app()
server = WSGIServer(("0.0.0.0", int(80)), botapp , handler_class=WebSocketHandler)
def shutdown():
print('Shutting down ...')
server.stop(timeout=60)
exit(signal.SIGTERM)
if pyver == 3:
sig(signal.SIGTERM, shutdown)
sig(signal.SIGINT, shutdown)
else:
signal(signal.SIGTERM, shutdown)
signal(signal.SIGINT, shutdown) #CTRL C
server.serve_forever()

Polling if I can PUSH or send in zmq?

By using 0mq, I am trying to detect if I have made a successful connection to a PULL port, and if I can PUSH. However, it didn't work as I had expected, see the example code below. Poller will return immediately even remote peer hasn't been started to accept connections. Is there a way to fix it?
import sys
import zmq
context = zmq.Context()
pusher = context.socket(zmq.PUSH)
pusher.connect("tcp://localhost:5555")
poller = zmq.Poller()
poller.register(pusher, zmq.POLLOUT)
socks = dict(poller.poll(timeout=1000))
if pusher in socks and socks[pusher] == zmq.POLLOUT:
print("Pusher can push")
else:
print("Failed to connect, exit.")
sys.exit(1)
You would be allowed to send as long as you haven't reached the High Water Mark ( HWM ) of the sending socket - the number of messages allowed to pile up on the sender side.
By default it is set to 1000 as far as I remember.
/Søren

ZeroMQ: Many-to-one no-reply aynsc messages

I have read through the zguide but haven't found the kind of pattern I'm looking for:
There is one central server (with known endpoint) and many clients (which may come and go).
Clients keep sending hearbeats to the server, but they don't want the server to reply.
Server receives heartbeats, but it does not reply to clients.
Hearbeats sent when clients and server are disconnected should somehow be dropped to prevent a heartbeat flood when they go back online.
The closet I can think of is the DEALER-ROUTER pattern, but since this is meant to be used as an async REQ-REP pattern (no?), I'm not sure what would happen if the server just keep silent on incoming "requests." Also, the DEALER socket would block rather then start dropping heartbeats when the send High Water Mark is reached, which would still result in a heartbeat flood.
The PUSH/PULL pattern should give you what you need.
# Client example
import zmq
class Client(object):
def __init__(self, client_id):
self.client_id = client_id
ctx = zmq.Context.instance()
self.socket = ctx.socket(zmq.PUSH)
self.socket.connect("tcp://localhost:12345")
def send_heartbeat(self):
self.socket.send(str(self.client_id))
# Server example
import zmq
class Server(object):
def __init__(self):
ctx = zmq.Context.instance()
self.socket = ctx.socket(zmq.PULL)
self.socket.bind("tcp://*:12345") # close quote
def receive_heartbeat(self):
return self.socket.recv() # returns the client_id of the message's sender
This PUSH/PULL pattern works with multiple clients as you wish. The server should keep an administration of the received messages (i.e. a dictionary like {client_id : last_received} which is updated with datetime.utcnow() on each received message. And implement some housekeeping function to periodically check the administration for clients with old timestamps.

In which manner zeroMQ read form multiple connections?

I want to know in which manner zeroMQ read form multiple connections ?
For example:
If I have a server which is connected to multiple clients and receiving data at the same time from all the clients which of the one it will read first?
Is it round robin or some other algorithm?
Use pub-sub routine, Python example:
#Publishing script
import zmq
ctx = zmq.Context()
socket_publish = ctx.socket(zmq.PUB)
socket_publish.bind("tcp://*:7787") #define socket for publishing
#subscribing script(s)
ctx = zmq.Context()
s = ctx.socket(zmq.SUB)
s.connect("tcp://127.0.0.1:7787") #connect to the socket multiple times
s.setsockopt(zmq.SUBSCRIBE,'')
msg = s.recv()

Resources