Applying Lab4 concepts to a simple Echo Protocol
In this document, we are going to come up with a simple Echo Protocol, that is here purely for instructional purposes. You do not have to follow any of the steps of the Echo Protocol in your Jukebox Protocol!
This Echo Protocol design document serves as a minimal working example of using select() for a custom application layer protocol. Your high-level takeaways from
this document are:
-
Understanding how to design a custom application layer protocol
-
Thinking through, very clearly, the steps associated with the server state, as the client makes different requests, and keeping track of this state on the server side.
-
Pseudo-code to understand how
fd_setworks for read and write socket descriptors
Echo Protocol Design
The Simple Echo Protocol is designed for a server to greet an incoming client and echo back the string that the client sends the server. Here are the steps that the Server needs to keep track of in order to serve the greeting to a client:
-
Client connects
-
Server sends a greeting (again, this is an example, your Jukebox Protocol does not have to do this)
-
Client sends a string to the Server
-
Server echoes the string back.
Here is a diagram of this:
Server States
The server has four states: Wait, Echo, Greet and Disconnected. The server spends most of its time in the Wait State when the client sends no data.
-
State Transition: Wait - Echo – Wait
-
When the server is in the waiting state, it can only transition to Echo or Disconnected.
-
When the client sends a string, the server transitions to the Echo State. The server then echos the string and transitions back to the Wait State.
-
-
State Transition: Wait/Greet/Echo - Disconnected
-
When the client issues a disconnect message, the server transitions to Disconnected State for this client from any of the states it currently is in.
-
The client can disconnect at any time, and therefore can transition to the disconnected state from any state.
-
-
State Transition: Disconnected – Greet
-
The server can only transition from the Disconnected to Greet State. When a client connects for the first time, the Server transitions from disconnected to connected, and sends a greeting to the Client.
-
-
Transition: Greet-Wait
-
Finally, from the Greet state the server can transition to waiting or disconnecting.
-
Once the server sends a greeting to the client, the server transitions into wait state.
-
Pseudo-code and Text on using select() to implement the Simple Echo Protocol:
First, let’s declare a struct that can store all the client states
that the server needs to keep track off.
typedef struct {
int connected; // 0 - Not connected. 1 - Connected.
int greeted; // 0 - Not greeted. 1 - Greeted.
int needs_echo; // 0 - No. 1 - Yes.
char data[1024]; //data array to store the string the client sends
int offset; //offset within the data array to keep track of how much data was sent
} client;
In main(), we can now declare a list of struct s for the list of clients up to some maximum number of clients, say MAX_CLIENTS
in main:
client clients[MAX_CLIENTS];
memset(clients, 0, MAX_CLIENTS * sizeof(client));
server_sock = bind(), listen();
Select loop - Phase 1: Initializing Read and Write Socket Descriptors
Always remember to add the server socket!
//declare a set of read and write file or socket descriptors
fd_set rfds, wfds;
int i;
int maxfd = server_sock;
// add your server socket to the list of read file descriptors
FD_SET(server_sock, &rfds);
//loop through all the clients and add them to the appropriate set.
for (i = 0; i < MAX_CLIENTS; i++) {
//once clients are connected and greeted, we need to start
//receiving data. I.e., we need to put the clients in the read file
//descriptor list.
if (clients[i].connected && clients[i].greeted) {
FD_SET(i, &rfds);
if (i > maxfd) maxfd = i;
}
//once clients are connected, greeted, and we have received data,
//we now need to send the data back! I.e., we need to put the
//clients in the write file descriptor list.
if (clients[i].connected && (!clients[i].greeted || clients[i].needs_echo)) {
FD_SET(i, &wfds);
if (i > maxfd) maxfd = i;
}
}
maxfd += 1;
Select loop - Phase 2: Call select()
Now that we have initialized all our sockets, into the read and write file descriptor sets, we can run select().
// call select.
int result = select(maxfd, &rfds, &wfds, NULL, NULL);
The call above to select() will filter the read and write file/socket descriptors we put in the set, and only return those that are actually ready to read (the O.S. recv buffer has data for the application) or to write (the O.S. send buffer has space for data to be
sent by the application).
In the case that no client is available, the call to select will block, and will only return if there is at least one client in the read or write set.
Select loop - Phase 3: Check the return values from select()
Once the call to select returns, for each socket in the list, if it is still in the set after select returns, then, it is safe to read/write ONE TIME.
for (i = 0; i < maxfd; i++) {
if (FD_ISSET(i, &rfds)) {
if (i == server_socket)
accept_new_client(i);
else
read_client(i);
}
if (FD_ISSET(i, &wfds)) {
if (!clients[i].greeted)
send_greeting(i);
else
send_data(i);
}
}