Jump to content
Nytro

Event driven socket programming

Recommended Posts

Posted

[h=1]Event driven socket programming[/h]Author:

[h=3]nslay[/h]I've been working on a fully featured IRC bot for a couple months on/off and I usually write my own IO multiplexer. This time, however, I gave libevent a try and decided to share my experiences.

libevent

Anyway, if you're not familiar with event-driven design and you're interested in socket programming, I strongly recommend you learn about event driven programming first since it tends to make socket programming easier and more flexible. Then employ something like libevent (or write your own) that allows you to hook IO events for sockets.

You'll find that your code

  • Generalizes to multiple sockets with no effort (and no threads)
  • Often implicitly supports timers with no effort (depending on the underlying polling mechanism you use)
  • Generalizes to multiple protocols (since you can just hook protocol-dependent handlers per socket)
  • Seamlessly integrates with OOP well (rather than have one object that has a blocking recv() loop, just implement an OnRead() method and support multiple objects simultaneously!)

Anyway, here's my IRC bot. It is fully featured, though since it's a replica of something written in ~1998, doesn't have cool features like searching wikipedia (for example).

http://sourceforge.n...rojects/ircbnx/

To see examples of event driven design, look at IrcClient.cpp and BnxDriver.cpp. Specific snippits are included below.

Relevant snippits from IrcClient.cpp

Here's a nifty trick to dispatch C callbacks to C++ member functions (I learned this trick from here)

template<void (IrcClient::*Method)(evutil_socket_t, short)>
static void Dispatch(evutil_socket_t fd, short what, void *arg) {
IrcClient *pObject = (IrcClient *)arg;
(pObject->*Method)(fd, what);
}

You can redirect C callbacks to your C++ member functions. In this case, I use it dispatch C callbacks to the following basic IO/timer functions:

// Libevent callbacks
void OnWrite(evutil_socket_t fd, short what);
void OnRead(evutil_socket_t fd, short what);
void OnSendTimer(evutil_socket_t fd, short what);

You can use the basic Read/Write events to determine

  • When a socket has completed a connection (OnWrite() is called)
  • Receiving data (OnRead() is called and recv() returns > 0)
  • When the remote host has closed the connection (OnRead() is called and recv() returns 0)

Here's how you do it in an event driven framework. Here is IrcClient::Connect()

First create the socket:

bool IrcClient::Connect(const std::string &strServer, const std::string &strPort) {
if (m_socket != INVALID_SOCKET)
Disconnect();

m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

if (m_socket == INVALID_SOCKET) {
Log("socket failed (%d): %s", errno, strerror(errno));
return false;
}

Next, set it to non-blocking. Non-blocking means that connect() and recv() will never put the process to sleep until the request has completed. It's easy to understand why you want this in the context of multiple connections. If you are handling 50 sockets, and you do not enable non-blocking I/O, then mishandling just one of those sockets causes the process to hang (and the other 49 sockets will not receive service until a request has completed one that one socket).

#ifdef _WIN32
u_long opt = 1;
if (ioctlsocket(m_socket, FIONBIO, &opt) != 0) {
Log("ioctlsocket failed (%d)", WSAGetLastError());

CloseSocket();

return false;
}
#else // _WIN32
int flags = fcntl(m_socket, F_GETFL);

if (fcntl(m_socket, F_SETFL, flags | O_NONBLOCK) == -1) {
Log("fcntl failed (%d): %s", errno, strerror(errno));

CloseSocket();

return false;
}
#endif // !_WIN32

In this code, I use getaddrinfo() to resolve hostnames. It is more flexible and hides the hackish way you normally resolve a hostname and setup a sockaddr.


struct addrinfo hints, *pResults = NULL;
memset(&hints, 0, sizeof(hints));

hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;

int e = getaddrinfo(strServer.c_str(), strPort.c_str(), &hints, &pResults);
if (e != 0) {
Log("getaddrinfo failed (%d): %s", e, gai_strerror(e));
CloseSocket();
return false;
}

Now we connect the socket. It will most likely fail with EAGAIN or WSAEWOULDBLOCK depending on platform. These mean the connection in progress and not yet completed and so you must treat these as not errors.

    // NOTE: Cast to socklen_t since Windows make ai_addrlen size_t
e = connect(m_socket, pResults->ai_addr, (socklen_t)pResults->ai_addrlen);

#ifdef _WIN32
int iLastError = WSAGetLastError();

if (e != 0 && iLastError != WSAEWOULDBLOCK) {
Log("connect() failed (%d)", iLastError);
CloseSocket();
freeaddrinfo(pResults);
return false;
}
#else // _WIN32
if (e != 0 && errno != EINPROGRESS) {
Log("connect() failed (%d): %s", errno, strerror(errno));
CloseSocket();
freeaddrinfo(pResults);
return false;
}
#endif // !_WIN32

freeaddrinfo(pResults);

Now, we create our event contexts and hook them. The write event is only meant to be triggered once to determine connectivity, hence EV_PERSIST is missing. Our read event is intended to be used to receive data and is set to EV_PERSIST. The last created event is a timer for send queues.

We only initially need to hook OnWrite() since we need to determine when the connect() has actually completed. Once it completes, we can then hook OnRead() and OnSendTimer().

Notice the use of the templated Dispatch() function. Nifty huh?


// XXX: Handle errors?
m_pWriteEvent = event_new(m_pEventBase, m_socket, EV_WRITE, &Dispatch<&IrcClient::OnWrite>, this);
m_pReadEvent = event_new(m_pEventBase, m_socket, EV_READ | EV_PERSIST, &Dispatch<&IrcClient::OnRead>, this);
m_pSendTimer = event_new(m_pEventBase, -1, EV_PERSIST, &Dispatch<&IrcClient::OnSendTimer>, this);

event_add(m_pWriteEvent, NULL);

m_strCurrentServer = strServer;
m_strCurrentPort = strPort;

return true;
}

Now here's OnWrite(). First it hooks OnRead() and then it sets up the send queue timer to run in 0.5 second increments. Because the write event was not created with EV_PERSIST, it will only trigger once.

void IrcClient::OnWrite(evutil_socket_t fd, short what) {
event_add(m_pReadEvent, NULL);

// TODO: Tunable for send timer
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500000;

event_add(m_pSendTimer, &tv);
m_clSendCounter.SetTimeStep(0.5f);

OnConnect();
}

And finally, here's OnRead() which constructs lines (since SOCK_STREAM sockets may produce fragmented data). If there are unexpected problems (such as remote host closing connection), then it dispatches said events.

void IrcClient::OnRead(evutil_socket_t fd, short what) {
#ifdef _WIN32
int readSize = recv(m_socket, m_stagingBuffer + m_stagingBufferSize,
(int)(sizeof(m_stagingBuffer)-1-m_stagingBufferSize),0);
#else // _WIN32
ssize_t readSize = recv(m_socket, m_stagingBuffer + m_stagingBufferSize,
sizeof(m_stagingBuffer)-1-m_stagingBufferSize,0);
#endif // !_WIN32

if (readSize == 0) {
Log("Remote host closed the connection.");
OnDisconnect();
return;
}
else if (readSize < 0) {
Log("recv() failed (%d): %s", errno, strerror(errno));
OnDisconnect();
return;
}

time(&m_lastRecvTime);

m_stagingBufferSize += readSize;

m_stagingBuffer[m_stagingBufferSize] = '\0';

char *p, *q;

p = q = m_stagingBuffer;

// We check the buffer size since Disconnect() can be called somewhere in ProcessLine()
while (m_stagingBufferSize > 0 && (q = strpbrk(p,"\r\n")) != NULL) {
*q = '\0';

m_stagingBufferSize -= (q-p) + 1;
if (q != p)
ProcessLine(p);

p = q + 1;
}

memmove(m_stagingBuffer, p, m_stagingBufferSize);
}

Relevant snippits from BnxDriver.cpp

Lastly, you need to actually dispatch the events. libevent makes this very simple. Here's BnxDriver::Run(). Notice that it can seamlessly handle multiple BnxBot objects with just one thread.

bool BnxDriver::Run() {
if (!Load())
return false;

struct event_base *pEventBase;

pEventBase = event_base_new();

for (size_t i = 0; i < m_vBots.size(); ++i) {
m_vBots[i]->SetEventBase(pEventBase);
m_vBots[i]->StartUp();
}

event_base_dispatch(pEventBase);

event_base_free(pEventBase);

return true;
}

Conclusion

For socket programming, you should definitely seek an event-driven design for simplicity and scalability. Enjoy.

If you want to see the code, check the repository or download the source zip. It's too big to paste here.

http://sourceforge.n.../85/tree/trunk/

http://sourceforge.n...s/ircbnx/files/

Sursa: Event driven socket programming - rohitab.com - Forums

Posted (edited)

Nice ! Thanks for sharing ! I will take a look @ it !

For those who don't know libevent is a mature library used in allot of well known applications like : ntp , memcahched , TOR, SCANssh and many others.

Edited by backdoor

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...