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:

  1. Gain increasing familiarity with C
  2. Understand the basics of sockets and communication through sockets in C
  3. Use read and write 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. The htons 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 like htons and htonl.

  • ipOfServer.sin_addr.s_addr needs to be htonl(INADDR_ANY) for the server to say it is listening for communication from any other computer; for the client it instead needs to be inet_addr(ip_address_of_server); where ip_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:

ServerClient

socket(AF_INET, SOCK_STREAM, 0) creates an unbound TCP/IP socket and returns its file descriptor.

socket(AF_INET, SOCK_STREAM, 0) creates an unbound TCP/IP socket and returns its file descriptor.

bind(s, &ip , sizeof(ip)) asks the OS to reserve this port and address for socket s.

listen(s, 20) asks the OS to allow incoming connection attempts on socket s, hinting that we’d like to queue up as many as 20 attempts at once.

int c = accept(s, 0, 0) suspends your program until there is a connection attempt on s and then returns the server communication socket as c.

connect(s, &ip, sizeof(ip)) connects socket s to the server identified in ip.

read and write with c as needed to communicate with the client.

read and write with s as needed to communicate with the server.

close(c) to end communication.

close(s) to end communication.

either accept another connection or close(s) to stop listening.

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 and write 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:

  1. 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
  2. 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
  3. 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 a main 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:

  1. 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 in stdlib.h converts strings to integers
  2. Connects to the server using that IP address and port.
  3. reads 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 to read 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 program opens and free everything it mallocs.

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.

Copyright © 2023 Daniel Graham, John Hott and Luther Tychonievich.
Released under the CC-BY-NC-SA 4.0 license.
Creative Commons License