-
Notifications
You must be signed in to change notification settings - Fork 1
Fast asynchronous network library/Fast and easy network library. This is a thin wrapper around boost asio, which makes it easy to create fast asynchronous message passing servers and clients.
karellodewijk/fanel
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
This code is a thin wrapper around boost Asio, making efficient asynchronous tcp/ssl networking easy. The library handles the low-level asynchronous network code for you that is kind of tricky to get right and even trickier to get optimal performance. It offers an intuitive interface that should cover a lot of use cases especially if you have control over both the server and the client. Let me give an example of the interface: Server ------- #include <fanel/Tcp_connection.h> #include <fanel/Tcp_acceptor.h> #include <set> #include <iostream> //accepts a connection and sends a welcome message class Acceptor : public Tcp_acceptor<> { public: Acceptor(boost::asio::io_service& io_service) : Tcp_acceptor<>(io_service) {} ~Acceptor() { for (std::set<Tcp_connection*>::iterator it = m_connections.begin(); it != m_connections.end(); ++it) { delete *it; } } //overload of the virtual function Tcp_acceptor::read, called any time a new connection is received //you take ownership of the connections, so we store them in a set void accepted(Tcp_connection* connection) { m_connections.insert(connection); std::cout << "Accepted a connection" << std::endl; std::string welcome_message = "Welcome to the server"; connection->write(welcome_message.data(), welcome_message.length()); } //one of the generated connections is in error void error(Tcp_connection* connection, const system::error_code& error_code) { std::cout << "A connection failed: " << error_code.message() << std::endl; delete connection; m_connections.erase(connection); } //the acceptor throws an error, this does not mean it will stop accepting connections, just that accepting one particular connection failed void error(const system::error_code& error_code) { std::cout << "Accept failed: " << error_code.message() << std::endl; } //prints out everython it receives from connections. void received(Tcp_connection* connection, const char* data, int size) { std::cout << std::string(data, size) << " size: " << size << std::endl; } std::set<Tcp_connection*> m_connections; }; int main() { boost::asio::io_service io_service; Acceptor acceptor(io_service); //listen for connections on port 6000-6002 acceptor.accept(6000); acceptor.accept(6001); acceptor.accept(6002); io_service.run(); return 0; } Asynchronous programming uses callbacks to warn the user that any io data is ready. We need some place for these callbacks to go. In this case they go to the class Acceptor, derived from from Tcp_acceptor<>. (ignore the template <> for now) You have to overload the following 4 functions obligated: 1) virtual void accepted(Tcp_connection* connection) = 0; 2) virtual void error(Tcp_connection* connection, const boost::system::error_code& error) = 0; 3) virtual void received(Tcp_connection* connection, const char* data, int size) = 0; optional: 4) virtual void error(const boost::system::error_code& error) {} 5) virtual void write_done(Connection* connection) {}; And you can call the functions 6) void write(Tcp_connection* connection, const char* data, int size); 7) void accept(int port); 8) coid stop_accept(int port); 1: This function is called when a connection is accepted. You must take ownership of the connection. If you do not store it somewhere and delete it when you no longer need it, you will create a leak. 2: This function is called when an error happened on a particular connection, such as a disconnect, but many others. This just receives all error codes messages boost asio generates and Maximum message size exceeded. We assume in the client here, the error is not recoverable and delete the connection 3: This function is called whenever a connection receives a message. If you use the accompanying client code. Messages written to a connection at one side will come out at the other side unaffected 4: Also the acceptor itself can receives an error. These errors are generally non-fatal, such as unable to bind or a particular accept failed, so we ignore them here. 5: A connection has emptied it's write queue. 6: Write to a particular connection, note that you can also use the connection pointer and call write on that write(connection, "hello", 5) is equivalent to connection->write("hello", 5); The api will copy the data and send it asynchronously, so you do not have to keep the data buffer intact. 7: Start accepting connection on the given port. 8: Stop accepting connection from a port from now on. Already accepted connections will not be dropped. This is all there is to creating a high performance asynchronous network application. Client ------- This code really shines when you are creating both a client and a server or a combination of both. If you think about it tcp clients and servers are not so different. Both generate the same bidirectional connections. The server by accepting connections and the client by connecting to a server(s). This library leverages this by providing an identical interface for both. The only differences between the server above and a client are. - You inherit from Tcp_connector<> - You call void connect(std::string servernamen, int port) rather than accept. 1,2,3,4,5,6 are identical. Connectors also generate connections. 7 and 8 become: 7) void connect(std::string server, int port); 8) void stop_connect(std::string server, int port); 7: Connect to a server 8: Stop connecting to a server. !!!This does not terminate an allready active connection, it just stops a connection in progress but not yet established. So a client that will connect to the above server looks like this. #include <fanel/Tcp_connection.h> #include <fanel/Tcp_connector.h> #include <set> #include <iostream> //can connect to server(s) and sends a welcome message class Connector : public Tcp_connector<> { public: Connector(boost::asio::io_service& io_service) : Tcp_connector<>(io_service) {} ~Connector() { for (std::set<Tcp_connection*>::iterator it = m_connections.begin(); it != m_connections.end(); ++it) { delete *it; } } //You take ownership of the connections received here void accepted(Tcp_connection* connection) { m_connections.insert(connection); std::string hello_message = "Hi, I'm a client"; m_connections.write(hello_message.data(), hello_message.length()); } //one of the connctions generated throws an error. void error(Tcp_connection* connection, const system::error_code& error_code) { delete connection; m_connections.erase(connection); } //the connector has an error, print void error(const system::error_code& error_code) { std::cout << "Connection failed" << std::endl; } //receives and print out data void received(Tcp_connection* connection, const char* data, int size) { std::cout << std::string(data, size) << std::endl; } std::set<Tcp_connection*> m_connections; }; int main() { boost::asio::io_service io_service; Connector connector(io_service); connector.connect("server", 6000); connector.connect("server", 6001); connector.connect("another_server", 6000); io_service.run(); return 0; } And through the magic of multiple inheritance, you can use both in the same application easily. by inheriting from Tcp_connector_and_acceptor<>. You can now generate new connection by both connecting to servers as accepting connection, making it easy to create peer applications. Ssl ---- When the code is compiled with -DUSE_SSL, Ssl_connector/Ssl_acceptor/Ssl_connection will now be availabale in <fanel/Ssl_connector.h>, <fanel/Ssl_acceptor.h> and <fanel/Ssl_connection.h> The interface is much the same than it's non-ssl counterparts: 1,2,3,4,5,6 are again identical. instead of 7 and 8, we have: Ssl_acceptor: 8) ssl_accept(int port, std::string private_key_file, std::string certificate_file, std::string password = ""); 9) ssl_set_certificate(std::string private_key_file, std::string certificate_file, std::string password = ""); 10) ssl_accept(int port); 11) ssl_stop_accept(int port); Ssl_connector: 12) ssl_connect(std::string server, int port, std::string certificate_file, std::string password = ""); 13) ssl_set_certificate(std::string certificate_file, std::string password = ""); 14) ssl_connect(std::string server, int port); 15) ssl_stop_connect(std::string server, int port); 8) Accept connections using the given file to get the certiificate + private key in pem format. If the certificate and/or private key is password protected, you must also supply the password. 9) Set the certificate that all future calls to ssl_accept(port) will use (in pem format) 10) Accept connections using the certificate set in ssl_set_certificate 11) Stop accepting connections on the given port 12) Connect to the given server and port using the certificate or certificate chain in the given certificate file (in format supported by boost::asio::ssl::context::load_verify_file) 13) Set the certificate that all future calls to ssl_connect(std::string server, int port) will use (in format supported by boost::asio::ssl::context::load_verify_file) 14) Connect using the certificate set in ssl_set_certificate 15) Stop a connection in progress on given server and port. !!!This will not terminate established connections. Network protocol and framing ----------------------------- This library provides reliable end to end message transmissions over tcp. To support this, the library needs to know when one messages ends and another begins, known as framing. Tcp does not provide this information, it is seen by the application as a constant stream of data. By default the library uses a length prefix encoded as a 4 byte binary unsigned int in network byte order (big endian). This is a variable that can be read across platorm with the htol (host to network long) and the ntol(network to host long) calls. The message itself can be anything, text, binary data, whatever. To provide the possibility to interopperate with some existing protols, the library supports 3 compile flags that change this behaviour. The first is DELIMITER (in g++, use -DDELIMITER=\"::\" for a :: delimiter). If you define this delimiter, the library will no longer send the size prefix first but will instead add the given delimiter at the end of every message and will remove it again at the other end. It is however now the responsibility of the user to supply messages that do not contain the delimiter. The second compile flag is NETSTRING (-DNETSTRING). If defined, the library will use NETSTRING encoding, read http://en.wikipedia.org/wiki/Netstring. The Third is STREAMING (-DSTREAMING). If defined, the library will send data to the application as soon as it arrives. In this mode, no framing will be done. Note that this means that data send at one end can arrive at the other end in different sized blocks. The total data stream will however arrive in order and exactly as it was sent. Socket options --------------- To change socket options on connections, you can just call socket(), to get the underlying socket to change it. If you want to make changes to the socket options of connections before they are conected, then you can do this by subclassing Tcp_connection, put the things you need to do in the constructor, and pass your new connection type as a template parameter to Tcp_connector and Tcp_acceptor. They will now generate your connections. FAQ ---- Q: Does it support ipv6 ? A: Yes, and automatically on all platforms boost asio supports it. Q: Does it support ipv4 ? A: For the connector (client) it is no issue, it will connect to whatever a dns query returns. On the acceptor, the server side it will work on all modern platforms with Hybrid dual-stack such as windows vista and onwards, linux (2.6 kernel) distributions and Mac OS X 10.3 and onwards. Q: I'm getting a compiler error in Tcp_acceptor.h ? A: There is a bug in boost 1.42 and older that makes the upgrade_to_unique_lock not compile on gcc 4.5 and onwards. (like on ubuntu natty). upgrade to boost 1.47 or downgrade the compiler I guess. Q: What libraries do i need to link to, which headers should be in my include path, which compiler options do I need ? A: In short, boost (system/regex/asio/threads), when compiled with -DUSE_SSL also openssl. Q: Why did you not ship it as a shared library ? A: The behaviour of the library depends heavily on compiler options and templates. Since there would be very little precompiled code, I've opted to make it header only. Q: The boost asio examples usually use shared pointers, why don't you ? A: The reason I don't return the pointers in smart pointer is because it is viral. Once a pointer is wrapped in a smart pointer, it is no longer possible to revert to a regular pointer ever. Putting a pointer into a smart pointer is very easy. I wanted to give users the option not to use this. That being set, smart pointers are a natural fit for event based programming. Event based programming puts more emphasis on paths of execution and less on objects and strict ownership. In fact if you use at the implementation you will see smart pointers being used. Getting memory management right with boost asio without them is not easy. Q: Why all the preprocessor flags ? preprocessor flags are evil. A: By using precompiler flags, you don't compile what you don't need. And one the focus of this project was making it more efficient than rolling your own network code. Q: Is it thread safe ? A: Using different acceptors/connectors on different io_service is always threadsafe. You can compile the library with the compile flag THREADSAFE to make the public interface and multiple event handlers on the same io_service threadsafe. It has however not yet been properly tested, so threat it as experimental and inform me of the results. ----- Compile flags: Threads: THREADSAFE If you define this it will make multithread access to the library safe. To actually make event handling multihreaded, use a single io_service and a thread pool calling io_service::run(). Note: Different Acceptors/Connectors with a different io_service can be used in different threads without the need to compile the code THREADSAFE. Note: Because all io-calls are guaranteed asynchronous, you might not need to enable threading. Boost asio in asynchronous mode can service a lot of simultaniously connected sockets from a single thread. Network framing: DELIMITER=\":\" Instead of the default length prefix framing, the network code will use the given delimiter. Note that intermediate delimiters are not automatically escaped. When receiving the message, the delimiter is not included in the size, but you may assume it is present in the buffer if you read past the end of the message. NETSTRING Instead of the default length prefix, the network code will use NETSTRING encoding, see http://en.wikipedia.org/wiki/Netstring STREALING The network code will do no framing Parameters: MAX_MESSAGE_SIZE=number_in_bytes It is wise to define an upper limit to the size of any message. if you do not do this, you will leave your connection vulnerable to DOS attacks. Malicious users just have to send you 1 or a few insanely large messages and your application will keep buffereing them and eventually run out of memory. If the size of any message however execeeds this value an error will be reported and you can disconnect to free the memory. NOTE: It is recommended to define this for servers, the default is 1GB, which is probably much larger than desirable. The algorithms here are copy once, so it is possible the memory usage of any one connection can increase to twice this amount. DEFAULT_BUFFER_SIZE=number_in_bytes Only used with delimiter based framing. This is the initial size of the read buffer. Buffer size will increase if larger messages appear and shrink again over time to this value. Even if your messages are only small, it will improve performance if you increase this value as we use a semi-rotating read buffer protocol and the less rotating the better. Extras: USE_SSL When this option is on, you will be able to use Ssl_connection/Ssl_acceptor/Ssl_connector for ssl based communication
About
Fast asynchronous network library/Fast and easy network library. This is a thin wrapper around boost asio, which makes it easy to create fast asynchronous message passing servers and clients.
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published