-
Notifications
You must be signed in to change notification settings - Fork 10.3k
C++ client low level API #8420
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
C++ client low level API #8420
Conversation
Ping, any feedback? No feedback is fine too, just want people to take a look :) One thing I've been thinking about is accepting the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good
{ | ||
public: | ||
int status_code = 0; | ||
std::string content; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to support SSE? If not, and I think we can probably do without it, this should be fine. If we do want to support it, we'll need this to be a stream of some kind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We just need to make sure that it can be added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We talked about using ostream for this and istream for the request content. The problem is that only offers blocking and polling APIs. The alternative is using Asio's AsyncRead/WriteStream.
Since we're only aiming for source compatibility initially instead of some stable C ABI, we can probably just add an async stream field to http_request/response later and gracefully skip SSE if the http_client that doesn't use the field. We'd need a new send overload that calls the callback prior to buffering the entire response anyway.
|
||
virtual ~websocket_client() {}; | ||
virtual void receive(std::function<void(std::string, std::exception_ptr)> callback) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the naming and structure I presume this calls the callback once for each call to receive, when a single frame is received? Another alternative would be a on_recieved
API that registers a callback which will be called once for each frame that arrives (and we'd do the looping ourselves).
|
||
logger m_logger; | ||
virtual void on_receive(std::function<void(std::string, std::exception_ptr)> callback) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect to use the same API in websocket_client
as we do here, so maybe we should change websocket_client
to use on_receive
as well...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question is, do we expect people who provide their own websocket impl to do the receive loop and call our callback, or do we do the receive loop in the transport and just call receive on their impl?
Did you say... ownership? |
I do think that's a good idea though. Big fan of |
http_method method; | ||
std::map<std::string, std::string> headers; | ||
std::string content; | ||
std::chrono::seconds timeout; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe timeout should be a separate parameter on send. Or better yet, define a single-method CTS-like class that we return from send instead.
|
||
if (!cts.get_token().is_canceled()) | ||
{ | ||
transport->receive_loop(cts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this stack dive if enough messages were buffered in memory that websocket_client->receive
repeatedly called its callback inline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely could. I believe we talked about this and decided to do it this way for now and re-evaluate later if we need to dispatch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there's a risk, I figure it's easy enough to turn this into a trampoline to avoid the problem. I think it would look something like this.
auto calling_receive = true;
while (calling_receive) {
websocket_client->receive([weak_transport, cts, logger, websocket_client, calling_receive](std::string message, std::exception_ptr exception)
{
// ...
if (!cts.get_token().is_canceled())
{
if (calling_receive)
{
calling_receive = false
}
else
{
transport->receive_loop(cts);
}
}
});
calling_receive = !calling_receive;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like that would work, but we still need to jump off the main thread for the start of the receive loop in order to avoid blocking start. Right now I'm relying on the transport dispatching, like we talked about in the meeting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the main thread? Whatever thread you call connection.start() on?
Do we jump off the main thread currently before calling websocket_client->receive()? Or does this design prevent us from jumping off the main thread in the future?
I would think the trampoline is strictly better than the recursive version because it avoids potentially stack diving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the main thread? Whatever thread you call connection.start() on?
Correct.
Do we jump off the main thread currently before calling websocket_client->receive()? Or does this design prevent us from jumping off the main thread in the future?
We don't do anything threading related yet, except in the http_client and websocket_client as those use pplx. We can add it whenever though.
I would think the trampoline is strictly better than the recursive version because it avoids potentially stack diving.
I agree.
d1e7157
to
b79217f
Compare
src/SignalR/clients/cpp/samples/HubConnectionSample/HubConnectionSample.cpp
Outdated
Show resolved
Hide resolved
} | ||
|
||
std::shared_ptr<connection_impl> connection_impl::create(const std::string& url, trace_level trace_level, const std::shared_ptr<log_writer>& log_writer, | ||
std::unique_ptr<web_request_factory> web_request_factory, std::unique_ptr<transport_factory> transport_factory) | ||
std::unique_ptr<http_client> http_client, std::unique_ptr<transport_factory> transport_factory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone passes a null http_client here, we should probably throw here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
try | ||
{ | ||
task.get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I need a bit more context on what this method is doing. Why do you need to complete the task provided here? Is the callback here only used for exception handling or is it required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a point in time thing as we're still using pplx::task
in our code.
What this is doing, is observing any exceptions from the completed task so we don't crash the process, and then forwarding the exception to the new callback based APIs
} | ||
catch (...) | ||
{ | ||
callback(std::current_exception()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced this pattern for Try/Catch is good. Why not catch a std::exception& and pass the ref into the callback? That way you don't need to rethrow to get the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM from the perspective of non-signalR dev. IMO let's try to get this in sooner than later.
src/SignalR/clients/cpp/src/signalrclient/websocket_transport.cpp
Outdated
Show resolved
Hide resolved
src/SignalR/clients/cpp/src/signalrclient/websocket_transport.cpp
Outdated
Show resolved
Hide resolved
b1bf056
to
0cffafa
Compare
👏 |
Please look over the API and ignore tests.
http_client: https://github.com/aspnet/AspNetCore/blob/28ccc03fb90499425a9a3deb0c066e8624fff23a/src/SignalR/clients/cpp/src/signalrclient/http_client.h
websocket_client: https://github.com/aspnet/AspNetCore/blob/28ccc03fb90499425a9a3deb0c066e8624fff23a/src/SignalR/clients/cpp/src/signalrclient/default_websocket_client.h
transport: https://github.com/aspnet/AspNetCore/blob/28ccc03fb90499425a9a3deb0c066e8624fff23a/src/SignalR/clients/cpp/src/signalrclient/transport.h
As a reminder, the
http_client
andwebsocket_client
are the things we're wanting to be replaceable. By default we will have implementations that use cpprestsdk.