In order not to be blocked in a write function on a socket (function write()) or in a read function on a socket (function read() or function accept()) while another input/output operation could be done, a multiple wait function has therefore been integrated into the "socket library."
This function takes 5 parameters :
The function modifies the 3 sets received and it returns an integer that indicates the number of sockets ready if it is positive or zero and an error code when it is negative.
Note: Under Linux, the timeout structure is modified, at the output of the function, in order to indicate the remaining time, but it is advisable, for the sake of compatibility with other operating systems, to consider that the content of the structure is then indeterminate and must therefore be reinitialised if one wishes to use it again.
The constants corresponding to the errors are:
In order to use this function and the associated macros, you must include sys/select.h.
To handle sets of file descriptor numbers, several macros have been defined in the "socket library" allowing to initialize, set, and test each descriptor of a set.
This macro is a procedure that sets a set of file descriptor numbers to zero. It takes one parameter:
Here is an example of C code, where we declare a set of file descriptor numbers and initialize it to 0. Thus the flags of each file descriptor number are all deactivated.
#include <sys/select.h> fd_set fds; FD_ZERO(&fds);
This macro is a procedure taking two parameters:
Here is an example of C code that creates and initializes a set of file descriptor numbers, where only the flags corresponding to socket1 and socket2 are active.
#include <sys/select.h> int socket1,socket2; fd_set fds; FD_ZERO(&fds); FD_SET(socket1, &fds); FD_SET(socket2, &fds);
This macro is a function that takes two parameters:
The function returns False (0) if the descriptor is not part of the set and True (1) otherwise.
#include <sys/select.h> int n, socket1,socket2; fd_set fds_read; ... FD_ZERO(&fds_read); FD_SET(socket1, &fds_read); FD_SET(socket2, &fds_read); n=select(FD_SETSIZE,&fds_read,NULL,NULL,NULL); if (n>=0) { if (FD_ISSET(socket1, &fds_read)) { read(socket1,&buf,sizeof(buf)); ... } if (FD_ISSET(socket2, &fds_read)) { read(socket2,&buf,sizeof(buf)); ... } }
This macro is a system constant that gives the maximum number of file descriptor that the system is able to handle. It is also the maximum size of a set of file descriptor numbers.
This macro, which is used to remove a file descriptor from the relevant set, is a procedure taking two parameters:
This first example program only handles reads via the select function, assuming (which may be wrong) that writes will not be blocking because the messages are short and few in number and will therefore be able to fit in the output buffers of the network stack.
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <netdb.h> #include <sys/socket.h> #include <sys/select.h> #include <string.h> // for the function bzero #include <arpa/inet.h> // for the function inet_addr int state[FD_SETSIZE]; void next(int fd, char *buf) { switch(state[fd]) { case 0: write(fd,"Hello\r\n",7); state[fd]=1; break; case 1: strtok(buf,"\r\n"); if (strcmp(buf,"Bye")==0) state[fd]=0; else write(fd,"I do not undestand\r\n",20); break; } } void srv(int fd_srv) { fd_set fdrs; FD_ZERO(&fdrs); FD_SET(fd_srv,&fdrs); while(1) { fd_set fds_read; int n,i,fd_con; struct sockaddr_in adr_cli; socklen_t adr_cli_len; char buf[1024]; memcpy(&fds_read,&fdrs,sizeof(fd_set)); n=select(FD_SETSIZE,&fds_read,NULL,NULL,NULL); if (n > 0) for (i = 0 ; i < FD_SETSIZE ; i++ ) if (FD_ISSET(i,&fds_read)) if (i==fd_srv) { adr_cli_len = sizeof(adr_cli); fd_con = accept(fd_srv, (struct sockaddr *) &adr_cli, &adr_cli_len); if (fd_con >= 0) { FD_SET(fd_con,&fdrs); printf("Connection accepted!\n"); state[fd_con]=0; suite(fd_con,""); } } else { n=read(i,&buf,sizeof(buf)-1); buf[n]=0x00; suite(i,buf); if (state[i]==0) { FD_CLR(i,&fdrs); close(i); printf("Connection closed!\n"); } } } } int main() { int fd_srv; struct sockaddr_in adr_srv; int err; fd_srv = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd_srv < 0) { printf("Socket creation error!\n"); exit(1); } bzero(&adr_srv, sizeof(adr_srv)); adr_srv.sin_family = AF_INET; err = inet_aton("0.0.0.0", &adr_srv.sin_addr); if (err == 0) { printf("Invalid IPv4 address!\n"); exit(1); } adr_srv.sin_port = htons(8080); err = bind(fd_srv, (struct sockaddr *) &adr_srv, sizeof(adr_srv)); if (err != 0) { printf("Server port access error!\n"); exit(1); } err = listen(fd_srv, 5); if (err != 0) { printf("Queue creation error!\n"); exit(1); } printf("Server started!\n"); srv(fd_srv); return 0; }
For this example, a PLC has been programmed using the procedure "next()" qwhich is called after each reading. The procedure operates, depending on the current status stored via the table "state" for each connection established, then it determines the next state.
In this state machine, state number 0 corresponds to the initial state and the return to state number 0 will end the connection. State 1 corresponds to the reading and subsequent processing.
As soon as one has to build a more complex program using the "select()" function to dispatch tasks, it is advisable to build a state diagram in which, at each change of state, the expectation of an available read or a possible write is associated.
To manage the read and write waitings, we will use two fd_set: fdrs and fdws declared as global variables and we will manage the various states of the automaton in the form of independent procedures. We will then store the state of each connection via a pointer to the corresponding procedure.
fd_set fdrs,fdws; void (*state[FD_SETSIZE])(int);
In each procedure managing a state, we propose to use the following procedures, defined as follows:
void will_read(int fd, void (*st)(int)) { FD_SET(fd,&fdrs); FD_CLR(fd,&fdws); state[fd]=st; }
void will_write(int fd, void (*st)(int)) { FD_SET(fd,&fdws); FD_CLR(fd,&fdrs); state[fd]=st; }
void will_close(int fd, void (*st)(int)) { FD_CLR(fd,&fdrs); FD_CLR(fd,&fdws); state[fd]=NULL; st(fd); close(fd); }
Each of these procedures is called with the parameters :
We modify the procedure srv() so that it monitors both the availability of a read and the availability of a write based on what each connection requests and then calls the procedure that was stored in the function pointer array. The code of the procedure leaves it to each procedure, managing a state of the automaton, to start with the expected read or write. The initial state of the automaton is managed by the procedure named state0.
void srv(int fd_srv) { FD_ZERO(&fdrs); FD_SET(fd_srv,&fdrs); FD_ZERO(&fdws); while(1) { fd_set fds_read,fds_write; int n,i,fd_con; struct sockaddr_in adr_cli; socklen_t adr_cli_len; memcpy(&fds_read,&fdrs,sizeof(fd_set)); memcpy(&fds_write,&fdws,sizeof(fd_set)); n=select(FD_SETSIZE,&fds_read,&fds_write,NULL,NULL); if (n > 0) for (i = 0 ; i < FD_SETSIZE ; i++ ) { if (FD_ISSET(i,&fds_read)) if (i==fd_srv) { adr_cli_len = sizeof(adr_cli); fd_con = accept(fd_srv, (struct sockaddr *) &adr_cli, &adr_cli_len); if (fd_con >= 0) state0(fd_con); } else state[i](i); if (FD_ISSET(i,&fds_write)) state[i](i); } } }
The state machine that will manage both the different writes and reads corresponding to the example is the following:
Instead of the procedure named suite(), there are the procedures of each state, of the state machine, named respectively state0(), state1(), state2() and state3():
void state0(int); void state1(int); void state2(int); void state3(int); void state0(int fd) { printf("Connection accepted!\n"); will_write(fd,&state1); } void state1(int fd) { write(fd,"Hello !\r\n",9); will_read(fd,&state2); } void state2(int fd) { char buf[1024]; int n; n=read(fd,&buf,sizeof(buf)-1); buf[n]=0x00; strtok(buf,"\r\n"); if (strcmp(buf,"Bye")==0) will_close(fd); else will_write(fd,&state3); } void state3(int fd) { write(fd,"I do not undestand\r\n",20); will_read(fd,&state2); }