Python and WebSockets - how to send from server to client without blocking? - python

Can anybody explain, or better yet provide a working example, of how to write a Python client and server where the server can initiate a send via WebSocket at any arbitrary time based on processing logic, and the client can receive at any time? Both have to be freed up to perform other processing activities as their majority purpose.
I've Googled extensively for this, including looking into Tornado, which seems to be the classic Python option and websockets, and asyncio which seem to be the newer way. In either case, every working example I can find is based on the client initiating the communication. Some cover sending from server to client as an echo where the client's logic initiates the series of communication, but I need to get an example working where the server initiates the communication.
Many Tornado examples are timeout based which won't work in my case. The websockets and asyncio examples all are either client to server and/or have a line like somethingOrOther.run() which blocks execution on the client or server or both. Also, many examples have one of either the client or the server written in Python but the other written in something else like JavaScript (I need both in Python).
I don't need the connection to be full-duplex, that is to say, the client never has to send to the server on the same socket, except perhaps for sending back an acknowledgment.
This question seems to be similar, however, the poster's code is not complete, there is no accepted answer, and the one answer provided (not accepted) does not contain complete code, and the client is in JavaScript (I need both in Python).
Here is an example of what I'm trying to do using regular sockets:
server:
# server.py
import socket
import time
import random
# module-level variables
HOST='127.0.0.1'
PORT=65439
ACK_TEXT = 'ack_text'
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((HOST, PORT))
sock.listen()
conn, addr = sock.accept()
while True:
# In an actual program some significant processing/logic would happen here that would take time
# and also produce a certain result, depending on which we may or may not need to send a message.
# To simulate this, we'll pause for a short and random amount of time, then get a random integer.
time.sleep(random.uniform(0.5, 1.0))
myRandInt = random.randint(0, 10)
# if the ranom integer is even, send a message
if myRandInt % 2 == 0:
message = str(myRandInt)
print('sending message: ' + message)
sendTextViaSocket(conn, message)
# end if
# end while
# end function
def sendTextViaSocket(conn, message):
# encode the message
encodedMessage = bytes(message, 'utf-8')
# send the encoded message
conn.sendall(encodedMessage)
# receive the encoded acknowledgement
encodedAck = conn.recv(1024)
# decode the acknowledgement
ack = encodedAck.decode('utf-8')
# check the received acknowledgement is correct, if not log an error
if ack == ACK_TEXT:
pass
else:
print('error: acknowledgement was received as ' + str(ack))
# end if
# end function
if __name__ == '__main__':
main()
client:
# client
import socket
import select
import time
import random
# module-level variables
HOST='127.0.0.1'
PORT=65439
ACK_TEXT = 'ack_text'
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# if the client is started 1st connect will crash, so continually try to connect
# in a try-catch until the server is available
connectionSuccessful = False
while not connectionSuccessful:
try:
sock.connect((HOST, PORT))
connectionSuccessful = True
except:
pass
# end try
# end while
socks = [ sock ]
# endlessly receive messages
while True:
readySocks, _, _ = select.select(socks, [], [], 0)
for readySock in readySocks:
message = receiveTextViaSocket(readySock)
print('received message = ' + str(message))
# end for
# In an actual program client would have other significant tasks to do
# To simulate this we'll pause for a short but random amount of time
time.sleep(random.uniform(0.5, 1.0))
# end while
# end function
def receiveTextViaSocket(sock):
# receive the encoded message
encodedMessage = sock.recv(1024)
# decode the message
message = encodedMessage.decode('utf-8')
# now send the acknowledgement
# encode the acknowledgement
encodedAck = bytes(ACK_TEXT, 'utf-8')
# send the encoded acknowledgement
sock.sendall(encodedAck)
return message
# end function
if __name__ == '__main__':
main()
How can I do the same thing, but with WebSockets ??
--- Edit ---
After spending most of today on this, this is the best I have done so far:
server:
# server.py
import threading
import asyncio
import websockets
import collections
import random
import time
sendMessageQueue = collections.deque()
def main():
webSocketServer = websockets.serve(sendMessages, 'localhost', 8765)
myLoop = asyncio.get_event_loop()
webSockThread = threading.Thread(target=webSockStart, args=(webSocketServer, myLoop,))
webSockThread.start()
while True:
myRandInt = random.randint(1, 10)
print('appending ' + str(myRandInt))
sendMessageQueue.append(str(myRandInt))
print('sendMessageQueue = ' + str(sendMessageQueue))
time.sleep(random.uniform(1.0, 2.0))
# end while
# end function
def webSockStart(webSocketServer, myLoop):
myLoop.run_until_complete(webSocketServer)
myLoop.run_forever()
# end function
async def sendMessages(websocket, path):
while len(sendMessageQueue) > 0:
await websocket.send(sendMessageQueue.popleft())
# end while
# end function
if __name__ == '__main__':
main()
client:
# client.py
import threading
import asyncio
import websockets
import collections
import random
import time
receiveMessageQueue = collections.deque()
def main():
receiveWebSockThread = threading.Thread(target=receiveWebSockStart)
receiveWebSockThread.start()
while True:
print('doing other stuff')
time.sleep(1.0)
while len(receiveMessageQueue) > 0:
message = receiveMessageQueue.popleft()
print('message = ' + str(message))
# end while
# end while
# end function
def receiveWebSockStart():
loop = asyncio.new_event_loop()
loop.run_until_complete(receiveMessages())
loop.run_forever()
# end function
async def receiveMessages():
while True:
uri = 'ws://localhost:8765'
async with websockets.connect(uri) as webSockConn:
message = await webSockConn.recv()
receiveMessageQueue.append(str(message))
# while True:
# end while
# end with
# end function
if __name__ == '__main__':
main()
server output:
$ python3 server.py
appending 3
sendMessageQueue = deque(['3'])
appending 8
sendMessageQueue = deque(['8'])
appending 8
sendMessageQueue = deque(['8', '8'])
appending 6
sendMessageQueue = deque(['8', '8', '6'])
appending 1
sendMessageQueue = deque(['8', '8', '6', '1'])
client output:
$ python3 client.py
doing other stuff
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/usr/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "client.py", line 30, in receiveWebSockStart
loop.run_until_complete(receiveMessages())
File "/usr/lib/python3.6/asyncio/base_events.py", line 484, in run_until_complete
return future.result()
File "client.py", line 38, in receiveMessages
message = await webSockConn.recv()
File "/usr/local/lib/python3.6/dist-packages/websockets/protocol.py", line 509, in recv
await self.ensure_open()
File "/usr/local/lib/python3.6/dist-packages/websockets/protocol.py", line 812, in ensure_open
raise self.connection_closed_exc()
websockets.exceptions.ConnectionClosedOK: code = 1000 (OK), no reason
message = 3
doing other stuff
doing other stuff
doing other stuff
doing other stuff
The server seems to be working as intended, however the client is disconnecting after receiving one message and I'm not sure why or how to resolve it. I'm not sure if threads are even the correct approach, or if I should be using run_in_executor from concurrent.futures, but I couldn't get that to work at all. Anybody with specific knowledge on this topic please help.
--- Edit2 ---
Well, at least I found an answer that works (tested with Python 3.6.9 on Ubuntu 18.04), but I'm really not happy with it:
server:
# server.py
import threading
import asyncio
import websockets
import collections
import random
import time
sendMessageQueue = collections.deque()
def main():
# start the WebSocket sending on a separate thread so it doesn't block main
webSockSendThread = threading.Thread(target=sendWebSockStart)
webSockSendThread.start()
while True:
# make up a random integer and append it to the send queue
myRandInt = random.randint(1, 10)
print('appending ' + str(myRandInt))
sendMessageQueue.append(str(myRandInt))
# an actual program would have many other activities to do here, use a random sleep to simulate this
print('doing other stuff')
time.sleep(random.uniform(1.0, 2.0))
# end while
# end function
def sendWebSockStart():
# since we're in a separate thread now, call new_event_loop() (rather than the usual get_event_loop())
# and set the returned loop as the current loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# instantiate the WebSocket server, note this also connects it to the sendMessages function
webSocketServer = websockets.serve(sendMessages, 'localhost', 8765)
# run the webSocketServer forever, which runs the sendMessages function forever
loop.run_until_complete(webSocketServer)
loop.run_forever() # note execution of this separate thread stays on this line forever
# end function
async def sendMessages(websocket, path):
while True:
while len(sendMessageQueue) > 0:
await websocket.send(sendMessageQueue.popleft())
# end while
# end while
# end function
if __name__ == '__main__':
main()
client:
# client.py
import threading
import asyncio
import websockets
import collections
import random
import time
receiveMessageQueue = collections.deque()
def main():
# start the WebSocket receiving on a separate thread so it doesn't block main
receiveWebSockThread = threading.Thread(target=receiveWebSockStart)
receiveWebSockThread.start()
while True:
# dequeue and print out all the messages in the receive queue currently
while len(receiveMessageQueue) > 0:
message = receiveMessageQueue.popleft()
print('message = ' + str(message))
# end while
# an actual program would have many other activities to do here, use a sleep to simulate this
print('doing other stuff')
time.sleep(1.0)
# end while
# end function
def receiveWebSockStart():
# since we're in a separate thread now, call new_event_loop() rather than the usual get_event_loop()
loop = asyncio.new_event_loop()
# run receiveMessages() forever
loop.run_until_complete(receiveMessages())
loop.run_forever() # note execution of this separate thread stays on this line forever
# end function
async def receiveMessages():
# set up the connection
uri = 'ws://localhost:8765'
async with websockets.connect(uri) as webSockConn:
# endlessly receive messages and append to the queue when received
while True:
message = await webSockConn.recv()
receiveMessageQueue.append(str(message))
# end while
# end with
# end function
if __name__ == '__main__':
main()
server output:
$ python3 server.py
appending 10
doing other stuff
appending 6
doing other stuff
appending 2
doing other stuff
appending 8
doing other stuff
appending 2
doing other stuff
appending 3
doing other stuff
appending 9
doing other stuff
client output:
$ python3 client.py
doing other stuff
message = 10
message = 6
doing other stuff
doing other stuff
message = 2
doing other stuff
doing other stuff
message = 8
doing other stuff
doing other stuff
message = 2
doing other stuff
doing other stuff
message = 3
doing other stuff
message = 9
doing other stuff
I'm using threading as an intermediary between main and the asyncio websockets function. Can anybody explain or better yet provide a working example of how to re-work this to use run_in_executor to eliminate the intermediary thread ??

Related

Making pynng and socket talk to each other

TL;DR
I spin up a server using pynng, then a client from Python Standard Library socket will try to send messages to it.
The problem is that client can send the message, but server is oblivious to it. Therefore, it doesn't work.
Am I missing something? Some low-level protocol setting? Some termination character?
The reason why I'm doing this is that I will build a Python script that uses pynng to act as a server. Then a non-Python program (which I assume has knowledge of basic TCP protocols) will try to talk with this Python server. Thus I am using the IMHO most primitive socket library I could operate, the socket module in the standard library.
The details
I will present code snippets as I discuss, but I will show the full minimal code example at the end.
I am trying to spin up a server using pynng
def server():
with pynng.Pair0(listen=f'tcp://{HOST:s}:{PORT:d}', recv_timeout=10000) as s:
print("Server running")
data = s.recv() # Blocks forever here
print(data)
Then, client that looks like this will try to connect to it:
def client():
with socket.create_connection(address=(HOST, PORT), timeout=5) as s:
print("Client connected")
s.sendall(b'Hello world')
print("Client sent message")
I put them all together in using threading:
def main():
srv = threading.Thread(target=server)
cli = threading.Thread(target=client)
srv.start()
cli.start()
srv.join()
cli.join()
Minimum working code
All told, this is the minimum working code:
import socket
import pynng
import threading
HOST = "127.0.0.1"
PORT = 65432
def main():
srv = threading.Thread(target=server)
cli = threading.Thread(target=client)
srv.start()
cli.start()
srv.join()
cli.join()
def server():
with pynng.Pair0(listen=f'tcp://{HOST:s}:{PORT:d}', recv_timeout=10000) as s:
print("Server running")
data = s.recv() # Blocks forever here
print("Message received")
print(data)
def client():
with socket.create_connection(address=(HOST, PORT), timeout=5) as s:
print("Client connected")
s.sendall(b'Hello world')
print("Client sent message")
if __name__ == "__main__":
main()
Then I run this in the terminal
$ python main.py
It seems that the server is unable to recv messages, and the recv attempt thus times out at 10000ms.
Server running
Client connected
Client sent message
Exception in thread Thread-1:
Traceback (most recent call last):
File "/home/kmonisit/miniconda3/envs/engg/lib/python3.8/threading.py", line 932, in _bootstrap_inner
self.run()
File "/home/kmonisit/miniconda3/envs/engg/lib/python3.8/threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "main.py", line 39, in server
data = s.recv() # Blocks forever here
File "/home/kmonisit/miniconda3/envs/engg/lib/python3.8/site-packages/pynng/nng.py", line 454, in recv
check_err(ret)
File "/home/kmonisit/miniconda3/envs/engg/lib/python3.8/site-packages/pynng/exceptions.py", line 201, in check_err
raise exc(string, err)
pynng.exceptions.Timeout: Timed out
pynng is based on Nanomsg Next Generation, which is an implementation of the Scalability Protocols. The scalability protocols work on many different transports, including tcp, but bare sockets are not compatible. However, with a little bit of prayer and elbow grease, they can be made compatible. Which is too say, you can implement the scalability protocols in pure Python if need be.
First, we need to know what the wire format is; thankfully that is documented in an RFC in the original nanomsg repository. An implementation of a Pair0 client is here:
class Pair0:
"""A poor implementation of the Pair0 protocol"""
def __init__(self, host, port, timeout=None):
self._sock = socket.create_connection(address=(host, port), timeout=timeout)
# https://github.com/nanomsg/nanomsg/blob/master/rfc/sp-tcp-mapping-01.txt
# upon making a connection, both ends are required to send this header
self._sock.send(b'\x00SP\x00\x00\x10\x00\x00')
print(self._sock.recv(8))
def send(self, data):
# messages are simply "length + payload". Length is 64-bit in network byte
# order.
packed = struct.pack('!Q', len(data))
self._sock.sendall(packed + data)
def recv(self):
size_bytes = self._sock.recv(8)
(size,) = struct.unpack('!Q', size_bytes)
received = 0
parts = []
while received < size:
data = self._sock.recv(size - received)
received += len(data)
parts.append(data)
return b''.join(parts)
And integrated into your test program:
import socket
import struct
import pynng
import threading
import time
HOST = "127.0.0.1"
PORT = 65432
def main():
srv = threading.Thread(target=server)
srv.start()
# sleep to give the server time to bind to the address
time.sleep(0.1)
_client = Pair0(HOST, PORT, 1)
_client.send(b'hello pynng')
_client.send(b'hope everything is going well for you')
print(_client.recv())
print(_client.recv())
srv.join()
def server():
with pynng.Pair0(listen=f'tcp://{HOST:s}:{PORT:d}', recv_timeout=1000) as s:
print("Server running")
for _ in range(2):
data = s.recv()
print("Message received")
print(data)
s.send(b'hello bad client')
s.send(b'I hope you are doing okay')
class Pair0:
"""A poor implementation of the Pair0 protocol"""
def __init__(self, host, port, timeout=None):
self._sock = socket.create_connection(address=(host, port), timeout=timeout)
# https://github.com/nanomsg/nanomsg/blob/master/rfc/sp-tcp-mapping-01.txt
# upon making a connection, both ends are required to send this header
self._sock.send(b'\x00SP\x00\x00\x10\x00\x00')
print(self._sock.recv(8))
def send(self, data):
# messages are simply "length + payload". Length is 64-bit in network byte
# order.
packed = struct.pack('!Q', len(data))
self._sock.sendall(packed + data)
def recv(self):
size_bytes = self._sock.recv(8)
(size,) = struct.unpack('!Q', size_bytes)
received = 0
parts = []
while received < size:
data = self._sock.recv(size - received)
received += len(data)
parts.append(data)
return b''.join(parts)
if __name__ == "__main__":
main()
Now, this is nowhere near as robust as the implementation in pynng (which relies on the underlying nng implementation). nng does The Right Thing™ in edge conditions, including losing network, handling multiple clients, keeping track of state machines, handling SIGINT, etc. This is also an imcomplete implementation, as it does not bind, etc.
Disclaimer: I am the author of pynng.

Have two infinite task with asyncio

I'm new in python and I have to create a program that keeps in linstening from web socket and pipe so I need two asynchronous functions.
Each of these functions call other method in different thrad that elaborate the content of received json.
i.e. I receive a message on socket thread, I get the message and throw a new thread to elaborate the message.
This is the actual code:
import asyncio
import sys
import json
import websockets
# Keep listening from web socket and pipe
async def socket_receiver():
"""Listening from web socket"""
file_socket = open(r"SocketReceived.txt", "w")
header = {"Authorization": r"Basic XXXXXXXXXXXXXX="}
async with websockets.connect(
'wss://XXXXXXXXX', extra_headers=header) as web_socket:
print("SOCKET receiving:")
greeting = await web_socket.recv()
json_message = json.loads(greeting)
file_socket.write(json_message)
print(json_message)
file_socket.close()
async def pipe_receiver():
"""Listening from pipe"""
file_pipe = open(r"ipeReceived.txt", "w")
while True:
print("PIPE receiving:")
line = sys.stdin.readline()
if not line:
break
jsonObj = json.loads(line);
file_pipe.write(jsonObj['prova'] + '\n')
# jsonValue = json.dump(str(line), file);
sys.stdout.flush()
file_pipe.close()
asyncio.get_event_loop().run_until_complete(socket_receiver())
asyncio.get_event_loop().run_until_complete(pipe_receiver())
run_until_complete method keep forever in my case (it waits the end of function), so only the socket starts.
How can I start both? Thanks
asyncio.gather does the trick, the only point is that both functions should share the same event loop, and both should be fully asynchronous.
asyncio.get_event_loop().run_until_complete(
asyncio.gather( socket_receiver(),pipe_receiver()))
From a quick reading of pipe_receiver, you will hang your event loop in sys.stdin.readline call, please consider using aioconsole to asynchronously handle the input.

Python WebSocket Client connecting but not sending messages

While using websocket client to send test messages to a django server, I cannot get a script to work which can both send and receive messages.
The following python script is what I have attempted:
import websocket
import threading
import json
from time import sleep
# handle message event
def on_message(ws, message):
print("message recieved: %s" % message)
# handle close event
def on_close(ws):
print("channel closed")
# execute as main script
if __name__ == "__main__":
websocket.enableTrace(True)
# new app object connecting to headstation
ws = websocket.WebSocketApp("ws://192.168.0.106:8000/?testI123", on_message = on_message, on_close = on_close)
# run in a new thread - kill if script ends
ws_listener = threading.Thread(target=ws.run_forever())
ws_listener.daemon = True
# start second thread
ws_listener.start()
# attempt connection 5 times
timeout = 5
while not ws.sock.connected and timeout:
sleep(1)
timeout -= 1
# error on timeout
if (timeout == 0):
print("Connection to server timed out")
print("test 1")
# periodically send test message to server
message_num = 0
while ws.sock.connected:
# send node id and message
message = 'hello %d'%message_num
ws.send(message)
sleep(1)
message_num += 1
This connections successfully, indicted by the server, and receives messages sent from the server, but does not send anything.
Periodically, something like this is displayed on the terminal:
send: b'\x8a\x84\xe2\xe9\xa8\xe2\x8f\xdc\xe2\x84'
If I simply use
ws = websocket.WebSocket()
ws.connect(url)
ws.send("hello")
then this works perfectly. Suggesting it is something wrong with my little python script displayed above.
Found the problem, stupid mistake of course:
ws_listener = threading.Thread(target=ws.run_forever())
should be:
ws_listener = threading.Thread(target=ws.run_forever)
without parentheses.
First one passes result of ws.run_forever to the target, second one sets ws.run_forever as the target, which was the intended outcome.

Python/Socket: How to send multi messages without waiting the respond?

I want to send multi messages using socket module without waiting the respond from the client or server. However the codes that are below can not do this. What are your suggestions in order to do that? Thanks in advance.
Here are the codes:
server.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 12345))
s.listen(1)
print(s)
c, addr = s.accept()
print('{} connected.'.format(addr))
while True:
respond = input("Server: ").encode("utf-8")
if respond == b"q":
exit()
else:
c.sendall(bytes(respond))
data = str(c.recv(1024))[1:]
if data:
print("Client: {}".format(data))
client.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("", 12345))
while True:
respond = input("Client: ").encode("utf-8")
if respond == b"q":
exit()
else:
s.sendall(bytes(respond))
data = str(s.recv(1024))[1:]
if data:
print("Server: {}".format(data))
Ok, so for what you want to achieve, you need to change the approach completely and use asyncio equivalents of socket methods as well as replacement for bare standard input handling.
The following code works on Python >= 3.5 and requires aioconsole Python package which can be installed with pip install aioconsole.
server.py
import asyncio
import aioconsole
class StreamWrapper(object):
"""This class is used to make socket stream created by server available for send_messgaes() function"""
def __init__(self):
self.reader = None
self.writer = None
async def send_messages(stream_wrapper, stdin):
# Wait asynchronously until server part initializes socket stream
while stream_wrapper.writer is None:
await asyncio.sleep(0.1)
writer = stream_wrapper.writer
# Asynchronusly read from standard input
async for message in stdin:
if message.decode().strip() == "q":
writer.close()
exit()
else:
# Send message through the socket
writer.write(message)
def receive_messages_wrapper(stream_wrapper, stdout):
"""Wrapper function which adds stream_wrapper and stdout to the scope of receive_messages()"""
async def receive_messages(reader, writer):
# Copy socket stream reference to stream wrapper
stream_wrapper.reader = reader
stream_wrapper.writer = writer
# Asynchronusly read messages from the socket
async for message in reader:
stdout.write('\nClient: {}'.format(message.decode()))
stdout.write("Server: ")
# Wrapper returns receive_messages function with enhanced scope - receive_messages() can "see" stream_wrapper and stdout
return receive_messages
async def run_server(loop):
"""Initialize stdin and stdout asynchronous streams and start the server"""
stdin, stdout = await aioconsole.get_standard_streams()
stream_wrapper = StreamWrapper()
# Asynchronously execute send_messages and start_server()
await asyncio.gather(
send_messages(stream_wrapper, stdin),
asyncio.start_server(receive_messages_wrapper(stream_wrapper, stdout), '127.0.0.1', 8888, loop=loop)
)
# Run the server on the event loop
loop = asyncio.get_event_loop()
loop.run_until_complete(run_server(loop))
loop.close()
client.py
import asyncio
import aioconsole
async def send_messages(writer, stdin):
# Asynchronusly read from standard input
async for message in stdin:
if message.decode().strip() == "q":
writer.close()
exit()
else:
# Send message through the socket
writer.write(message)
async def receive_messages(reader, stdout):
# Asynchronusly read messages from the socket
async for message in reader:
stdout.write('\nServer: {}'.format(message.decode()))
stdout.write("Client: ")
async def run_client(loop):
"""Initialize stdin and stdout asynchronous streams and open the client connection, then start exchanging messages"""
stdin, stdout = await aioconsole.get_standard_streams()
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
stdout.write("Client: ")
# Asynchronously execute send_messages and receive_messages()
await asyncio.gather(
send_messages(writer, stdin),
receive_messages(reader, stdout)
)
# Run the client on the event loop
loop = asyncio.get_event_loop()
loop.run_until_complete(run_client(loop))
loop.close()
This might seem complicated if you never used asyncio before but it does exactly what you want. You can spam multiple messages from any end (client or server) and the other end will receive it and print, while waiting for user input. I've provided comments but if you want to fully understand it, you should get familiar with asyncio documentation.
The other possible approaches involve using threads or multiprocessing. I wouldn't say they are easier than asyncio.

GRPC streaming select (python)

Let's say I want to create a chat-like application. A client can send text to the server and vice versa. The order of text exchanges can be arbitrary.
The server depends on another stream which controls the server response stream.
The GRPC stream is exposed as a python generator. How can the server now wait for client input and input on the other stream at the same time? Normally one would use something like select(), but here we have generators.
I have some example code which implements the wanted behavior but requires an additional thread on the client and server side. How can I achieve the same result without a thread?
Proto:
syntax = 'proto3';
service Scenario {
rpc Chat(stream DPong) returns (stream DPong) {}
}
message DPong {
string name = 1;
}
Server:
import random
import string
import threading
import grpc
import scenario_pb2_grpc
import scenario_pb2
import time
from concurrent import futures
class Scenario(scenario_pb2_grpc.ScenarioServicer):
def Chat(self, request_iterator, context):
def stream():
while 1:
time.sleep(1)
yield random.choice(string.ascii_letters)
output_stream = stream()
def read_incoming():
while 1:
received = next(request_iterator)
print('received: {}'.format(received))
thread = threading.Thread(target=read_incoming)
thread.daemon = True
thread.start()
while 1:
yield scenario_pb2.DPong(name=next(output_stream))
if __name__ == '__main__':
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
scenario_pb2.add_ScenarioServicer_to_server(
Scenario(), server)
server.add_insecure_port('[::]:50052')
server.start()
print('listening ...')
while 1:
time.sleep(1)
Client
import threading
import grpc
import time
import scenario_pb2_grpc, scenario_pb2
def run():
channel = grpc.insecure_channel('localhost:50052')
stub = scenario_pb2_grpc.ScenarioStub(channel)
print('client connected')
def stream():
while 1:
yield scenario_pb2.DPong(name=input('$ '))
input_stream = stub.Chat(stream())
def read_incoming():
while 1:
print('received: {}'.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()
It is not currently possible to do this without spending the threads that you're spending. We're thinking about implementing enhancements that would allow implementations to avoid taking another thread, but those would be months away at earliest.

Categories

Resources