Loco
07-13-2002, 12:04 AM
A SIMPLE WAY TO USE SELECT
This żarticle? concentrates in explaining the usage of the "select()" function call in a pratical way. Giving some examples of its use with easy to understand code, and explanations that could lead the reader to understand its use, advantages, disadvantages and even debug/modify existing code...
The first two examples are very easy and try to show the use of select with just one descriptor. The objective is that the reader gets a little knowledge about the system call and some of its parameters.
First of all, you must have an open file descriptor.
As this is the Networking section of the forum, I assume we have a SOCK_STREAM socket that is already connected.
FIRST EXAMPLE:
The first example waits indefinitely until there is data ready to be read on the socket.
int socket_fd, result;
fd_set readset;
...
/* Socket has been created and connected to the other party */
...
/* Call select() */
do {
FD_ZERO(&readset);
FD_SET(socket_fd, &readset);
result = select(socket_fd + 1, &readset, NULL, NULL, NULL);
} while (result == -1 && errno == EINTR);
if (result > 0) {
if (FD_ISSET(socket_fd, &readset)) {
/* The socket_fd has data available to be read */
result = recv(socket_fd, some_buffer, some_length, 0);
if (result == 0) {
/* This means the other side closed the socket */
close(socket_fd);
}
else {
/* I leave this part to your own implementation */
}
}
}
else if (result < 0) {
/* An error ocurred, just print it to stdout */
printf("Error on select(): %s\", strerror(errno));
}
Now the explanation:
First, I fill up an fd_set struct with the information about the sockets that I want to "monitor". In this case, I first unset all bits in the set, then I set the bit corresponding to the socket_fd file descriptor. This is done using the handy macros FD_ZERO and FD_SET
Then I use the select() function to monitor the availability of data on the socket.
The first parameter to select() is the maximum file descriptor that is set in the structs PLUS ONE. That is, if you have 20 file descriptor in the sets, and the maximum value a file descriptor has is 123, then the value passed as the first parameter must be 124.
The subsequent lines just check the select()'s return code, and do what is expected for each situation.
select() can return one of -1, 0, > 0
-1: Means an error was encountered, you should do something about it. I just print the error
0: Means the call timed out without any event ready for the sockets monitored
> 0: Is the number of sockets that have events pending (readability, writability, exception-ability ) :wink:
Q&A:
But you wrote a do { ... } while() loop around the select() call, why did you do that?
The while() loop is used to keep select() restarting each time an EINTR error is thrown. The select() man page reads "... the sets and timeout become undefined, so do not rely on their contents after an error. " That is why I put the initialization and setting of the file descriptor inside the do ... while() loop
Why do you use a do ... while() loop?
That is my coding style, you can as well use whatever you like...
Why don't you just use another fd_set and copy it to the one used in select()?
Don't be impatient, I'll get there soon...
But that is the same as doing a recv() inside a loop... Why bother with so much code?
It's for demonstration purposes only... We'll mess the whole thing up soon.
Why didn't you check for the 0 result?
Well, we do not have a timeout value, do we?
Let's move on to a messier example...
SECOND EXAMPLE:
In this example I take the previous code a little bit farther and write something that may be useful: A recv() with timeout.
As it is meant to be useful (someone may actually find a use for this), I will wrap it inside a function
/*
Params:
fd - (int) socket file descriptor
buffer - (char*) buffer to hold data
len - (int) maximum number of bytes to recv()
flags - (int) flags (as the fourth param to recv() )
to - (int) timeout in milliseconds
Results:
int - The same as recv, but -2 == TIMEOUT
Notes:
You can only use it on file descriptors that are sockets!
'to' must be different to 0
'buffer' must not be NULL and must point to enough memory to hold at least 'len' bytes
I WILL mix the C and C++ commenting styles...
*/
int recv_to(int fd, char *buffer, int len, int flags, int to) {
fd_set readset;
int result, iof = -1;
struct timeval tv;
// Initialize the set
FD_ZERO(&readset);
FD_SET(fd, &readset);
// Initialize time out struct
tv.tv_sec = 0;
tv.tv_usec = to * 1000;
// select()
result = select(fd+1, &tempset, NULL, NULL, &tv);
// Check status
if (result < 0)
return -1;
else if (result > 0 && FD_ISSET(fd, &tempset)) {
// Set non-blocking mode
if ((iof = fcntl(fd, F_GETFL, 0)) != -1)
fcntl(fd, F_SETFL, iof | O_NONBLOCK);
// receive
result = recv(fd, buffer, len, flags);
// set as before
if (iof != -1)
fcntl(fd, F_SETFL, iof);
return result;
}
return -2;
}
The function is meant to be a simple replacement for recv(), it just takes an additional parameter. This means that the function:
- can be interrupted by a signal
- can return -1, 0 or >0
- can read less data than asked for
Additionally, the function can return -2, meaning the operation timed out. I thought about using -1 and setting errno to ETIMEDOUT, but that is a hard error code, and the time out of select() is just an expected condition.
The function creates an fd_set that later initializes and fills in with the socket descriptor. Then it puts the timeout value in the timeval struct.
And then selects() on the set for readability.
What a good explanation! It was so clear!
The function may fail here for two reasons, which I just return to the caller process: -1= some error, even EINTR; and 0 timeout, in this case I return -2 (remember that recv() == 0 when the socket has been closed)
If there is data ready to be recv'd, then I proceed to recv' it. Before doing so, I set the socket to non-blocking mode, and after recv'ing I set it to whatever flags it had before. This means that the function could return -1 with an errno value of EAGAIN/EWOULDBLOCK.
Thank you Rob for pointing this out...
Q&A:
Why do you switch the socket to non-blocking mode before reading?
Well, as Rob noted, the recv() could block even if the socket selected readable. In multithreaded programs its usual that more than one thread recv() using the same descriptor. I just wash my hands clean.
Why don't you wrap the select() and recv() calls inside a do ... while()loop like you did before?
Well, I thought about it (and even wrote part of it), but the idea is to get a similar behaviour to that of recv(). The only thing that is a bit tricky is that you can get a EWOULDBLOCK error even if you are using non-blocking sockets.
I don't see this code useful. It is very similar to the man pages - it doesn't help much
I tried to keep it easy to understand. The rest of this post will be more complicated, and finally you'll get to read something useful.
A NOT SO SIMPLE WAY TO USE SELECT - BUT MORE USEFUL
In this part, I'll try to show how to use select() to check for readability on more than one file descriptor. NOTE: This works for any kind of file descriptor.
THIRD EXAMPLE:
I assume that I have an array of integers (the array with file descriptors), and that I am going to check for readability on all of them at the same time.
int myfds[N], maxfd, j;
fd_set readset;
...
// Initialize the set
FD_ZERO(&readset);
maxfd = 0;
for (j=0; j<N; j++) {
FD_SET(myfds[j], &readset);
maxfd = (maxfd>myfds[j])?maxfd:myfds[j];
}
// Now, check for readability
result = select(maxfd+1, &readset, NULL, NULL, NULL);
if (result == -1) {
// Some error...
}
else {
for (j=0; j<N; j++) {
if (FD_ISSET(myfds[j], &readset)) {
// myfds[j] is readable
}
}
}
Wasn't that bad, eh?
Well, in fact this is almost the core of every process using select().
There is not much to say about this code, except that more than one socket may select readable at the same time, and that is why I use a loop after checking "result > 0"...
Q&A:
What is that (maxfd>myfds[j])... stuff all about?
It's just a way to save the greatest file descriptor value. The first parameter to select() requires so.
It doesn't help me much. I need to implement an echo server for "Networking essentials 101", and you are not being of much help
Yes, I know. But if you really want to learn (unless you are me, Albert Einstein or that other guy I can't recall) you need to begin with easy things and move to the difficult ones when you feel confortable.
And now... for those of you that don't want to wait any longer...
FOURTH EXAMPLE:
In this (hopefully) last example, I will show part of the code of an echo server. It just waits for new connections (on some TCP port) and each new connection is kept in a fd_set. When data is available in one or more of the sockets, it just recv's it and echoes back through the same socket.
I will NOT use non-blocking sockets.
I will only check for the basic errors and will try to keep the code as simple as possible.
So, here I go:
fd_set readset, tempset;
int maxfd, flags;
int srvsock, peersoc, j, result, result1, sent, len;
timeval tv;
char buffer[MAX_BUFFER_SIZE+1];
sockaddr_in addr;
/* Here should go the code to create the server socket bind it to a port and call listen
srvsock = socket(...);
bind(srvsock ...);
listen(srvsock ...);
*/
FD_ZERO(&readset);
FD_SET(srvsock, &readset);
maxfd = srvsock;
do {
memcpy(&tempset, &readset, sizeof(tempset));
tv.tv_sec = 30;
tv.tv_usec = 0;
result = select(maxfd + 1, &tempset, NULL, NULL, &tv);
if (result == 0) {
printf("select() timed out!\n");
}
else if (result < 0 && errno != EINTR) {
printf("Error in select(): %s\n", strerror(errno));
}
else if (result > 0) {
if (FD_ISSET(srvsock, &tempset)) {
len = sizeof(addr);
peersock = accept(srvsock, &addr, &len);
if (peersock < 0) {
printf("Error in accept(): %s\n", strerror(errno));
}
else {
FD_SET(peersock, &readset);
maxfd = (maxfd < peersock)?peersock:maxfd;
}
FD_CLR(srvsock, &tempsock);
}
for (j=0; j<maxfd+1; j++) {
if (FD_ISSET(j, &tempset)) {
do {
result = recv(j, buffer, MAX_BUFFER_SIZE, 0);
} while (result == -1 && errno == EINTR);
if (result > 0) {
buffer[result] = 0;
printf("Echoing: %s\n", buffer);
sent = 0;
do {
result1 = send(j, buffer+sent, result-sent, MSG_NOSIGNAL);
if (result1 > 0)
sent += result1;
else if (result1 < 0 && errno != EINTR);
break;
} while (result > sent);
}
else if (result == 0) {
close(j);
FD_CLR(j, &readset);
}
else {
printf("Error in recv(): %s\n", strerror(errno));
}
} // end if (FD_ISSET(j, &tempset))
} // end for (j=0;...)
} // end else if (result > 0)
} while (1);
Explanation:
Well, this one was more sofisticated, don't you think?
The code first sets an fd_set with the srvsock descriptor, and sets a variable (maxfd) to hold the value of that descriptor. You must recall that srvsock is a socket that has been explicitely bound to some port, then listen() has been called for this socket, allowing it to receive new connections.
The loop just calls select() with a copy of this set and with a timeout value. Once the select() call returns readability for any of the sockets in the set (that initially is only one), a check is made to verify if the srvsock descriptor is the one that selected readable. If it was, then it means that a new connection arrived. So I just call accept() on this socket, hoping that a new file descriptor will be returned, or an error. This part can be troublesome as the connection may not be available after select() and before accept() which would make the server hang in this call. The non-blocking mode for srvsock would be of help.
Once the new descriptor is returned by the accept() call, it is added to the readset struct using FD_SET(), and maxfd is modified accordingly. Last, but not least, the srvsock descriptor is cleared from the tempset struct.
After the check, a for() loop iterates from 0 to maxfd (inclusive), and a check is made against the tempset with each value in the range. If one of them is set, then it means that specific socket has selected readable, and recv() will be called on it. This is the importance of clearing srvsock from the tempset, I don't want to call recv() on srvsock!
When recv() returns some data, the data is echoed back through the same socket, thus giving the functionality desired.
If recv() returns 0, which means the socket was closed, then that socket is cleared from readset and the operation continues.
In the next iteration, the contents of readset are copied to tempset, thus keeping track of every active connection in readset.
I hope you like it... It's still immature, and needs (a lot) of work to be bullet-proof.
Q&A:
I don't see any array of socket descriptors. How do you keep track of new connections?
Well, that part is not so easy to see for the beginners, I thought the same thing some time ago, until I realized that:
1) Sockets descriptors are just "int"s.
2) The fd_set is some kind of bitset that can be accessed through the operations (Macros): FD_SET(), FD_CLR(), FD_ISSET(), FD_ZERO(), etc...
So, you only have to use the bitset functionality to keep track of your socket descriptors using a single variable. It is easier to keep track of them this way, as you will also need an fd_set to pass to select(), so, copying from one struct to the other is just a function call.
When should I use select(), dedicated processes per connection, or dedicated threads per connection?
Well, that is not an easy question... I think that discussing that subject in this thread is not healthy (it can get quite large), however, I will try to explain when select() is a good option:
1) You have many, many connections that don't need much processing time. The time elapsed from the point of recv() until send() is not too much, so you can give the "clients" a good response time.
2) The server acts as a central point, distributing messages or information from one (or more) of its connections to other(s) connection(s). For example, a chat server would be easier to code using select() than using threads, and much easier than using dedicated servers.
3) The processing time needed to give a response to the client is long for each message (contrary to the first point), but you can delegate that task to another process using sockets or pipes or any other IPC mechanism than can be select'ed, so the processing of each message would take little time.
In fact, you could always use select(), unless you have special constraints that would make its use "unconfortable", or that would make the other solutions easier to code, debug and understand.
<<<< I'M HERE... waitting for comments, flames, etc...
This żarticle? concentrates in explaining the usage of the "select()" function call in a pratical way. Giving some examples of its use with easy to understand code, and explanations that could lead the reader to understand its use, advantages, disadvantages and even debug/modify existing code...
The first two examples are very easy and try to show the use of select with just one descriptor. The objective is that the reader gets a little knowledge about the system call and some of its parameters.
First of all, you must have an open file descriptor.
As this is the Networking section of the forum, I assume we have a SOCK_STREAM socket that is already connected.
FIRST EXAMPLE:
The first example waits indefinitely until there is data ready to be read on the socket.
int socket_fd, result;
fd_set readset;
...
/* Socket has been created and connected to the other party */
...
/* Call select() */
do {
FD_ZERO(&readset);
FD_SET(socket_fd, &readset);
result = select(socket_fd + 1, &readset, NULL, NULL, NULL);
} while (result == -1 && errno == EINTR);
if (result > 0) {
if (FD_ISSET(socket_fd, &readset)) {
/* The socket_fd has data available to be read */
result = recv(socket_fd, some_buffer, some_length, 0);
if (result == 0) {
/* This means the other side closed the socket */
close(socket_fd);
}
else {
/* I leave this part to your own implementation */
}
}
}
else if (result < 0) {
/* An error ocurred, just print it to stdout */
printf("Error on select(): %s\", strerror(errno));
}
Now the explanation:
First, I fill up an fd_set struct with the information about the sockets that I want to "monitor". In this case, I first unset all bits in the set, then I set the bit corresponding to the socket_fd file descriptor. This is done using the handy macros FD_ZERO and FD_SET
Then I use the select() function to monitor the availability of data on the socket.
The first parameter to select() is the maximum file descriptor that is set in the structs PLUS ONE. That is, if you have 20 file descriptor in the sets, and the maximum value a file descriptor has is 123, then the value passed as the first parameter must be 124.
The subsequent lines just check the select()'s return code, and do what is expected for each situation.
select() can return one of -1, 0, > 0
-1: Means an error was encountered, you should do something about it. I just print the error
0: Means the call timed out without any event ready for the sockets monitored
> 0: Is the number of sockets that have events pending (readability, writability, exception-ability ) :wink:
Q&A:
But you wrote a do { ... } while() loop around the select() call, why did you do that?
The while() loop is used to keep select() restarting each time an EINTR error is thrown. The select() man page reads "... the sets and timeout become undefined, so do not rely on their contents after an error. " That is why I put the initialization and setting of the file descriptor inside the do ... while() loop
Why do you use a do ... while() loop?
That is my coding style, you can as well use whatever you like...
Why don't you just use another fd_set and copy it to the one used in select()?
Don't be impatient, I'll get there soon...
But that is the same as doing a recv() inside a loop... Why bother with so much code?
It's for demonstration purposes only... We'll mess the whole thing up soon.
Why didn't you check for the 0 result?
Well, we do not have a timeout value, do we?
Let's move on to a messier example...
SECOND EXAMPLE:
In this example I take the previous code a little bit farther and write something that may be useful: A recv() with timeout.
As it is meant to be useful (someone may actually find a use for this), I will wrap it inside a function
/*
Params:
fd - (int) socket file descriptor
buffer - (char*) buffer to hold data
len - (int) maximum number of bytes to recv()
flags - (int) flags (as the fourth param to recv() )
to - (int) timeout in milliseconds
Results:
int - The same as recv, but -2 == TIMEOUT
Notes:
You can only use it on file descriptors that are sockets!
'to' must be different to 0
'buffer' must not be NULL and must point to enough memory to hold at least 'len' bytes
I WILL mix the C and C++ commenting styles...
*/
int recv_to(int fd, char *buffer, int len, int flags, int to) {
fd_set readset;
int result, iof = -1;
struct timeval tv;
// Initialize the set
FD_ZERO(&readset);
FD_SET(fd, &readset);
// Initialize time out struct
tv.tv_sec = 0;
tv.tv_usec = to * 1000;
// select()
result = select(fd+1, &tempset, NULL, NULL, &tv);
// Check status
if (result < 0)
return -1;
else if (result > 0 && FD_ISSET(fd, &tempset)) {
// Set non-blocking mode
if ((iof = fcntl(fd, F_GETFL, 0)) != -1)
fcntl(fd, F_SETFL, iof | O_NONBLOCK);
// receive
result = recv(fd, buffer, len, flags);
// set as before
if (iof != -1)
fcntl(fd, F_SETFL, iof);
return result;
}
return -2;
}
The function is meant to be a simple replacement for recv(), it just takes an additional parameter. This means that the function:
- can be interrupted by a signal
- can return -1, 0 or >0
- can read less data than asked for
Additionally, the function can return -2, meaning the operation timed out. I thought about using -1 and setting errno to ETIMEDOUT, but that is a hard error code, and the time out of select() is just an expected condition.
The function creates an fd_set that later initializes and fills in with the socket descriptor. Then it puts the timeout value in the timeval struct.
And then selects() on the set for readability.
What a good explanation! It was so clear!
The function may fail here for two reasons, which I just return to the caller process: -1= some error, even EINTR; and 0 timeout, in this case I return -2 (remember that recv() == 0 when the socket has been closed)
If there is data ready to be recv'd, then I proceed to recv' it. Before doing so, I set the socket to non-blocking mode, and after recv'ing I set it to whatever flags it had before. This means that the function could return -1 with an errno value of EAGAIN/EWOULDBLOCK.
Thank you Rob for pointing this out...
Q&A:
Why do you switch the socket to non-blocking mode before reading?
Well, as Rob noted, the recv() could block even if the socket selected readable. In multithreaded programs its usual that more than one thread recv() using the same descriptor. I just wash my hands clean.
Why don't you wrap the select() and recv() calls inside a do ... while()loop like you did before?
Well, I thought about it (and even wrote part of it), but the idea is to get a similar behaviour to that of recv(). The only thing that is a bit tricky is that you can get a EWOULDBLOCK error even if you are using non-blocking sockets.
I don't see this code useful. It is very similar to the man pages - it doesn't help much
I tried to keep it easy to understand. The rest of this post will be more complicated, and finally you'll get to read something useful.
A NOT SO SIMPLE WAY TO USE SELECT - BUT MORE USEFUL
In this part, I'll try to show how to use select() to check for readability on more than one file descriptor. NOTE: This works for any kind of file descriptor.
THIRD EXAMPLE:
I assume that I have an array of integers (the array with file descriptors), and that I am going to check for readability on all of them at the same time.
int myfds[N], maxfd, j;
fd_set readset;
...
// Initialize the set
FD_ZERO(&readset);
maxfd = 0;
for (j=0; j<N; j++) {
FD_SET(myfds[j], &readset);
maxfd = (maxfd>myfds[j])?maxfd:myfds[j];
}
// Now, check for readability
result = select(maxfd+1, &readset, NULL, NULL, NULL);
if (result == -1) {
// Some error...
}
else {
for (j=0; j<N; j++) {
if (FD_ISSET(myfds[j], &readset)) {
// myfds[j] is readable
}
}
}
Wasn't that bad, eh?
Well, in fact this is almost the core of every process using select().
There is not much to say about this code, except that more than one socket may select readable at the same time, and that is why I use a loop after checking "result > 0"...
Q&A:
What is that (maxfd>myfds[j])... stuff all about?
It's just a way to save the greatest file descriptor value. The first parameter to select() requires so.
It doesn't help me much. I need to implement an echo server for "Networking essentials 101", and you are not being of much help
Yes, I know. But if you really want to learn (unless you are me, Albert Einstein or that other guy I can't recall) you need to begin with easy things and move to the difficult ones when you feel confortable.
And now... for those of you that don't want to wait any longer...
FOURTH EXAMPLE:
In this (hopefully) last example, I will show part of the code of an echo server. It just waits for new connections (on some TCP port) and each new connection is kept in a fd_set. When data is available in one or more of the sockets, it just recv's it and echoes back through the same socket.
I will NOT use non-blocking sockets.
I will only check for the basic errors and will try to keep the code as simple as possible.
So, here I go:
fd_set readset, tempset;
int maxfd, flags;
int srvsock, peersoc, j, result, result1, sent, len;
timeval tv;
char buffer[MAX_BUFFER_SIZE+1];
sockaddr_in addr;
/* Here should go the code to create the server socket bind it to a port and call listen
srvsock = socket(...);
bind(srvsock ...);
listen(srvsock ...);
*/
FD_ZERO(&readset);
FD_SET(srvsock, &readset);
maxfd = srvsock;
do {
memcpy(&tempset, &readset, sizeof(tempset));
tv.tv_sec = 30;
tv.tv_usec = 0;
result = select(maxfd + 1, &tempset, NULL, NULL, &tv);
if (result == 0) {
printf("select() timed out!\n");
}
else if (result < 0 && errno != EINTR) {
printf("Error in select(): %s\n", strerror(errno));
}
else if (result > 0) {
if (FD_ISSET(srvsock, &tempset)) {
len = sizeof(addr);
peersock = accept(srvsock, &addr, &len);
if (peersock < 0) {
printf("Error in accept(): %s\n", strerror(errno));
}
else {
FD_SET(peersock, &readset);
maxfd = (maxfd < peersock)?peersock:maxfd;
}
FD_CLR(srvsock, &tempsock);
}
for (j=0; j<maxfd+1; j++) {
if (FD_ISSET(j, &tempset)) {
do {
result = recv(j, buffer, MAX_BUFFER_SIZE, 0);
} while (result == -1 && errno == EINTR);
if (result > 0) {
buffer[result] = 0;
printf("Echoing: %s\n", buffer);
sent = 0;
do {
result1 = send(j, buffer+sent, result-sent, MSG_NOSIGNAL);
if (result1 > 0)
sent += result1;
else if (result1 < 0 && errno != EINTR);
break;
} while (result > sent);
}
else if (result == 0) {
close(j);
FD_CLR(j, &readset);
}
else {
printf("Error in recv(): %s\n", strerror(errno));
}
} // end if (FD_ISSET(j, &tempset))
} // end for (j=0;...)
} // end else if (result > 0)
} while (1);
Explanation:
Well, this one was more sofisticated, don't you think?
The code first sets an fd_set with the srvsock descriptor, and sets a variable (maxfd) to hold the value of that descriptor. You must recall that srvsock is a socket that has been explicitely bound to some port, then listen() has been called for this socket, allowing it to receive new connections.
The loop just calls select() with a copy of this set and with a timeout value. Once the select() call returns readability for any of the sockets in the set (that initially is only one), a check is made to verify if the srvsock descriptor is the one that selected readable. If it was, then it means that a new connection arrived. So I just call accept() on this socket, hoping that a new file descriptor will be returned, or an error. This part can be troublesome as the connection may not be available after select() and before accept() which would make the server hang in this call. The non-blocking mode for srvsock would be of help.
Once the new descriptor is returned by the accept() call, it is added to the readset struct using FD_SET(), and maxfd is modified accordingly. Last, but not least, the srvsock descriptor is cleared from the tempset struct.
After the check, a for() loop iterates from 0 to maxfd (inclusive), and a check is made against the tempset with each value in the range. If one of them is set, then it means that specific socket has selected readable, and recv() will be called on it. This is the importance of clearing srvsock from the tempset, I don't want to call recv() on srvsock!
When recv() returns some data, the data is echoed back through the same socket, thus giving the functionality desired.
If recv() returns 0, which means the socket was closed, then that socket is cleared from readset and the operation continues.
In the next iteration, the contents of readset are copied to tempset, thus keeping track of every active connection in readset.
I hope you like it... It's still immature, and needs (a lot) of work to be bullet-proof.
Q&A:
I don't see any array of socket descriptors. How do you keep track of new connections?
Well, that part is not so easy to see for the beginners, I thought the same thing some time ago, until I realized that:
1) Sockets descriptors are just "int"s.
2) The fd_set is some kind of bitset that can be accessed through the operations (Macros): FD_SET(), FD_CLR(), FD_ISSET(), FD_ZERO(), etc...
So, you only have to use the bitset functionality to keep track of your socket descriptors using a single variable. It is easier to keep track of them this way, as you will also need an fd_set to pass to select(), so, copying from one struct to the other is just a function call.
When should I use select(), dedicated processes per connection, or dedicated threads per connection?
Well, that is not an easy question... I think that discussing that subject in this thread is not healthy (it can get quite large), however, I will try to explain when select() is a good option:
1) You have many, many connections that don't need much processing time. The time elapsed from the point of recv() until send() is not too much, so you can give the "clients" a good response time.
2) The server acts as a central point, distributing messages or information from one (or more) of its connections to other(s) connection(s). For example, a chat server would be easier to code using select() than using threads, and much easier than using dedicated servers.
3) The processing time needed to give a response to the client is long for each message (contrary to the first point), but you can delegate that task to another process using sockets or pipes or any other IPC mechanism than can be select'ed, so the processing of each message would take little time.
In fact, you could always use select(), unless you have special constraints that would make its use "unconfortable", or that would make the other solutions easier to code, debug and understand.
<<<< I'M HERE... waitting for comments, flames, etc...