Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

questions about the scs #3

Open
yalov opened this issue May 14, 2023 · 11 comments
Open

questions about the scs #3

yalov opened this issue May 14, 2023 · 11 comments
Assignees
Labels
enhancement New feature or request question Further information is requested

Comments

@yalov
Copy link

yalov commented May 14, 2023

Hello, I am trying to modify scstest.cpp to create multiple scs::clientPtrs within a single client application. Each of these clients class will have a different logic and protocol and will request different data from the server.

To make the question more specific, let's say that we want a client to send a string "String" in addition to the random data (as is the case in the current example) and receive the response "Answer" from the server every second. How can we implement this?

To start, I tried put the scs::clientPtr client inside an if(true){} statement, but the client stopped working

https://gist.github.com/yalov/1d73666029ca6e6fcaabacdef60db6cf

@yalov yalov changed the title Several connection to server from one client [question] Several connection to server from one client May 14, 2023
@JamesBoer
Copy link
Owner

Honestly, I never really thought of utilizing multiple clients in a single process. I think it should be possible in theory, but it hasn't really been tested. I'll start by seeing if this works in a simple test, and go from there.

As far as passing data... the SCS API just passed buffers of data. So you'll need to create some sort of high-level protocol yourself to understand what a "string" is on the server. This can be as simple as a simple number or enumeration that acts as a "message type", which then interprets the bytes that come in. This is up to the application to figure out.

@JamesBoer JamesBoer self-assigned this May 15, 2023
@JamesBoer JamesBoer added enhancement New feature or request question Further information is requested labels May 15, 2023
@JamesBoer
Copy link
Owner

I've added some tests to make sure the system works with multiple clients in a single process. So far it seems like there's no problem with that.

@yalov
Copy link
Author

yalov commented May 28, 2023

thanks,
I thought the client and server classes creates threads for every connection. Do you have some examples with threads and scs ?

@yalov
Copy link
Author

yalov commented May 28, 2023

For example, this code interrupts my pipeline and unable to consume data in set_consuming_callback (because it is all in one thread and server class takes all process time?)
After I press ESC is it continue to consume data through stages.
How to make it to send data if there is client, and does not interrupts data process if there is no connection?

class SenderStage : public BaseStage {
public:
    SenderStage(std::string port)
    {
        // callback to get data, that need to be send to the client
        set_consuming_callback([this](const boost::any& data) {
            results = boost::any_cast<std::list<string>>(data);
            if (!results.empty()) {
                need_send = true;
            }
        }
 
        // ...
        auto server = CreateServer(params);
        std::optional<ClientID> server_clientId;

        // Handler for incoming data
        server->OnReceiveData([&](IServer& server, ClientID clientId, const void* data, size_t bytes)
            {
                std::string str((const char*)data, bytes);
                if      (str == "start") { server_clientId = clientId; }
                else if (str == "stop") { server_clientId = std::nullopt; }
            });

        // Handler for per-cycle update
        server->OnUpdate([&](IServer& server)
            {
                if (server2_clientId.has_value())
                {
                    if (need_send)
                    {
                        std::string res = results[0];
                        server.Send(server2_clientId.value(), res.data(), res.size());
                        need_send = false;
                    }
                }
            });

        // Start listening for client connections
        server->StartListening();

        // Wait until ESC is pressed
        while (GetChar() != 27) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); }
    }

private:
    std::list<string> results;
    bool need_send = false;
}

@JamesBoer
Copy link
Owner

So, first of all, I should point out that you can't serialize a list of strings across a network connection simply by casting it to bytes. That's going to crash horribly. Look for a serialization library to help with that (Boost has one), and I think "cereal" is somewhat popular too.

Basically, the multi-threaded nature of SCS clients and servers is mostly handled for you. You're just providing callback handlers that either get called on connection or receiving data. It also provides handlers for periodic updates, which is useful if you want to do things like time-based logic, or responses to things other than receiving data.

As you indicated, each connection operates it's own thread. This is a simple system architecturally-speaking, but not very efficient if you want to build servers that handle huge numbers of simultaneous connections. In the modified example, the only reason this code blocks is because of the while (GetChar() != 27) loop, which is simply waiting for ESC to be pressed.

When a client connects to the server, notice that there's a ClientID passed as a parameter. Each new client will have it's own ID, so you can use this in the server code to identify individual clients. Remember, these callback functions are being called from background threads, so you'll need to protect your shared data structures with locks to make them thread-safe.

So, if I was writing complex server logic with multiple stages or other state to track per-client, I'd create a map of client IDs to state data. And remember, you'll have to create a lock each time you read for write from this map data on the server. You can then store any state data you need in the map data, like the current stage.

@yalov
Copy link
Author

yalov commented May 29, 2023

Thanks, in this example I have send a string instead a list of strings, I want to deal with the threads and connections, and later send proper data. Is it acceptable to send a std::string in the manner I am currently doing ?

the server doesn't have many clients, it is just one. The applications (that I want to send data between) have backend-frontend relations — 3 type of data will be send in the 3 connections (an original frame, results of an algorithm, and json settings)

I have understood, the while(GetChar() != 27){} make runtime of this thread stop there, that make my data stop generating, and make server do not go out of scope, and if I comment out the line, then my data is generating, but server is shutdown right after the start, because the definition of server class goes out of scope.

Is it desired to have CreateServer() and Server Handlers in separate std::thread,
or it's ok to make server a class member, so it will be in the scope, and to rely on creation of the threads for listening and sending data inside of scs ?

@JamesBoer
Copy link
Owner

So, std::string can't be sent as-is across the wire as-is either. Again, this is what serialization libraries are for. Any container that internally contains a pointer to data is going to have this same issue. Remember, on the receiving end, all you have is a series of bytes. So for a string, you'll want to first make sure it's identified as a string via some message or type ID. You'll probably then need to get the length of the string, and then finally, you'll know how long the string is and can get the data, and convert this back into a std::string.

If you need the server to stay active over time, you can use the OnUpdate() handler. This code gets called roughly every millisecond. There's probably no need to launch multiple threads yourself (unless those are needed for some sort of processing), but you do have to make sure the handlers are thread-safe themselves. I think you're mentally over-complicating what you need to do, as far as I can tell from what you're describing. This is what's known as an event-driven architecture. Your code won't be linear - it just needs to respond to the events generated by the SCS Server and Client objects.

Maybe I need to create a more complex example to really explain how this might work...

@yalov yalov changed the title [question] Several connection to server from one client questions about the scs Jun 5, 2023
@yalov
Copy link
Author

yalov commented Jun 10, 2023

How I can I make server->OnUpdate() run less frequently, for example once every second ?

@JamesBoer
Copy link
Owner

You could do this yourself with std::chrono, but it seems worthwhile to have a parameter to control this aspect of both client and server explicitly. I've added updateMs to the creation params of both client and server. Specify in milliseconds how frequently the update callback is called. For example, set the server params updateMs value to 1000 to have it tick once per second. Note that this is currently in the development branch only.

@yalov
Copy link
Author

yalov commented Sep 7, 2023

Hello, are there default way to client close its connection on the server side in this library?
like, I have a test console server and test console client,
if I close a client, and open it again, the server have m_connectionList.size() == 2, and new client have id == 2 on the server

@yalov
Copy link
Author

yalov commented Sep 7, 2023

It will be disconnected after timeout, but how to make client to request disconnect?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants