Advertisement

netLink a c++11 networking library

Started by December 29, 2013 08:44 PM
5 comments, last by hplus0603 10 years, 10 months ago

Hi,

I'm developing my own game engine for some time now and wanted to add multiplayer/network support.

Therefore I was looking around the web for a c++ (11) library which suites my needs for a game engine and not for a browser.

I wanted a lightweight library without the big overhead of HTTP, XML/DOM-parser or even Boost.

But such a thing didn't seem to exist, so I decided to write my own one:

netLink which can be found under zlib licence on Github.

It supports IPv4/IPv6 UDP/TCP sockets, a manager to handle multiple connections and MsgPack, which is a JSON like binary format.

I can now transmit small packets (like position updates or chat messages) over those sockets using the MsgPack encoding.

My questions:

Do you have a use case for such a library and were you looking for something like this too?

Is there something important missing (except the windows support) ?

Or do you have good experiences with other libraries and when yes, which ones and why?

I think it would be easier to understand what your library does, compared to others, if you actually described the difference.

For example, in addition to MsgPack, there is the std::stringstream class, and Google Protocol Buffers, and Apache Thrift, and boost::serialization, and NFS XDR, and a number of other binary serialization libraries. How is MsgPack different, and why should I use that instead? Do you have specific metrics of what you're trying to optimize, and measurements to show how the different libraries fare?

For base connectivity, there is plain TCP sockets, and boost::asio, and Enet, and RakNet, and ZeroMQ, and SDLNet, and ClanLib, and a number of other libraries, which provide different kinds of support. Again, what is your design goal; why is your library different; and do you have numbers?

I look forward to learning more about your library, what you learned from the others, and how it is different from the others that came before it.
enum Bool { True, False, FileNotFound };
Advertisement

Why did I decide to use MsgPack?

Compared to std::stringstream:

* All std::streams are just passing through bytes, they don't really serialize or deserialize data structures

* You would have to make your own encoding based on ASCII chars and that isn't space efficient

Compared to Google Protocol Buffers:

* Define message formats in a .proto file.
* Use the protocol buffer compiler. (Not needed with MsgPack)
* Use a C++ API to write and read messages. (There's no getting around it)
Compared to Apache Thrift:
* It supports more container types, but less primitive types than MsgPack
* Write a Thrift IDL file and use the code generator. (Not needed with MsgPack)
* It is just a personal preference of mine but I don't like Boost. It is just redundant because we already have the std::lib.
* I don't overlook the documentation. But it seems to have similar features compared to MsgPack.
All in all, what makes MsgPack unique?
* It is like the widespread JSON, but binary.
* Data and types are encapsulated: e.g. You don't have to worry how a number is stored,
the serializer decides to use (u)int8/16/32/64 or float32/64.
* The data flow is controllable. You can feed to deserializer with small pieces of data and
will get the results immediately without having to wait until an entire packet is deserialized.
What about sockets and connectivity?
Compared to unix sockets, Enet, SDL_net and ZeroMQ:
* They are plain C, no OOP, just ugly function calls with to many parameters or structures.
Compared to boost::asio:
* Well again: It is boost and I consequently avoid it.
Compared to RakNet:
* It is not open source and has a commercial license.
* But it is especially made for games.
Compared to ClanLib:
* It is more like a entire game engine, not just a networking library.
* It doesn't seem to have as many features as my library has, for example: UDP multicast and broadcast are missing.
All in all, what makes my netLink unique?
* It is OOP, easy to use, light-weight and you don't have to care about stuff like select() or blocking.
* You can use it with std::streams for I/O or call the base functionality directly (multiple abstraction layers).
* It supports IPv4 and the coming IPv6.
* It is written in C++11 and LLVM compiled, so it is more efficient than older versions.


Compared to std::stringstream:
* All std::streams are just passing through bytes, they don't really serialize or deserialize data structures

Well, the sole and defined purpose of std::ostream is to serialize data to a text stream, and of std::istream is to deserialize data from a text stream. Perhaps what you meant was that human-readable serialization is less efficient? Numbers to show this is true would be useful. Make sure you include the engineer-hours costing of debugging problems with non-human-readable serialization streams. Remember, tools like wireshark and telnet are already available to make debugging human-readable on-the-wire problems fast and easy.

The purpose of std::streambuf is to just pass through bytes. Your MsgPack is basically reinventing std::streams, only serializing as big-endian ILP64 binary data instead of text. Fair enough, but I'd like to see numbers to decide if that's better.

* You would have to make your own encoding based on ASCII chars and that isn't space efficient
All in all, what makes my netLink unique?
* It is OOP, easy to use, light-weight and you don't have to care about stuff like select() or blocking.

While I see OOP as a minor advantage, you have a long way to go to provide a modern OOP interface. You seem to require creating and assembling objects externally (create the Socket, then use a train wreck to insert it into the SocketManager -- why not have a factory member of the SocketManager that does not expose internal details)? I've seen worse, but this could be improved. Simplicity is king in the land of APIs.

I can't see any way to actually use your library without blocking. Is the blocking listen() call the only way? Is there no way to integrate it into my main event loop?

Requiring LLVM is a bit of a deal-breaker. Not insurmountable for a binary distribution, but limiting.

Stephen M. Webb
Professional Free Software Developer

Perhaps what you meant was that human-readable serialization is less efficient? Numbers to show this is true would be useful. Make sure you include the engineer-hours costing of debugging problems with non-human-readable serialization streams.

Well the idea of using a library for binary serialization is that the debugging is as simple as if the format its self would be human readable. For example my implementation has a method stringify() which generates a human readable representation if needed for debugging.

Your MsgPack is basically reinventing std::streams, only serializing as big-endian ILP64 binary data instead of text. Fair enough, but I'd like to see numbers to decide if that's better.

Just to make clear that I didn't invent MsgPack, its an open format. You can click "Try!" at the website and compare it to the human readable format JSON. In my case it compressed up to 66% with a average around 33%.

While I see OOP as a minor advantage, you have a long way to go to provide a modern OOP interface. You seem to require creating and assembling objects externally (create the Socket, then use a train wreck to insert it into the SocketManager -- why not have a factory member of the SocketManager that does not expose internal details)? I've seen worse, but this could be improved. Simplicity is king in the land of APIs.

The thing is that a SocketManager is not necessary just a simplification to manage multiple sockets at once. But it is a good idea to add some sugar here. I reformed the examples from:


std::shared_ptr<netLink::Socket> socket(new netLink::Socket());
socketManager.sockets.insert(socket);

to


//std::shared_ptr<netLink::Socket> socket = socketManager.generateSocket();
auto socket = socketManager.generateSocket();

Is this what you meant?

I can't see any way to actually use your library without blocking. Is the blocking listen() call the only way? Is there no way to integrate it into my main event loop?

The listen() call can be blocking or non blocking, it depends on the sockets (they are non blocking by default) and if a time to wait is given. So I don't see any problem integrating it into a main event loop. It is the same in my own game engine.

Requiring LLVM is a bit of a deal-breaker. Not insurmountable for a binary distribution, but limiting.

I also tried GCC but it got behind in terms of C++11 support so I switched to Clang and LLVM.

This only my personal opinion, but I think LLVM is the future and it will surpass all other compilers and byte codes.

Perhaps what you meant was that human-readable serialization is less efficient? Numbers to show this is true would be useful. Make sure you include the engineer-hours costing of debugging problems with non-human-readable serialization streams.

Well the idea of using a library for binary serialization is that the debugging is as simple as if the format its self would be human readable. For example my implementation has a method stringify() which generates a human readable representation if needed for debugging.

Having to write code to view what's going over the wire isn't the same as just viewing it. When I need to debug what an HTTP server returns I can telnet to the server, issue a GET (or a precanned POST from a text file) and view the results immediately.

I'm also not convinced using anything but text serialization is ever a significant savings, given network speeds. Even back in the 1990s when we had 4 MB/s token rings we couldn't justify the saved microseconds on a millisecond-level transport, but losing a couple of hours debugging time would wipe our margins on support.

Then again, I don't mean to sound harsh. I'm just asking if there's more than theoretical justification for the tradeoff, speaking from a project manager point of view ('cos it's what i do with much of my days).

The thing is that a SocketManager is not necessary just a simplification to manage multiple sockets at once. But it is a good idea to add some sugar here. I reformed the examples from:


std::shared_ptr<netLink::Socket> socket(new netLink::Socket());
socketManager.sockets.insert(socket);
to

//std::shared_ptr<netLink::Socket> socket = socketManager.generateSocket();
auto socket = socketManager.generateSocket();
Is this what you meant?

Yes. Very nice.

I can't see any way to actually use your library without blocking. Is the blocking listen() call the only way? Is there no way to integrate it into my main event loop?

The listen() call can be blocking or non blocking, it depends on the sockets (they are non blocking by default) and if a time to wait is given. So I don't see any problem integrating it into a main event loop. It is the same in my own game engine.

I don't see it. I see listen() is blocking with a timeout. That means I could implement polling, but polling is anathema in a production server environment. I don't have a problem with the listen() interface, but I would also want a way to integrate the library into my own event loop and avoid the poorly-scalable select() altogether.

On a Linux system you could use epoll() to provide a socket I could integrate into my event loop. BSDs have kqueue(). WSA has IOCPs. If I have a server trying to handle thousands of connections these are all more scalable. Just something to consider.

Stephen M. Webb
Professional Free Software Developer

Advertisement
First, stringstream (or other ostreams) can write bytes rather than text if you want. Of course, doing so, makes reading the packets inside something like WireShark (or asking your ISP to read the packets) a lot harder.

Also, text-mode data that is highly repetetive is not very much bigger than binary-mode data that expresses the same information, once compressed. Text-mode formats that are much bigger, typically either encode more than the binary format, or uses some kind of verbose encoding that doesn't compress as well as it should. If the information is truly the same, then the Shannon entropy limit is the same, and thus they should compress to the same amount of data. Again, compression (be it text or binary) will make reading the plaintext of the packets harder.

Second, it's my opinion that if you want to use a binary format, you should not do "binary JSON." The point of using JSON (or, worse, XML,) is that the data is self-describing. But if you're writing a game where you know what the format of the packets is, it's more important to use fewer bytes, than to be resilient to re-play of captured streams ten years from now. Thus, you should know what fields will be included, and in what order, and send only the field values. Because the sender and receiver agree on what fields are in messages of type X, they will read/write the same fields in the same order, and no field tags are needed.
MessagePack is extra bad for this, because it doesn't even tokenize the various field names, instead writing out the strings -- thus even Protobuf will outperform it.

Third, tying yourself to Clang may be what you want to do for your own library; that's fine! If you want others to use the library (which I presume is the reason you're asking for feedback,) then compatibility with a variety of platforms is important. Those platforms include Windows (MSVC,) Linux (gcc,) PlayStation (gcc,) Xbox (MSVC,) MacOS X (clang,) iOS (clang/obj-c) and Android (gcc/java.) Engineering for portability is generally a more healthy trait in a library than engineering for using/needing all the latest features of any particular technology.

The goals you set for yourself may be different than the goals I assume here, though, so the feedback I'm giving is mostly applicable if your goals match those I suggest :-)
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement