How to perform functions simultaneously through a Socket?

Asked

Viewed 932 times

1

I’m developing a program in Python that’s basically a Chat, just for training. I would like it to have functions like: microphone, webcam, message and screen sharing. I’ve managed to do each of these functions separately. The problem is at socket assembly time, because I wanted each function to be independent but connected to the server through a single socket, for example: the program used by the user sends the webcam frames to the Server at the same time as the Desktop frames are sent, and this has to occur in a single socket. I tried through the Python socket library, but the application hangs because all data is sent to the first instance created from receiving on the Server, even if I use Thread. I tried to use zerorpc, but I couldn’t create a Thread for each function, I didn’t understand why. That’s more or less it:

import zerorpc
c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")

def enviarWebcam():
    frame_da_webcam = None #só exemplo
    c.Webcam(frame_da_webcam)

def enviarDesktop():
    frame_do_desktop = None
    c.Desktop(frame_do_desktop)

_thread.start_new_thread(enviarWebcam, ())
_thread.start_new_thread(enviarDesktop, ())

And on the server:

import zerorpc

class Chat(object):
    def Webcam(self, frame):
        #aqui eu recebo o frame e envio para o usuário que está se comunicando com o outro
    def Desktop(self, frame):
        #a mesma coisa do acima

s = zerorpc.Server(Chat())
s.bind("tcp://0.0.0.0:4242")

I tried Ironpython too, but because I didn’t know much about C# I gave up. If anyone can give me a little help of what this network would be like, I really appreciate it!

  • Although the question there is different, I think I clarify a lot of things that you need to answer: https://answall.com/questions/254953/howto create a communication%C3%A7%C3%A3o-entre-dois-clientes-via-servidor-usando-socket/255288#255288

1 answer

1


So - you found out why there are higher-level protocols than TCP, like HTTP and FTP for example: TCP deals with the exchange of messages between two computers, with an established connection - but it does not care at all about the contents of these messages.

Any information beyond that - including the message length, or end marker, are data that also has to be inside the messages, in addition to the final content itself, in your case, the bytes with the image files.

In your specific case, you want to share the same TCP connection to transit multiple files - or, create separate connections, and access distinct functionalities, but with the same input socket - both situations require more control than just having a TCP and using recv there to read the bytes. The "HTTP" protocol, for example, was created to have the messages that we know so much, that send messages like "GET", "POST", "PUT", and are answered by the server with the famous "Status Code". A raw socket has none of that- as you noticed in the text that is in the first part of your question, you have to either use a higher-level protocol, or reinvent the wheel.

Then I have good news and bad news:

Fortunately, instead of reinventing the wheel, you chose to use the library zerorpc that already does this: adds layers of protocol around the Socket, to allow the call of Python methods, and already formats and serializes the answer - that is, you are no longer concerned with just a "socket" - your question is zerorpc. then is to follow his documentation and see how to handle asynchronous calls: that is - the ability to process and respond to a request without blocking the arrival of new requests.

Hence the bad news - Python’s zeroprc server doesn’t create multiple Workers automatically, using any method, and the existing documentation is pretty bad. The good part is that it manages multiple connections in the same socket, so, yes, once you can create the parallel Workers, if your client is already making the calls in parallel, it will work (but if you need to parallelize the client as well, there are more things).

Well, in short - zerorpc uses a Python technology called gevent, which is lighter than threads, however this implies that you will have to use gevent to parallelize your server, not threads. Threads are already complicated, but you find plenty of example and documentation. Gevent uses something called "greenlet" instead of threads - it has the same difficulties, but much less documentation and examples.

The gevent documentation is here: http://www.gevent.org/api/

But in short, in every role of yours that is made available to be called remotely, you can create a new Greenlet pointing to the actual function, and call your method .join - This will perform the function inside greenlet - equivalent to the threads of S.What Python. uses - and can free the server to receive other connections (but - more on that ahead) while doing its processing. When the call to .join return, you can return the attribute .value greenlet - this will be the return value of the remote function.

Here I have a simple server and client, who only sleeps a fraction of a random second, first sequentially and then in parallel:

import random, time

import gevent
import zerorpc

def sync_world(self, i):
    print(f"starting {i}")
    gevent.sleep(random.random())
    print(f"finishing {i}")
    return i

class Hello:

    def world(self, i):
        glet = gevent.spawn(sync_world, i)
        glet.join()
        return glet.value

s = zerorpc.Server(Hello())
s.bind("tcp://127.0.0.1:8877")
gevent.spawn(s.run)
gevent.wait()

And the client - realize that the client’s calls have to pass the value async=True. This causes the call to return a "Future", instead of the real value - then it is necessary to call the method get at each value returned to have the value processed on the server.

import time

import zerorpc

c = zerorpc.Client("tcp://127.0.0.1:8877")

def timeit(func):
    def wrapper(*args, **kw):
        start = time.time()
        result = func(*args, **kw)
        print (f"\n{func.__name__} ran in {time.time() - start}s\n")
        return result
    return wrapper

@timeit
def query():
    for i in range(10):
        print(c.world(i))

@timeit
def parallel_query():
    results = []
    for i in range(10):
        results.append(c.world(i, async=True))
    for result in results:
        print(result.get(block=True))

query()
parallel_query()

In other words - this client has a function that only calls the methods in sequence - it works, but this will wait for each response to be ready on the server to send the next request- even though the server is parallelized. The second function adds the parameter async=True (not to be confused with the keyword async introduced in Python 3.5 - it’s just the same idea, but implemented separately, using greenlets). (this code also has a little decoration to print the time of each function - it, of course, is not necessary)

Now - another bad news - notice that for my server to be parallel, I have to call the function greenlet.sleep. That is: I have to pass the control, at some point, to the loop of the "gevent", otherwise the function is not parallel, it runs until the end - without letting the server access new connections - then the remedy really is to call the gevent.sleep() (without a number - or with "0"), this makes gevent check if there is another connection in the socket and start treating it. This is different from threads - where the operating system itself automatically switches to another thread. It’s more like the asyncio most used in recent Python.

And here comes the third bad news: the original greenlet, which called the gevent.sleep to give a chance for another call, you will be standing by waiting - if the time it takes to answer the server-side request uses too much CPU or blocks IO (for example, when saving the file on disk) - this is not parallelized by gevent or Python - you will need a third technology (besides zerorpc and gevent) to run the task really in parallel. So, yes, you can still have a good road ahead.

One thing to try, once you can parallelize the server, even if you haven’t realized the benefits yet, is to call gevent’s Monkey-patch functions - they cause some of the standard Python library functions to automatically cooperate with gevent, and you may already be able to respond to queries in parallel, depending on what each worker function does: http://www.gevent.org/api/gevent.monkey.html (but I can’t tell if zerorpc already makes that call).

You wrote that you are doing "just a chat" - I understand this as a text chat - in this case, the parallel handling of sockets only with the above Monkey-patch is more than enough. But in your code you have functions to want to save in real time all the frames of two video streams - this is much heavier, and you can’t do it much simpler.

Alternative:

Forget zerorpc if you start getting too complicated and don’t have better documentation on how to parallelize it (I didn’t find it). Instead use zerorpc celery - he uses another process like "Broker" - that is, a message manager, but he does it transparently. Then instead of having a single server, you go up several Workers consuming messages, and the remote method calls, instead of talking to the Workers directly, put the messages in Broker - for whom the program is the same thing: its Python function here is called remotely from the other process - but all the parallelism and networking is on account of Celery. - https://celery.org

Browser other questions tagged

You are not signed in. Login or sign up in order to post.