uWebSockets
uWebSockets copied to clipboard
WebSocket client support
v0.15 will not feature client support but will be added later on together with proper Http client. It would make sense to add client support in a separate release just for that.
It would be nice for Http client to support simple HTTP(s) proxy and chunked responses. If not in the initial release, please consider designing it in a way that those features could be easily implemented by users. I use those two features in my app (implemented them on top of v0.14) and except the http client itself are the only missing features preventing me from migrating to v0.15
Will it support http/2
?
+1 to a ws client
would be nice a ws client
for those who need a sample client websocket , it wraps https://libwebsockets.org/ (pure C)
https://github.com/maurodelazeri/bitmexcpp
It seems like you're going to handle this in the next version. I was wondering if there would be any issues if I use libcurl multi stuff with the Loop's defer
in order to be a simple http client for now.
Hey guys, the v0.14 branch of uWebSockets has websocket client support, so using the older version might be your best bet if you need a client. It's way better than the libwebsockets approach suggested above, and v0.14 is the best C++ library with both server and client support IMO.
I've got a fork of the v0.14 branch with some minor improvements in it:
https://github.com/hoytech/uWebSockets
- Compression works for client connections (without sliding window for now)
- Server now supports
server_no_context_takeover
if sent by a client (and doesn't allocate a sliding window for that client) - Allow apps to find out the size of compressed messages (both sent and received) so you can log your compression ratios and such (I kept the API backwards compatible)
- Added some docs on compression: https://github.com/hoytech/uWebSockets/blob/master/docs/User-manual-v0.14.x.md#compression
- Tweaked Makefile a bit so you can build a .a file in addition to an .so
Of course I'm looking forward to client support in v0.18 or whenever and will port over my apps once that is released.
Cheers!
How should a client API look like?
I think App and SSLApp should be renamed to Server and SSLServer and then introduce Client and SSLClient?
Or something similar
As far as I'm concerned, the client API can be pretty much the same as the server API, except that you call connect instead of listen. You then have the same .get, .post, .ws etc APIs but you deal with responses from the target server instead of requests to your server.
Please write a few examples of how it could look like in code
Just a few ideas, what do you think? It would work for me, but others please comment too to see what are everyone's needs
// GET https://example.com/hello
uWS::SSLApp({})
// this can be called immediately to build the
// HTTP request (add headers etc), and then again when we get the response
// what do you think?
.get("/hello", [](auto *res, auto *req) {
if (res) {
// handle the response
} else {
// build the request
req->writeHeader("Authorization", "xyz")
->end();
}
}).connect("example.com", 443, [](auto *token) {
// Alternatively, .get() can receive full url .get("https://example.com/hello")
// and then connect() will take no arguments (as the host and port would
// be deduced already from the url) - example below in POST
}).run();
// POST https://example.com/upload
uWS::SSLApp({})
.post("https://example.com/upload", [](auto *res, auto *req) {
if (res) {
// handle the response
} else {
// build the request
req->writeHeader("Content-Type", "text/plain; charset=utf-8")
->end("some POST data");
}
}).connect([](auto *token) {
}).run();
// WebSockets to https://example.com/ws
uWS::SSLApp({})
.ws<UserData>("/ws", {
.open = [](auto *ws, auto *req) {
// May be similar to previous examples, ie. we need a way to add custom headers to the HTTP request before it's sent
// ...
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
}
}).connect("example.com", 443, [](auto *token) {
}).run();
or maybe don't use connect at all, and make all get/post/ws immediately start the connection?
Also would be nice to have:
- Some way to set the proxy (CONNECT method)
- Allow for a single App or SSLApp to make different requests at any time (so if you want to do 100 http requests you don't have to create multiple Apps or event loops)
The 0.14 API works well for me. I haven't really considered how the HTTP client features will interact with websockets, but purely thinking about websockets, here's a quick sketch of an idea:
struct ConnData { /* ... */ };
uWS::ClientApp myClient({
.insecureDontValidateCertYesIknowTheRisks = true,
// stuff like SSL client cert options?
.async = [](){},
}).run();
Then later on, probably in an async handler (us::Async
in 0.14):
myClient.connect<ConnData>("wss://server.com:8443/path", {
.compress = true,
.slidingWindow = false,
.open = [](auto *ws) {
// ws->getUserData() points to a ConnData
},
.error = [](int code) {
std::cout << "Error connecting, reason: " << uWS::code2str(code) << std::endl;
},
.close = [](auto *ws, int code, std::string_view message) {
if (code != uWS::Codes::NORMAL_SHUTDOWN) {
std::cout << "Disconnection, reason: " << uWS::code2str(code) << std::endl;
}
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode, size_t compressedSizeIn) {
std::cout << "Incoming msg was " << compressedSizeIn << " bytes on wire" << std::endl;
size_t compressedSizeOut;
ws->sendCompressed(message, opCode, &compressedSizeOut);
std::cout << "Outgoing msg is " << compressedSizeOut << " bytes on wire" << std::endl;
},
});
Notice that you pass in a URL which can be either ws://
or wss://
just like in 0.14. I think this is convenient since you can just take a URL from the command-line or a config file, and don't have to worry about whether to make a Client
or SSLClient
. Of course this would require URL parsing which is a bit annoying. Not a big deal either way, since users could make a wrapper that does it if it's out of scope for the library.
Thanks!
EDIT: 0.14 lets you choose whether messages should be compressed on a per-message basis. I changed send
to sendCompressed
to illustrate that, but maybe it could just use a boolean argument to send
) (like in 0.14).
EDIT: Made it clear that the connect happens later on in some sort of async callback. This allows multiple connections be open per event loop, as mentioned by @AdrianEddy.
uWebSockets is the fastest ws client library out there. Adding ws client support to 0.18 would allow people using 0.14 to upgrade to the new version.
One possible API, based on @AdrianEddy third suggestion:
// WebSockets to https://example.com/ws
uWS::SSLApp({})
.ws<UserData>("*", {
.open = [](auto *ws, auto *req) {
// ...
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
}
}).connect("example.com/ws", 443, [](auto *token) {
}).run();
Status on this? Thanks.
Please write a few examples of how it could look like in code
I think your (server) APIs are really nice and close to what nodejs has. Perhaps, client should also follow what works well in nodejs?
Problem with these proposed client interfaces is that they break the entire idea with uSockets: to hold a socket context and then instantiate a socket referring to that context.
In other words; you have to first define a shared behavior (the context) then from that context create a socket.
You cannot do both in one function call because then the entire point of all memory hierarchy is wasted.
So the app.ws call needs to be separate from the app.connect call but the app has to be able to hold many different contexts and app.connect has to refer to what context it should use.
The only proposal that remotely follow this rule is this one:
uWS::SSLApp({})
.ws<UserData>("*", {
.open = [](auto *ws, auto *req) {
// ...
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
}
}).connect("example.com/ws", 443, [](auto *token) {
}).run();
Is it suppoed to map outgoing URL "example.com/ws" to the pattern "*"? Like an inverted Http router?
Edit: That example doesn't made any sense anyways, because you cannot have the same context for clients as you can for servers.
Perhaps, client should also follow what works well in nodejs?
Absolutely not. Node.js is the anti-inspiration in every sense.
I'm trying to wrap my head around what you're saying. When I referred to uWebSockets14 client the way the connections happened were with Hubs like so:
uWS14::Hub* hub = nullptr;
uWS14::Group<uWS14::CLIENT> *hubGroup = nullptr;
hub = new uWS14::Hub(0, false, 16777216);
hubGroup = hub->createGroup<uWS14::CLIENT>();
hubGroup->onConnection([](uWS14::WebSocket<uWS14::CLIENT> *ws, uWS14::HttpRequest req){
std::cout << "Connected" << std::endl;
std::string workerInfo = fmt::format("workerinfo-mastersignature:{}/workername:{}/");
ws->send("hello", 6, uWS14::OpCode::TEXT);
});
hubGroup->onMessage([](uWS14::WebSocket<uWS14::CLIENT> *ws, char *message, size_t length, uWS14::OpCode opCode) {
std::cout << "Got reply: " << std::string(message, length) << std::endl;
ws->send(message, strlen(message), uWS14::OpCode::TEXT);
//ws->terminate();
});
hubGroup->onDisconnection([](uWS14::WebSocket<uWS14::CLIENT> *ws, int code, char *message, size_t length) {
std::cout << "Disconnect." << std::endl;
});
hubGroup->onError([](uWS14::CLIENT::type errorType){
ERROR("HubGroup Error, couldn't connect: {}", errorType);
});
hub->connect("ws://someservice.local:3333", nullptr, { }, 5000, hubGroup);
hub->connect("ws://someOtherService.local:3334", nullptr, { }, 5000, hubGroup); // note the reuse of hubGroup
It seems to me that the HubGroup
from the past is similar to the socket context that you're talking about. The slick thing about the current version of uWebSockets is that the websocket context is built into the server application initialization. At first glance I don't think that's easily possible to do on the client side. Each connection needs its own handlers unless you want to heavily generalize it.
I think a modified version of https://github.com/uNetworking/uWebSockets/issues/791#issuecomment-585656572 would work best. No .connect()
, just .run()
.
uWS::SSLApp({})
.wsClient<UserData>("wss://example.com:443", {
.connection = [](auto *ws, auto *req) {
// ...
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
},
.disconnection = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
},
.error = [](errorType error) {
// ...
},
}).run();
That rvalue reference settings object is essentially the aforementioned HubGroup
from what I can tell. Can you just either have an array of them or allow multiple callings of wsClient to set the handlers and context for different connections? If it's possible to refer to the SSLApp later then we can connect to a bunch of stuff from within the handlers or with some kind of functionality inside the UserData
. I think HubGroup isn't really a good name for it btw. I have no suggestions for names.
I would like to add that I have not dropped down into the uSockets internals much so I don't know how reusable the us_socket_context_t
is or what can be derived from them but from the hammer test https://github.com/uNetworking/uSockets/blob/577c822ac2e27d297eb6ea8712c84db5bbfe3b44/examples/hammer_test.c it seems like they share something.
Last post for the night, but I think that settings object would work for choosing if we're sending a get, post, or put as well if it's just an http request instead of a websocket initialization request. That would be a Client
instead of wsClient
. So they would be write or read handlers, similar to async multi curl. I think doing it that way would make it easier to fold both the client and the server together into one run loop. I don't know whether doing it that way or not make sense though.
OK so I looked into doing some rough socket work with uSockets and I think I understand what you're getting at by saying that the app.connect needs to understand what context it should refer to.
struct user_data {
char *backpressure;
int length;
int testvalue;
};
struct us_socket_t* s = us_socket_context_connect(SSL, client_context, other_host, other_port, NULL, 0, sizeof(struct user_data));
struct user_data *usr = (struct user_data *)us_socket_ext(SSL, s);
That usr
right there is separate from any other connection you create even with the same client_context so that equates to the UserData from my previous suggestion. Is the socket_context for connections only used to connect the loop, handlers and ssl options to the socket?
It's a hierarchy from most general to most specific. Only the most specific memory is kept per socket. The most general is kept per loop, which is kept per thread. Every part can have user data, even loop and socket context.
So all settings and callbacks are stored per socket context, not per socket. That's why sockets are down to 90 byte or so. This is very different from how most other implementations do it
The socket context is the "behavior". Sockets are created using a specific "behavior". Therefore the API needs to work like this; first you define the behavior then you instantiate from that behavior a socket
If it's meant to work like that then the previous suggestion is definitely clunky. Maybe this makes more sense? C++20 allows string literals as a template parameter. I think that feature makes this possible.
Here's how it should be initialized
uWS::SSLApp({})
.context<"ContextName", UserData>({
.open = [](auto *ws) {
// ...
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
},
.ping = [](auto *ws) {
// ...
},
.pong = [](auto *ws) {
// ...
},
.close = [](auto *ws, int code, std::string_view closeVal) {
// ...
}
})
.context<"ContextName2", UserData>({
.open = [](auto *ws) {
// ...
},
.message = [](auto *ws, std::string_view message, uWS::OpCode opCode) {
// ...
},
.ping = [](auto *ws) {
// ...
},
.pong = [](auto *ws) {
// ...
},
.close = [](auto *ws, int code, std::string_view closeVal) {
// ...
},
})
.run();
Here's how it could be used while looping.
This should work for the websocket client. They could also support custom ones that are less efficient but possibly more flexible.
struct SocketUserData{
void* something = nullptr;
};
uWS::connect<SocketUserData>(std::string_view contextName, std::string_view host, uint16_t port); // if the template literals don't work
uWS::connect<"ContextName2", SocketUserData>(std::string_view host, uint16_t port); // if the template literals work
uWS::connect<SocketUserData>(std::string_view_host, uint16_t port, WebSocketSettingsStruct&& thingy); // flexibility? Maybe not needed.
These should work for http requests since the signatures are different. The API is clean but I don't know about its clarity. Maybe you can do something with contexts here as well for default handlers.
uWS::get(std::string_view host, uint16_t port, fu2::unique_function<void(uint8_t*)> &&handler);
uWS::post(std::string_view host, uint16_t port, uint8_t* data, fu2::unique_function<void(uint8_t*)> &&handler);
uWS::put(std::string_view host, uint16_t port, uint8_t* data, fu2::unique_function<void(uint8_t*)> &&handler);
uWS::patch(std::string_view host, uint16_t port, uint8_t* data, fu2::unique_function<void(uint8_t*)> &&handler);
uWS::del(std::string_view host, uint16_t port, fu2::unique_function<void(uint8_t*)> &&handler);
Input would be appreciated.
I can't help but feel you've already thought of this solution before but it's the best I can seem to come up with.
The idea currently is that you attach behavior (context) to routes (URLs), then listen to incoming connections.
Doing the opposite for clients, attaching behavior to whole URLs and then referring to that behavior when connecting could be a natural extension.
This is close to, or identical to what @AdrianEddy proposed.
This makes sense because your behavior is very likely to be grouped by URLs, not by any individual connection. So for instance, all websockets connecting to wss;//api.bitfinex.com are very likely to have the same behavior.
SSLApp().wsClient("api.bitfinex.com", {the behavior here, defined once}).connect("wss://api.bitfinex.com", [](auto *error) {}).
I think this solution has the same benefits and drawbacks as the current incoming router has, for instance, defining the same behavior for many different routes or URLs requires duplicated contexts - but by using wildcards this could be mitigated in similar ways as we already do.
I think this is the best and cleanest API to build on, for now. It makes a lot of sense in my mind, and allows to efficiently re-use contexts when establishing many connections to the same URL/service.
You could have "*" wildcards for all outgoing URLs, "*.bitfinex.com/*" for semi-wildcards, or entirely static "api.bitfinex.com" ones. "wss://" or "ws://" should be excluded, or ignored if present.
This does make a lot of sense to me. Clearly the best API!
To those wondering about status - as you can see we have a rough idea about the APIs, but this remains a low priority issue that is not being worked on.
I don't have any customers who want or need client support, they are all on the server side and only care about server side performance. If you want client support, you can order it by paying for contracting hours required to make this happen. I would say 2 weeks or less.
It's not going to happen by waiting for it to happen, so if you need this it's up to you. I have no intention to work on things I have no use for. And frankly, if something is not paid for that means it has no commercial value, hence shouldn't exist. It's basic capitalism at play.
As you prefer. But the client support would make the library more complete and allow people like me to use it instead of discard it. Cheers ;)
As you prefer. But the client support would make the library more complete and allow people like me to use it instead of discard it. Cheers ;)
Go ahead, use an inferior, poorly conceived websocket library since you don't care about quality. If you cared about your work you'd use this one.