Lab 7 Solutions

These questions were written by Jerry Cain, Nick Troccoli, Chris Gregg, and Ryan Eberhardt.

Before the end of lab, be sure to fill out the lab checkoff sheet here!

Problem 1: Networking short-answer questions

Problem 2: Implementing a basic web server

In this problem, we’ll work through the process of implementing a fully-functional web server, just like the one running at web.stanford.edu that is hosting this website!

Before starting, go ahead and clone the lab7 folder:

$ git clone /usr/class/cs110/repos/lab7/shared lab7
$ cd lab7
$ make

To run the server, pick a random port number between 1025 and 65,535 and run ./web-server with the port number as the first argument:

./web-server 16382

The starter code parses the port number from argv. You’ll need to work through starting the server and handling incoming requests.

Part 1: Starting the server

Use createServerSocket to bind to the given port. If this function fails, print an error and return kServerStartFailure.

Here’s our solution:

int main(int argc, char *argv[]) {
    // ...
    int server = createServerSocket(port);
    if (server == -1) {
        cerr << "Failed to bind to port " << port << endl;
        return kServerStartFailure;
    }
    cout << "Server listening on port " << port << "." << endl;
}

Possible questions for discussion:

Part 2: Handling connections

Wait for a client to connect. Every time a client connects, print a message (e.g. “Client connected”).

Connect to the myth machine you’re running on using the Stanford VPN, just as you did in Assignment 5, or use an SSH proxy:

ssh -L 16382:localhost:16382 yourSunet@mythXX.stanford.edu
# ^ replace 16382 and mythXX with your myth machine and chosen port number

Then, start web-server, open your browser, and navigate to http://mythXX.stanford.edu:portNum/, replacing mythXX with the myth machine you’re running on, and replacing portNum with your chosen port number. Your browser may show an error or may not display anything, but you should see “Client connected” show up in your terminal.

Here’s our code. We have some extra code to get the IP address of the connecting client, but this isn’t necessary.

int main(int argc, char *argv[]) {
    if (argc > 2) {
        cerr << "Usage: " << argv[0] << " [<port>]" << endl;
        return kWrongArgumentCount;
    }
    unsigned short port = extractPort(argv[1]);
    if (port == USHRT_MAX) {
        cerr << "Invalid port number specified" << endl;
        return kIllegalPortArgument;
    }
    int server = createServerSocket(port);
    if (server == -1) {
        cerr << "Failed to bind to port " << port << endl;
        return kServerStartFailure;
    }
    cout << "Server listening on port " << port << "." << endl;

    while (true) {
        struct sockaddr_in address;
        // used to surface ip address from the client
        socklen_t size = sizeof(address);
        bzero(&address, size);
        int client = accept(server, (struct sockaddr *)&address, &size);
        char str[INET_ADDRSTRLEN];
        cout << "Received a connection request from "
            << inet_ntop(AF_INET, &address.sin_addr, str, INET_ADDRSTRLEN)
            << "." << endl;
    }
    return 0;
}

Possible questions for discussion:

Part 3: Reading the request

Using an iosockstream, read the full request from the client (including headers), and determine the path being requested by the client. It’s good practice to read the full request (including headers) even though you only need the request line, and failing to do so may cause problems in rare cases where the client sends a large amount of header data.

You can read a single token (where tokens are separated by whitespace) using an istream like so:

string token;
someIstream >> token;

To read a full line, you can use getline:

string line;
getline(someIstream, line);

Print out the requested path. If you navigate to http://mythXX.stanford.edu:portNum/samples/cs110/, you should receive the path /samples/cs110/.

Here’s our code:

int main(int argc, char *argv[]) {
    if (argc > 2) {
        cerr << "Usage: " << argv[0] << " [<port>]" << endl;
        return kWrongArgumentCount;
    }
    unsigned short port = extractPort(argv[1]);
    if (port == USHRT_MAX) {
        cerr << "Invalid port number specified" << endl;
        return kIllegalPortArgument;
    }
    int server = createServerSocket(port);
    if (server == -1) {
        cerr << "Failed to bind to port " << port << endl;
        return kServerStartFailure;
    }
    cout << "Server listening on port " << port << "." << endl;

    while (true) {
        struct sockaddr_in address;
        // used to surface ip address from the client
        socklen_t size = sizeof(address);
        bzero(&address, size);
        int client = accept(server, (struct sockaddr *)&address, &size);
        char str[INET_ADDRSTRLEN];
        cout << "Received a connection request from "
            << inet_ntop(AF_INET, &address.sin_addr, str, INET_ADDRSTRLEN)
            << "." << endl;
        serveFile(client);
    }
    return 0;
}

static void serveFile(int client) {
    sockbuf sb(client);
    iosockstream ss(&sb);

    string fileName = getFilename(ss);
    skipHeaders(ss);
}

static string getFilename(iosockstream& ss) {
    string method, path, protocol;
    ss >> method >> path >> protocol;
    string rest;
    getline(ss, rest);
    cout << "\tPath requested: " << path << endl;
    return path;
}

static void skipHeaders(iosockstream& ss) {
  string line;
  do {
    getline(ss, line);
  } while (!line.empty() && line != "\r");
}

Part 4: Loading the requested file

Use the loadPath function from the starter code to read the file requested by the client. Note that the path specified by the client will be in the form /samples/cs110, but you want to treat this as a relative path, with no leading / (e.g. loadPath("samples/cs110")). Here is some code that can handle this for you:

// given some variable `path`...
if (path == "/") {
    path = ".";
}
// strip off leading /
size_t slashPos = path.find("/");
path = slashPos == string::npos ? path : path.substr(slashPos + 1);

loadPath returns a pair including the file contents and a boolean indicating whether the file contents are HTML. (HTML is a language that is used to represent web pages; browsers know how to render HTML into a visual page that users can interact with.) For now, you can just print the contents and the boolean. If you navigate to http://mythXX.stanford.edu:portNum/samples/subdir/file1, your program should print file1 contents, and the HTML boolean should be false.

Here’s our code:

int main(int argc, char *argv[]) {
    if (argc > 2) {
        cerr << "Usage: " << argv[0] << " [<port>]" << endl;
        return kWrongArgumentCount;
    }
    unsigned short port = extractPort(argv[1]);
    if (port == USHRT_MAX) {
        cerr << "Invalid port number specified" << endl;
        return kIllegalPortArgument;
    }
    int server = createServerSocket(port);
    if (server == -1) {
        cerr << "Failed to bind to port " << port << endl;
        return kServerStartFailure;
    }
    cout << "Server listening on port " << port << "." << endl;

    while (true) {
        struct sockaddr_in address;
        // used to surface ip address from the client
        socklen_t size = sizeof(address);
        bzero(&address, size);
        int client = accept(server, (struct sockaddr *)&address, &size);
        char str[INET_ADDRSTRLEN];
        cout << "Received a connection request from "
            << inet_ntop(AF_INET, &address.sin_addr, str, INET_ADDRSTRLEN)
            << "." << endl;
        serveFile(client);
    }
    return 0;
}

static void serveFile(int client) {
    sockbuf sb(client);
    iosockstream ss(&sb);

    string fileName = getFilename(ss);
    skipHeaders(ss);

    pair<string, bool> contents = loadPath(fileName);
}

static string getFilename(iosockstream& ss) {
    string method, path, protocol;
    ss >> method >> path >> protocol;
    string rest;
    getline(ss, rest);
    cout << "\tPath requested: " << path << endl;
    if (path == "/") {
        // serve current directory
        return (".");
    }
    size_t pos = path.find("/");
    return pos == string::npos ? path : path.substr(pos + 1);
}

