Thinking libevent in C++
Brief
'libevent' does help communication programming a lot! While there are still too many 'callbacks' and 'void opaque*' making things not easy.
Can we just think communication stuff in OO? For example, when we need to connect to somewhere via tcp, we need something like this:
class BaseConnection
{
public:
void connect_tcp(const char *hostname, int port);
// we dont't want callback functions with 'void* opaque', C++ virual function plays this role.
virtual void on_conn_event(short what); // when get connected or disconnected, this 'CB' will be called.
virtual void on_readable(); // when income data available, this 'CB' will be called.
virtual int queue_to_send( const void *data, size_t size); // as name implies, this the 'aync write' helper.
};
When we are on 'server side' , we need:
class BaseListener
{
public:
void start_listen_on_tcp(const char* addr);
virtual void listener_cb( evutil_socket_t fd, struct sockaddr *sa, int socklen) = 0; // implement this 'CB' to hadle incoming connections
};
So, a tiny tcp server program will be as simple as:
#include "EventLoop.h"
#include "listeners.h"
#include "signals.h"
#include "connections.h"
static const char MESSAGE[] = "Hello, World!\n";
static const int PORT = 9995;
class HelloServerConnection // our 'connection', when connected it says a hello msg to peer then disconnect.
:public BaseConnection
{
public:
HelloServerConnection(SimpleEventLoop* loop, evutil_socket_t fd )
: BaseConnection(loop) {
take_socket(fd, EV_WRITE );
}
virtual void on_writable()
{
struct evbuffer *output = bufferevent_get_output(bev);
if (evbuffer_get_length(output) == 0) {
printf("flushed answer\n");
delete this;
}
}
};
class HelloWorldListener // our real 'listener', creates our 'connection' instances to handle incoming connecting request.
: public BaseListener
{
public:
HelloWorldListener(SimpleEventLoop * loop)
:BaseListener(loop ) {
}
virtual void listener_cb( evutil_socket_t fd, struct sockaddr *sa, int socklen) {
HelloServerConnection* conn = new HelloServerConnection( my_app, fd );
conn->queue_to_send( MESSAGE, strlen(MESSAGE));
}
};
int main(int argc, char **argv){
try {
SimpleEventLoop loop;
//1. TCP listen on PORT
HelloWorldListener tcp_listener( &loop);
tcp_listener.start_listen_on_tcp( CString("0.0.0.0:%d", PORT).c_str() );
//2. also we quit gracefully when ctrl-C
QuitSignalHandler control_c_handler(&loop);
//3. the main loop
loop.run();
printf("done\n");
}
catch (std::exception& ex) {
LOG_ERROR("Exception in main(): %s\n", ex.what());
return 1;
}
return 0;
}
Sure, in real world, we need much more.
The 'BaseConnection' should treat 'unix domain socket', 'serial port', 'generic file' , 'stdio' in similar way, and there should also be 'udp' wrapper, 'timer', 'signal handler', 'http server wrapper', and so on.
What is more important, we need a 'connection manager' when we build real (server ) applications. And since we are on async style, we can not do any block operation (such as DB accessing) in any 'CB', we need worker-thread / thread-pool, and should be able to 'marshal' jobs between main-thread and worker-thread freely.
When we have all stuff mentioned above in hand, we can view the framework of a brand new back-end service.
Build dependences
(On Fedora) yum install gcc-c++ libstdc++-devel libevent-devel socat.
Build and run
$ make #build all samples
$ make test_hello #run the 'libevent hello-world sample' C++ ported.
$ make test_cat #run the 'cat' sample, which implement 'cat' in libevent style.
$ make test_wt #run the 'wt_demo' sample, which does some stuff in worker thread.
$ make test_wt2 #run the 'wt_demo2' sample, which does some stuff in worker thread pool.
$ make test_echo #run the 'echo to all' sample -- a chat server echoes any income msg to all cliets.
$ make test_http_server #run sample of httpserver wrapper.