Tuesday, January 8, 2013

poll() vs select() in Handling unlimited connections!

Let's get into detail about poll() and select() first before we talk about handling unlimited connections.

Well, we know that accept() blocks until a client connects to the server. And while we do that, how do we read and write to other sockets if it's blocking? The answer is simple, we either use select() or poll().

What select() and poll() both do is they read from a collection of file descriptors (sockets) and checks if one of those file descriptors (even the server socket!) has any data coming in or is able to have data written on it. Then it sets a flag so you can handle that file descriptor.

Here are the functions themselves:

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

select() blocks until one of the file descriptors get flagged


int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll() returns a non-negative value and doesn't block

Pretty similar if you look at it. So we plug in number of file descriptors and then we put a timeout. Then you got fd_set to hold all of the file descriptors for select() and you got struct pollfd to hold a file descriptor for poll().


But wait, oh shit! The maximum file descriptors that one single fd_set can hold is 1024? That means you can only have 1024 clients on your server because once it reaches that limit, it can't add anymore to that fd_set. 

So what? Why don't we change the fd_setsize and be able allocate more to an fd_set? Sounds too trashy, too messy.

On the other hand, we got poll() and it accepts pollfd, number of file descriptors, and a timeout. So that means we can make an array of struct pollfd[], add in file descriptors to that array and get the element count and just plug it all in.

Wrong! How can array be dynamically allocated? We have to define the number of struct pollfd's we want when we initialize an array. We can't be psychic and just know how many players will connect, we want it to be limitless!

Bradley Cooper in Limitless (2011)
 So we use a vector! A dynamically sized array basically.
vector<pollfd> poll_sockets;
There we go, but it gets a little bit tricky here because poll expects something else and we're giving it something totally different.

Let's add our server socket (called master_socket) in the vector because we want to read it to see if any incoming connections are ready to be accept()ed.
pollfd add_socket;
//We'll just call the pollfd add_socket
add_socket.fd = master_socket;
add_sockets.events = POLLIN;
poll_sockets.push_back(add_socket);
There's a whole page of what kind of events poll() can catch but, in my opinion, you'll really only need POLLIN. But if you do want to find out more events, google is your friend.
So now how do we add it to poll()? Well the solution is to use a vector pointer. Here's how.




pollfd *poll_sockets_ptr;
int poll_activity;
while(server_online){
    poll_sockets_ptr = &poll_sockets[0];
    poll_activity = poll(poll_sockets_ptr, poll_sockets.size(), 0);
    if(poll_activity <0){
        perror("accept");
    }
}
Critical Note: Remember to always keep pointing the poll_sockets_ptr to &poll_sockets[0], because when you add more sockets to poll_sockets, it needs to be reiterated to point to the newly added sockets.

For the poll_activity part, poll() returns less than 0 if it could not poll, otherwise it returns 0 or more than 0 reflecting it's number of flags.

Now you're ready to accept unlimited connections! To handle reading data just do so.
while(server_online){
    poll_sockets_ptr = &poll_sockets[0];
    poll_activity = poll(poll_sockets_ptr, poll_sockets.size(), 0);
    if(poll_activity <0){
        perror("accept");
    }
    if(poll_sockets[0].events & POLLIN){
     //accept() with master socket then create a struct pollfd holding new client with POLLIN and push back to poll_sockets vector.  
   }
}
Make sure you remember to accept or poll() will just keep bothering you until you do. And when you do get a new client socket from that, be sure to add it into poll_sockets the same way to added master_socket and use POLLIN too. 

Because all we ever really need to worry about is reading. (That's how you learn more things)

If you wanted to handle the new client sockets, possibly use a for loop to look through poll_sockets and just POLLIN like with master_socket.


Now it's optimization time! You know how I said earlier that select() blocks until a file descriptor gets flagged? Well poll() doesn't block and it'll just keep going and going on and this is hiking up our CPU.

So to fix this, we make timeout -1, for it to block until it gets a flag.

This is what we could do...
int poll_activity;
while(server_online){
    poll_sockets_ptr = &poll_sockets[0];



        poll_activity = poll(poll_sockets_ptr, poll_sockets.size(), -1);


    if(poll_activity <0){
        perror("accept");
    }
    if(poll_sockets[0].events & POLLIN){

//ACCEPT!
   }
  if(poll_sockets[1].events & POLLIN){

//read() if bytes == 0 then disconnect, if not, handle messages here!
   }
}


No comments:

Post a Comment