Lecture 14: Intro to Networking
Note: Reading these lecture notes is not a substitute for watching the lecture. I frequently go off script, and you are responsible for understanding everything I talk about in lecture unless I specify otherwise.
How the Internet is Structured
IP addresses and DNS
Every computer on a network has a unique IP address that identifies it on the network. When you want to connect to a server, you need to know its IP address. An IP address is stored as four bytes, separated by periods; an example IP address is “192.168.1.1”. (“500.304.259.1” is not a valid IP address, since the numbers 500, 304, and 259 are greater than 255 and thus can’t be stored in single bytes.)
Humans aren’t generally good at remembering strings of numbers like IP addresses. The Domain Name System (DNS) translates human-friendly hostnames (also called domain names) into IP addresses. You can make a DNS query to ask for the IP address of “web.stanford.edu,” and you’ll hear back that it has an IP address of “171.67.215.200.”
Recall: in filesystems, if you want to find the inode for the file
/usr/class/cs110/hello.txt
, you first ask the root directory for usr
's
inumber, then ask /usr
for class
's inumber, then ask /usr/class
for
cs110
's inumber, then ask /usr/class/cs110
for hello.txt
's inumber. A
very similar process exists in DNS. A set of root name servers are defined to
answer DNS lookups for each domain name suffix (e.g. .com
has a set of root
name servers, .org
has a set of root name servers, .edu
has a set of root
name servers, etc). When you want to look up web.stanford.edu
, we ask the
edu
root server where the nameserver for stanford.edu
is, and then we ask
the nameserver for stanford.edu
where web.stanford.edu
is. If we wanted to
support names like cs.web.stanford.edu
(which Stanford doesn’t do, but it’s
entirely possible), then we could ask the nameserver for web.stanford.edu
where cs.web.stanford.edu
is.
You can play around with DNS lookups by running dig
. You don’t have to be
familiar with the specifics of DNS and you don’t have to know what all the
different kinds of records mean, but you should have a general familiarity with
what it does and how it works.
$ dig -t NS +noall +answer edu # Where are the .edu nameservers?
edu. 91576 IN NS d.edu-servers.net.
edu. 91576 IN NS l.edu-servers.net.
edu. 91576 IN NS a.edu-servers.net.
edu. 91576 IN NS c.edu-servers.net.
edu. 91576 IN NS f.edu-servers.net.
edu. 91576 IN NS g.edu-servers.net.
$ dig -t NS +noall +answer stanford.edu # Where are the stanford.edu nameservers?
stanford.edu. 172800 IN NS argus.stanford.edu.
stanford.edu. 172800 IN NS ns7.dnsmadeeasy.com.
stanford.edu. 172800 IN NS ns6.dnsmadeeasy.com.
stanford.edu. 172800 IN NS atalante.stanford.edu.
stanford.edu. 172800 IN NS avallone.stanford.edu.
stanford.edu. 172800 IN NS ns5.dnsmadeeasy.com.
$ dig -t A +noall +answer web.stanford.edu # Where is web.stanford.edu?
web.stanford.edu. 1800 IN A 171.67.215.200
Port numbers
We want to run a server on a computer. People should be able to connect to this computer over a network and interact with this server.
When you start a server, it binds to a particular port number (valid port numbers are 0-65535). You might have several servers running on a machine for running different services (e.g. a web server, an SSH server, a file server), and each server would be bound to a unique port number. Port numbers for well-known services are generally fixed; SSH servers usually listen for connections on port 22, web servers usually listen for connections on port 80, and mail servers often listen on port 25.
You can think of IP addresses like apartment building addresses, and think of port numbers like apartment numbers within the building; when someone tries to connect to your server, they go to your apartment building, then walk to your specific apartment number in order to reach you. If someone is trying to log into SSH on a server, they will connect to that server’s IP address on port 22, knowing that the SSH server is in that particular apartment.
Network connections
A lot goes into establishing and maintaining a network connection, but most of these details are handled for us by the operating system. If you’re interested, take CS 144 (Networking). From our perspective, once a network connection is opened, we have something like a bidirectional pipe between two computers.
Log into myth, and run the following (if you get an error about “Address already in use,” just choose a number other than 12345 that is at least 1234):
echo $HOST # note this, you need it for the telnet command below
nc -l 12345
In a different terminal window, log into myth (it can be a different myth machine) and run the following:
nc myth65.stanford.edu 12345 # replace myth65 with the host from above
Anything you type into the second window shows up in your first window, and
anything you type into nc
shows up in your first window.
Networking as a form of function call/return
Usually, if you want to run some functionality, you call a function. In this
class, we started to explore another possibility where if you want some
function, you might invoke an executable that performs that functionality (e.g.
if you want to compress a file, but you don’t want to write the compression
code and you don’t want to bundle a C library implementing the compression, you
can invoke the tar
executable present on almost all Linux systems). In
networking, we see another form of function call/return: we can request
functionality to be executed on some remote server, and get the response from
that server. This might be helpful, for example, if we wanted to look up images
of cats (there is no way the Google Images database is going to fit on your
laptop, but you could make a function call to the Google Images servers asking
them for images of cats), or if we want to distribute a computation amongst
more cores than we could fit in a single machine (we can use many servers, and
make “function calls” to those servers, asking them to participate in the
collective computation).
Using basic network connections demonstrated using nc
and telnet
above, we
can develop various communication protocols. Someone can send a command over
the network connection to request something, and the server cand send back a
response. For example, someone could remotely call a function (sometimes called
a remote procedure call – RPC) by sending this over the network connection:
EXECUTE functionName: argument1, argument2
The server can respond over the network connection with the return value of whatever function was requested.
It turns out that this format is very similar to the HTTP protocol, which we’ll talk about next Monday. There are many simple HTTP APIs:
- http://icanhazip.com (tells you your own IP address)
- http://api.open-notify.org/astros.json (lists astronauts currently in space)
- https://www.placecage.com/200/400 (generates placeholder images of a desired size featuring Nick Cage)
- https://placekitten.com/ (same as above, but with kittens)
There are many more complicated APIs letting you do more useful things:
- https://apilist.fun/
- https://www.reddit.com/r/webdev/comments/3wrswc/what_are_some_fun_apis_to_play_with/
For today, we’ll use very basic request/response schemes.
Implementing a server
Hello world, for the twentieth time
int main(int argc, char *argv[]) {
int serverSocket = createServerSocket(12345);
if (serverSocket < 0) {
cout << "Error: Could not start server" << endl;
return 1;
}
while (true) {
int clientSocket = accept(serverSocket, NULL, NULL);
cout << "Sending response to new client" << endl;
write(clientSocket, "Hello world!\n", 13);
close(clientSocket);
}
return 0;
}
createServerSocket
is a function we have written for you, which we will
implement together in class on Wednesday. It returns a file descriptor that is
connected to a virtual file storing a list of people trying to connect to our
server on port 12345
. (It is technically more complicated than that, but you
can think of it this way for now.) The accept
syscall waits until someone
tries to connect to our server, and then once someone tries to connect, it
takes their info off the list of incoming connections that serverSocket
stores, and returns the clientSocket
file descriptor.
The clientSocket
file descriptor is a network socket connected to the
person contacting our server. Sockets are bidirectional, and this file
descriptor is configured to be readable and writable, which is unlike what
you have seen with pipes. If you read from this file descriptor, you’ll read
bytes that the client has sent you, and if you write to this file descriptor,
the bytes you write will be sent to the client.
Since clientSocket
is just a file descriptor, we can write
a message to it.
If you run this server and run nc myth55.stanford.edu 12345
, you’ll see
“Hello world!” printed.
We could extend this program to also print out anything the client sends us by
adding a while
loop between the write
and close
calls to echo out
anything the client sends us:
int main(int argc, char *argv[]) {
int serverSocket = createServerSocket(12345);
if (serverSocket < 0) {
cout << "Error: Could not start server" << endl;
return 1;
}
while (true) {
int clientSocket = accept(serverSocket, NULL, NULL);
cout << "Sending response to new client" << endl;
write(clientSocket, "Hello world!\n", 13);
while (true) {
char buf[1024];
ssize_t bytesRead = read(clientSocket, buf, 1024);
if (read <= 0) break;
write(STDOUT_FILENO, buf, bytesRead);
}
cout << "Client hung up" << endl;
close(clientSocket);
}
return 0;
}
Implementing a basic time server
This server tells you what time it is:
static void publishTime(int clientSocket) {
time_t rawtime;
time(&rawtime);
struct tm *ptm = gmtime(&rawtime);
char timeString[128]; // more than big enough
strftime(timeString, sizeof(timeString), "%c\n", ptm);
write(clientSocket, timeString, strlen(timeString));
close(clientSocket);
}
int main(int argc, char *argv[]) {
int serverSocket = createServerSocket(12345);
if (serverSocket < 0) {
cout << "Error: Could not start server" << endl;
return 1;
}
while (true) {
int clientSocket = accept(serverSocket, NULL, NULL);
cout << "Sending response to new client" << endl;
publishTime(clientSocket);
}
return 0;
}
However, it has a potential problem. (The hello world example above also has
this problem.) We ask write
to send sizeof(timeString)
bytes, but if the
network is congested, it’s possible that write
may only send a few of the
bytes, and ask you to try transmitting the rest later. We need to use a while
loop to handle this case:
static void publishTime(int clientSocket) {
time_t rawtime;
time(&rawtime);
struct tm *ptm = gmtime(&rawtime);
char timeString[128]; // more than big enough
strftime(timeString, sizeof(timeString), "%c\n", ptm);
size_t numBytesWritten = 0, numBytesToWrite = strlen(timeString);
while (numBytesWritten < numBytesToWrite) {
numBytesWritten += write(clientSocket,
timeString + numBytesWritten,
numBytesToWrite - numBytesWritten);
}
close(clientSocket);
}
This works well enough.
Introducing sockbuf and iosockstream
It would be nice to use the C++ stream abstractions instead of needing to work
with raw file descriptors and read
and write
calls. The sockbuf
and
iosockstream
classes let us do this.
static void publishTime(int clientSocket) {
time_t rawtime;
time(&rawtime);
struct tm *ptm = gmtime(&rawtime);
char timeString[128]; // more than big enough
strftime(timeString, sizeof(timeString), "%c", ptm);
sockbuf sb(clientSocket); // destructor closes socket
iosockstream ss(&sb);
ss << timeString << endl;
}
Note that there is no close
call. The sockbuf
destructor takes care of
that.
Implementing a client
We can implement a really simple client that connects to a server, receives a line of data, and prints that out:
#include <string>
#include "socket++/sockstream.h" // for sockbuf, iosockstream
#include "client-socket.h"
using namespace std;
int main(int argc, char *argv[]) {
// Connect to the server
int sock = createClientSocket("myth55.stanford.edu", 12345);
if (sock < 0) {
cout << "Error establishing connection!" << endl;
return 1;
}
// Create a sockstream to make it easier to work with the fd
sockbuf sb(sock);
iosockstream ss(&sb);
// Read a line from the server
string line;
getline(ss, line);
// Print
cout << line << endl;
// sockbuf destructor will close the sock file descriptor
}