In today’s exercise you will practice using POSIX sockets to write programs that communicate over a network. Networking is a large and important area of computer science that we cannot possible cover entirely in this course, but you will have experience with one type of networking that plays a particularly important role in the internet: TCP/IP.
While POSIX sockets are not particularly complex or difficult to learn, the API is a bit unusual and cumbersome to use. Instead of using sockets directly, you will have the option of using a collection of simple wrappers around socket operations that is included with the starter code for today’s exercise.
To obtain a copy of the starter code, connect to MathLAN and run the following shell command:
$ git clone /home/curtsinger/csc213/exercises/networking ~/csc213/exercises/
At this point you can open your copy of the exercise starter code in whatever editor you prefer. The following sections run through what the code is doing; follow along in your editor, and pay attention to the comments.
The starter code includes C code for two different programs: a server and a client. You’ll need to run both of these programs, starting with the server. The server starts up and begins listening for connections. The client connects to the server, and the two programs exchange short messages over sockets.
Open the file client.c to see the main code for the client program.
This program takes two command-line parameters: the name of a machine to connect to, and a port number.
After dealing with command-line arguments, the client calls socket_connect.
This function is a wrapper around POSIX sockets that is defined in socket.h, which you can find in your provided starter code.
The function takes a string name of a server and a port number, then attempts to open a TCP connection to that server and port.
You should review the code and comments with this function in socket.h to see how the POSIX sockets interface actually works.
Today’s exercise will not require you to call any of the POSIX socket functions directly;
you can use the wrappers in socket.h instead.
After connecting to the server and checking for errors, the client waits to receive a message from the server.
The client does this using the receive_message function defined in message.c.
This function, which you should review, uses the read system call to read in two parts of a message;
the client first reads the length of the message itself, which the server will send before the message.
Then the client validates that length, allocates space to hold the entire message, and reads the message from the server socket.
Adding the length prefix makes it easy to deal with variable length messages when we have to use the read system call, but this is not the only possible design;
messages could be sent with a special delimiter to denote the end of the message, or we could send fixed-length messages.
As long as the client and server agree on the form of the message the system can work well.
After receiving a message from the server, the client prints it and frees the space returned from receive_message.
The client then sends a message to the server using send_message, which mirrors the behavior of receive_message.
This function first writes out the length of the string message, and then writes the message itself.
The server’s messaging code is similar to the client’s, although it sends a message and then waits for an incoming message—the reverse of the client’s messaging behavior. However, the server’s setup code is necessarily different.
First, the server creates a server socket using the server_socket_open function, defined in socket.h.
You should review the documentation for this function in socket.h before moving on.
Once a server socket is set up, we begin listening for connections on the socket by calling the listen function.
The second parameter to listen says how many incoming connections can be queued at once;
additional connections are just dropped immediately.
Once the server socket is listening, calling server_socket_accept will accept an incoming connection.
If there are no incoming connections yet, this call blocks the server until one arrives.
The returned value is a file descriptor for the socket we can use to talk to the connected client.
After this point you could accept another connection with the server socket and also talk to the client, but the example server just deals with one client at a time.
After establishing a connection, the server behaves much like the client. It exchanges messages with the client (in the reverse order), closes the socket file descriptor, and then exits.
Once you’ve reviewed the starter code, run make to build the server and client.
Start the server in one terminal window with this command:
$ ./server
Server listening on port 64529
You will almost certainly see a different port number in your case. Now that we know the port, start a client in another terminal window.
$ ./client localhost 64529
This tells the client to connect to the server named "localhost", which is a convenient name for the machine you’re running on.
Make sure you pass in the port number that was printed out by the server during startup.
You should see messages exchanged between the two programs, then they will exit.
Once you have verified that the server and client can communicate on the same machine, you should test a connection between machines.
Start another session with FastX that is (hopefully) running on a different machine, and try running the server on one machine, and the client on another.
You should use computer names;
if the server is running on a machine named turing, pass in turing.cs.grinnell.edu as the server address.
Once you have verified that you can connect across machines you should move on to the exercises for today.
Try to work through each exercise during class. You do not need to turn in your work for any of the exercises.
One of the first client–server programs most CS students write is an echo server.
This system simply reads input from the user in the client program (using fgets or getline) and sends it to the server.
The server then sends this exact message back.
The client waits for this response and displays it.
This process continues until the client sends the message quit, which tells the server to close the connection.
Here is an example run of the client talking to an echo server.
Messages sent by the server are displayed with Server: as a prefix:
$ ./client localhost 1234
Hello
Server: Hello
Testing
Server: Testing
This is a longer message, which should also work correctly.
Server: This is a longer message, which should also work correctly.
quit
Now that you have a program that sends messages from the client to the server and back, you can do something more interesting on the server end. Instead of sending back the original message, have the server send back a capitalized version of the message.
While you will need to change the server program for this exercise, the client program is unchanged. Here is a sample run of the client when talking to a capitalization server:
$ ./client localhost 5678
Hello
Server: HELLO
Hey, stop shouting.
Server: HEY, STOP SHOUTING.
How rude
Server: HOW RUDE
123456789
Server: 123456789
quit
Hint: You can convert a character to its capitalized version with the toupper function.
Most real servers deal with multiple clients at a time, but the server programs you have written so far only talk to one client. In this exercise, you will build on your capitalization server to interact with more than one client. As with the previous exercise, you will not need to change your client code.
The first step in supporting multiple clients is to handle them in sequence;
modify your capitalization server to run accept in a loop.
The server should accept a connection and interact with the one and only client.
When that client sends a quit message, the server closes the connection to the client.
Instead of exiting, the server should loop back and accept a new connection.
Verify that you are able to connect to your server, interact with it, quit, and then connect with another client.
You will need to press ctrl+C to stop your server when you are done testing.
This isn’t quite what we’re aiming for; our server handles multiple clients, but still only interacts with one client at a time. You can verify this by trying to connect to the server with a client while another client is still connected to the server. To handle multiple clients simultaneously, we need to introduce concurrency to your server program.
The easiest way to deal with multiple clients at the same time is to accept connections in a loop, but pass each new connection off to a new thread that interacts only with that client.
Your main function should still have a loop that calls server_socket_accept to connect with a new client.
However, instead of interacting with a client inside this loop before calling server_socket_accept again, you should create a thread and send it the file descriptor corresponding to the newly-connected client.
This thread will receive and send messages with the client, and should exit when the client sends a quit message.
The main thread will continue to accept connections until you press ctrl+C.
The concurrent echo server you implemented in the third exercise is more sophisticated than the single client version, but it still keeps clients separated from each other. We can take this system one step further by passing messages from one client to other clients in the system; any time the server receives a message from one client, it can send it on to all the other clients. Instead of just being an echo server, this is now a simple networked chat program. To do this you will need to add code to keep track of the socket file descriptors for each connected client. Each thread will read from one client’s socket, and when it receives a message it will send that to all clients’ sockets.
This will require some changes on the client end as well. The provided client code alternates between reading a message from the user to send to the server, and then waiting for a response from the server. That’s not going to work well if the client could receive multiple message from the server before sending anything. We can fix this by using threads in the client as well. One thread should read from the user and send the message to the server, and the other can read from the server and print every message it receives.