static void skipHeaders(iosockstream& ss) {
  string line;
  do {
    getline(ss, line);
  } while (!line.empty() && line != "\r");
}

Questions for discussion:

Part 5: Serving the file to the client

Now that you have the file contents, let’s send this back to the client as an HTTP response. Your response should include a Content-Length header specifying the size of the file contents, as well as a Content-Type header that is text/html; charset=UTF-8 if the file is HTML or text/plain; charset=UTF-8 otherwise.

You should now have a working HTTP server! Navigate to http://mythXX.stanford.edu:portNum/samples/cs110/ to see your server in action.

If you encounter problems, try running curl -vv http://mythXX.stanford.edu:portNum/samples/cs110/ to see what is being sent to your server and what is being received. Alternatively, try nc mythXX portNum and manually type in an HTTP request, then see the response sent back by your server.

Here’s our code:

int main(int argc, char *argv[]) {
    if (argc > 2) {
        cerr << "Usage: " << argv[0] << " [<port>]" << endl;
        return kWrongArgumentCount;
    }
    unsigned short port = extractPort(argv[1]);
    if (port == USHRT_MAX) {
        cerr << "Invalid port number specified" << endl;
        return kIllegalPortArgument;
    }
    int server = createServerSocket(port);
    if (server == -1) {
        cerr << "Failed to bind to port " << port << endl;
        return kServerStartFailure;
    }
    cout << "Server listening on port " << port << "." << endl;

    while (true) {
        struct sockaddr_in address;
        // used to surface ip address from the client
        socklen_t size = sizeof(address);
        bzero(&address, size);
        int client = accept(server, (struct sockaddr *)&address, &size);
        char str[INET_ADDRSTRLEN];
        cout << "Received a connection request from "
            << inet_ntop(AF_INET, &address.sin_addr, str, INET_ADDRSTRLEN)
            << "." << endl;
        serveFile(client);
    }
    return 0;
}

static void serveFile(int client) {
    sockbuf sb(client);
    iosockstream ss(&sb);

    string fileName = getFilename(ss);
    skipHeaders(ss);

    pair<string, bool> contents = loadPath(fileName);
    sendResponse(ss, contents.first, contents.second);
}

static void sendResponse(iosockstream& ss, const string& payload, bool isHTML) {
    ss << "HTTP/1.1 200 OK\r\n";
    if (isHTML) {
        ss << "Content-Type: text/html; charset=UTF-8\r\n";
    } else {
        ss << "Content-Type: text/plain; charset=UTF-8\r\n";
    }
    ss << "Content-Length: " << payload.size() << "\r\n";
    ss << "\r\n";
    ss << payload << flush;
}

Questions for discussion:

Part 6: Adding threading

Let’s speed up your server! Add a ThreadPool to your code to serve files to up to 16 clients at a time.

Here’s our code:

int main(int argc, char *argv[]) {
    if (argc > 2) {
        cerr << "Usage: " << argv[0] << " [<port>]" << endl;
        return kWrongArgumentCount;
    }
    unsigned short port = extractPort(argv[1]);
    if (port == USHRT_MAX) {
        cerr << "Invalid port number specified" << endl;
        return kIllegalPortArgument;
    }
    int server = createServerSocket(port);
    if (server == -1) {
        cerr << "Failed to bind to port " << port << endl;
        return kServerStartFailure;
    }
    cout << "Server listening on port " << port << "." << endl;

    ThreadPool pool(16);
    while (true) {
        struct sockaddr_in address;
        // used to surface ip address from the client
        socklen_t size = sizeof(address);
        bzero(&address, size);
        int client = accept(server, (struct sockaddr *)&address, &size);
        char str[INET_ADDRSTRLEN];
        cout << "Received a connection request from "
            << inet_ntop(AF_INET, &address.sin_addr, str, INET_ADDRSTRLEN)
            << "." << endl;
        pool.schedule([client] {
            serveFile(client);
        });
    }
    return 0;
}

Questions for discussion: