Lab 13 - Sockets
From now on, we’ll assume that you start lab by connecting to the CS portal and that you are familiar with the command line environment. If you have not been practicing with the terminal, we strongly encourage reviewing Lab 1.
We will also assume that you ran the setup script from that lab and have all modules (including clang and git) by default.
In this lab, we’ll be experimenting with sockets. Specifically, you’ll be given a server program and asked to write a client program that connects to the server using sockets. The client will then read data from the server and display it to the user before exiting.
Lab Goals
After this lab, you should:
- Gain increasing familiarity with C
- Understand the basics of sockets and communication through sockets in C
- Use
read
andwrite
system calls
Necessary background
fcntl.h
and unistd.h
provide functional wrappers around the internal operating-system abstraction of a file-like object: things the OS lets you read to and write from. Conceptually the operating system maintains, for each running process, an array of these objects. User-level code can interact with them by passing in indexes into this array, called “file descriptors” to these system calls.
In addition to actual files and the terminal, file descriptors can also be used to represent other communications channels. Among those are a set of related concepts collectively called “sockets.”
Creating a socket creates an object on the OS’s file-like-objects array, but does not finish hooking it up. For the TCP/IP sockets this lab will use, we’ll need several other steps to do this. In the end, we’ll have two programs running at once, possibly on different computers, each with a socket connected by a virtual two-way communication channel.
Each connected socket has exactly two ends: the local end (client) you use in your code, and the remote end (server) somewhere else.
|--------| |--------|
| Client | --------->| Server |
|--------| |--------|
A basic TCP/IP socket application uses three socket pairs:
- A server listening socket that connects a computer to the Internet and waits around for other computers to contact it
- The remote end of this socket is held by the OS, which sends “I got a new connection attempt” messages to your code through it
- Your server must be running before your client can connect to it
- A client socket that contacts the server listening socket
- The remote end of this socket is the server communication socket
- A server communication socket that the OS creates and sends through the server listening socket to your code
- The remote end of this socket is the client socket
In typical “use words loosely” fashion, the client socket, server communication socket, and the connection between them is together often called simply a “socket”.
Address and Port
A socket connection requires an address which identifies which computer to contact and a port which helps the OS know which process the connection should be sent to.
Port
Ports are partially specified by IANA. We’ll use a random port from the ephemeral port region.
The server will pick one using a random number generator seeded with the OS’s number of the currently running process, which should minimize the risk of us picking one that another process is already using and means if we do we can re-run the program to get a new one:
srandom(getpid()); // random seed based on this process's OS-assigned ID
int port = 0xc000 | (random()&0x3fff); // random element of 49152–65535
The client will need to know the port that the server it wants to contact is listening on, so we’ll have the port be a command-line parameter in the client. The Server program we provide prints both the IP and Port, when it runs you’ll want to write these down.
Address
TCP/IP addresses tell us what computer and program we’re talking to. They are somewhat involved to explain (we’ll go into more detail on these in CSO2), but they are stored in a struct sockaddr_in
declared in <netinet/in.h>
. For our uses, we’ll need to (a) create one of these, (b) zero it out, and then (c) set three fields:
-
ipaddr.sin_family = AF_INET;
says we are using an IPv4 address, still the most widely-supported address family though it is starting to be replaced by IPv6. -
ipOfServer.sin_port = htons(port);
puts the [Port] number into the address structure. Thehtons
is an endian-changing function; because computers of both endiannesses can attach to the Internet, network communications are handled “network byte order” (i.e., big endian), requiring conversion functions likehtons
andhtonl
. -
ipOfServer.sin_addr.s_addr
needs to behtonl(INADDR_ANY)
for the server to say it is listening for communication from any other computer; for the client it instead needs to beinet_addr(ip_address_of_server);
whereip_address_of_server
will be a string containing four numbers separated by periods, like"128.143.67.241"
.-
You can learn the IP address of a URL by using the
host
command line tool.$ host portal01.cs.virginia.edu portal01.cs.virginia.edu has address 128.143.69.111
There many be several other addresses listed; you want the one with four integers separated by periods.
-
Important functions
The following are the main socket functions you need, in the order you’ll need to use them:
Server | Client |
---|---|
|
|
| |
| |
|
|
|
|
|
|
either |
Reading and Writing
We will be talking more about the read
and write
system calls in class tomorrow, including looking at how they work. For lab, we’ll be using them to read and write to the socket that we’ve opened.
read
andwrite
are system calls, meaning that they call a special function that is provided by the kernel. Therefore, they are in section 2 of the manual. To read more about each of these functions, run:man 2 read
or
man 2 write
Each of these functions take:
- The file descriptor we want to read from or write to
- This is the index into our process’ array of file-like-objects:
- standard in = 0
- standard out = 1
- standard err = 2
- first file/socket = 3
- …
- This is the index into our process’ array of file-like-objects:
- A pointer to a buffer (of bytes) to write (or read into)
- This is an array of bytes (
void *
pointer) but we can pass a character array so that it will write those bytes
- This is an array of bytes (
- The number of bytes to write
- This is on the byte level, not in the Standard Buffered Input/Output library, so we must give it the number of bytes to write/read
For example, consider the following C code:
const char *txt = "Hello world."; write(1, txt, 13);
This code writes 13 bytes in the buffer pointed to by
txt
to standard output. If we include this in amain
function, we’ll see “Hello world.” printed to the screen. Note: the 13th character is the null byte,\0
.
Getting Started
Below is the complete code of a server program that sends the same message to every client that connects:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
const char *msg = "Congratulations, you've successfully received a message from the server!\n";
int main() {
// start by getting a random port from the ephemeral port range
srandom(getpid()); // random seed based on this process's OS-assigned ID
int port = 0xc000 | (random()&0x3fff); // random element of 49152–65535
// create an address structure: IPv4 protocol, any IP address, on given port
// note: htonl and htons are endian converters, essential for Internet communication
struct sockaddr_in ipOfServer;
memset(&ipOfServer, 0, sizeof(struct sockaddr_in));
ipOfServer.sin_family = AF_INET;
ipOfServer.sin_addr.s_addr = htonl(INADDR_ANY);
ipOfServer.sin_port = htons(port);
// we'll have one socket that waits for other sockets to connect to it
// those other sockets will be the ones we used to communicate
int listener = socket(AF_INET, SOCK_STREAM, 0);
// and we need to tell the OS that this socket will use the address created for it
bind(listener, (struct sockaddr*)&ipOfServer , sizeof(ipOfServer));
// wait for connections; if too many at once, suggest the OS queue up 20
listen(listener , 20);
system("host $(hostname)"); // display all this computer's IP addresses
printf("The server is now listening on port %d\n", port); // and listening port
for(;;) {
printf("Waiting for a connection\n");
// get a connection socket (this call will wait for one to connect)
int connection = accept(listener, (struct sockaddr*)NULL, NULL);
printf("Got a connection\n");
if (random()%2) { // half the time
write(connection, msg, 40); // send half a message
usleep(100000); // pause for 1/10 of a second
write(connection, msg+40, strlen(msg+40)); // send the other half
} else {
write(connection, msg, strlen(msg)); // send a full message
}
close(connection); // and disconnect
}
// unreachable code, but still have polite code as good practice
close(listener);
return 0;
}
Copy and paste this code into a new file server.c
(or copy the starter code directly from /p/cso1/labs/server.c
and /p/cso1/labs/client.c
). Compile and run your server.c
file. You should see the following output.
[id@portal04 ~]$ clang server.c -o server
[id@portal04 ~]$ ./server
portal04.cs.Virginia.EDU has address 128.143.69.114
The server is now listening on port 5048
Waiting for a connection
Great you server is now waiting for the client that you’ll write to connect to it. Don’t close this terminal, we want to keep server running. Open a new terminal and use this new terminal to develop your client.c
program
Your Task
For this lab, you should complete the client.c
code below to create a client program that:
- Accepts an IP address and port number on the command line. Your program must have the IP address as the first argument and the port as the second argument. Hint: the
atoi()
function instdlib.h
converts strings to integers - Connects to the server using that IP address and port.
read
s the message from the server and prints it to the command line.
Note: It will be important and helpful to read through man
-pages so that you understand what every line of the server code does.
Hint: Read the table above. It lists several of the functions that you need when developing your client.
Ideally your program should
- Verify that the connection worked, giving a reasonable error message if the IP and port combination failed to connect.
- Use proper
while
-loop structure toread
all the data sent, even if all the data was not sent at once (run the client repeatedly to test this). - Check the return values of every other function that returns an error status (see the return value section in each function’s manual page).
close
everything your programopen
s andfree
everything itmalloc
s.
Starter Code
Here is starter code for the client.c
file.
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h> //atoi
int main(int argc , char *argv[]){
/*
Initalize struct sockaddr_in to contain IP address and port number of server.
Remember to read values from argv. (You also need to convert it from
a string to an int, so the atoi function is very helpful)
*/
//Create Socket (Remember to check the return value to see if an error occured)
//Connect to Remote server
//Read message from server (Note: The server sends multiple messages, so you might need a loop)
//Close socket and free anything you malloced
}
Testing your code
You’ll need a server running at some known IP address and port. You’ll also need to run your client.
If you want to run your own server (alternatively, you can use a server another student is running if you wish), you’ll need two different programs. Specifically, you’ll need your program executables to have different names, not just the default a.out
. The -o
compiler flag helps with this; clang mycode.c -o my_prog
names the resulting binary my_prog
instead of a.out
, to be run as ./my_prog
.
You’ll need to leave the server running as long as you want to run your client. Do this by opening two terminal windows and running the client in one, the server in the other.
Make sure you kill the server program when you are done working with it (e.g., by pressing Ctrl+C in that terminal window). If too many people leave too many servers running, the portal back-end servers may eventually start running out of open ports and need to be restarted by the systems staff…
Check-off with a TA
Upload the following to gradescope:
- Your C code
client.c
To check-off this lab, show a TA:
- Once you submitted your files check in with a TA so that they can give you attendence credit.