Reading 23: Sockets & Networking 6.031 6.031 — Software Construction Spring 2019 Reading 23: Sockets & Networking Client/server design pattern Sockets and streams Using network sockets in Java Wire protocols Testing client/server code Summary Reading 23: Sockets & Networking Software in 6.031 Safe from bugs Easy to understand Ready for change Correct today and correct in the unknown future. Communicating clearly with future programmers, including future you. Designed to accommodate change without rewriting. Objectives In this reading we examine client/server communication over the network using the socket abstraction. Network communication is inherently concurrent, so building clients and servers will require us to reason about their concurrent behavior and to implement them with thread safety. We must also design the wire protocol that clients and servers use to communicate, just as we design the operations that clients of an ADT use to work with it. Client/server design pattern In this reading (and in the problem set) we explore the client/server design pattern for communication with message passing. In this pattern there are two kinds of processes: clients and servers. A client initiates the communication by connecting to a server. The client sends requests to the server, and the server sends replies back. Finally, the client disconnects. A server might handle connections from many clients concurrently, and clients might also connect to multiple servers. Many Internet applications work this way: web browsers are clients for web servers, an email program like Outlook is a client for a mail server, etc. On the Internet, client and server processes are often running on different machines, connected only by the network, but it doesn’t have to be that way — the server can be a process running on the same machine as the client. Sockets and streams We begin with some important concepts related to network communication, and to input/output in general. Input/output (I/O) refers to communication into and out of a process – perhaps over a network, or to/from a file, or with the user on the command line or a graphical user interface. IP addresses A network interface is identified by an IP address. IP version 4 addresses are 32-bit numbers written in four 8-bit parts. For example (as of this writing): 18.9.22.69 is the IP address of a MIT web server. 18.9.25.15 is the address of a MIT incoming email handler. 173.194.193.99 is the address of a Google web server. 127.0.0.1 is the loopback or localhost address: it always refers to the local machine. Technically, any address whose first octet is 127 is a loopback address, but 127.0.0.1 is standard. You can ask Google for your current IP address. In general, as you carry around your laptop, every time you connect your machine to the network it can be assigned a new IP address. Hostnames Hostnames are names that can be translated into IP addresses. A single hostname can map to different IP addresses at different times; and multiple hostnames can map to the same IP address. For example: web.mit.edu is the name for MIT’s web server. You can translate this name to an IP address yourself using dig, host, or nslookup on the command line, e.g.: $ dig +short web.mit.edu
18.9.22.69
dmz-mailsec-scanner-4.mit.edu is the name for one of MIT’s spam filter machines responsible for handling incoming email. google.com is exactly what you think it is. Try using one of the commands above to find google.com’s IP address. What do you see? localhost is a name for 127.0.0.1. When you want to talk to a server running on your own machine, talk to localhost. Translation from hostnames to IP addresses is the job of the Domain Name System (DNS). It’s super cool, but not part of our discussion today. Port numbers A single machine might have multiple server applications that clients wish to connect to, so we need a way to direct traffic on the same network interface to different processes. Network interfaces have multiple ports identified by a 16-bit number. Port 0 is reserved, so port numbers effectively run from 1 to 65535. A server process binds to a particular port — it is now listening on that port. Clients have to know which port number the server is listening on. There are some well-known ports that are reserved for system-level processes and provide standard ports for certain services. For example: Port 22 is the standard SSH port. When you connect to athena.dialup.mit.edu using SSH, the software automatically uses port 22. Port 25 is the standard email server port. Port 80 is the standard web server port. When you connect to the URL http://web.mit.edu in your web browser, it connects to 18.9.22.69 on port 80. When the port is not a standard port, it is specified as part of the address. For example, the URL http://128.2.39.10:9000 refers to port 9000 on the machine at 128.2.39.10. When a client connects to a server, that outgoing connection also uses a port number on the client’s network interface, usually chosen at random from the available non-well-known ports. Network sockets A socket represents one end of the connection between client and server. A listening socket is used by a server process to wait for connections from remote clients. In Java, use ServerSocket to make a listening socket, and use its accept method to listen to it. A connected socket can send and receive messages to and from the process on the other end of the connection. It is identified by both the local IP address and port number plus the remote address and port, which allows a server to differentiate between concurrent connections from different IPs, or from the same IP on different remote ports. In Java, clients use a Socket constructor to establish a socket connection to a server. Servers obtain a connected socket as a Socket object returned from ServerSocket.accept. Buffers The data that clients and servers exchange over the network is sent in chunks. These are rarely just byte-sized chunks, although they might be. The sending side (the client sending a request or the server sending a response) typically writes a large chunk (maybe a whole string like “HELLO, WORLD!” or maybe 20 megabytes of video data). The network chops that chunk up into packets, and each packet is routed separately over the network. At the other end, the receiver reassembles the packets together into a stream of bytes. The result is a bursty kind of data transmission — the data may already be there when you want to read them, or you may have to wait for them to arrive and be reassembled. When data arrive, they go into a buffer, an array in memory that holds the data until you read it. reading exercises Client server socket buffer* You’re developing a new web server program on your own laptop. You start the server running on port 8080. Fill in the blanks for the URL you should visit in your web browser to talk to your server: __A__://__B__:__C__ __A__ 80,8080,http,localhost,loopback,web-server(missing answer) __B__ 80,8080,http,localhost,loopback,web-server(missing answer) __C__ 80,8080,http,localhost,loopback,web-server(missing answer) (missing explanation) check explain Address hostname network stuffer* A connected socket is identified by: local IP address(missing answer) remote IP address(missing answer) local hostname(missing answer) remote hostname(missing answer) local port number(missing answer) remote port number(missing answer) local buffer(missing answer) remote buffer(missing answer) (missing explanation) check explain * see What if Dr. Seuss Did Technical Writing?, although the issue described in the first stanza is no longer relevant with the obsolescence of floppy disk drives Byte streams The data going into or coming out of a socket is a stream of bytes. In Java, InputStream objects represent sources of data flowing into your program. For example: Reading from a file on disk with a FileInputStream User input from System.in Input from a network socket OutputStream objects represent data sinks, places we can write data to. For example: FileOutputStream for saving to files System.out for normal output to the user System.err for error output Output to a network socket Character streams The stream of bytes provided by InputStream and OutputStream is often too low-level to be useful. We may need to interpret the stream of bytes as a stream of Unicode characters, because Unicode can represent a wide variety of human languages (not to mention emoji). A String is a sequence of Unicode characters, not a sequence of bytes, so if we want to use strings to manipulate the data inside our program, then we need to convert incoming bytes into Unicode, and convert Unicode back to bytes when we write it out. In Java, Reader and Writer represent incoming and outgoing streams of Unicode characters. For example: FileReader and FileWriter treat a file as a sequence of characters rather than bytes the wrappers InputStreamReader and OutputStreamWriter adapt a byte stream into a character stream One of the pitfalls of I/O is making sure that your program is using the right character encoding, which means the way a sequence of bytes represents a sequence of characters. The most common character encoding for Unicode characters is UTF-8. For network communication, UTF-8 is the right choice. Usually, when you create a Reader or Writer, Java will default to UTF-8 encoding. But problems occur when other programs on your computer use a different character encoding to read and write files, which means your Java program can’t interoperate with them. To compensate for this file-compatibility problem, Java on your platform may instead default to using a different character encoding, picking it up from a system setting – and then messing up Java code that does network communication, for which the better default is UTF-8. For example, Microsoft Windows has a non-standard encoding called CP-1252, and for Java programs running on Windows, this may be the default. Character-encoding bugs can be hard to detect. UTF-8, CP-1252, and most other character encodings happen to be supersets of one of the oldest standardized character encoding, ASCII. ASCII is big enough to represent English, so English text tends to be unaffected by character-encoding bugs. But the bug is lying in wait for accented Latin characters, or scripts other than the Latin alphabet, or emoji, or even just ‘fancy’ “curved” quotes. When there is a character encoding disagreement, these characters turn into garbage. To avoid character-encoding problems, make sure to explicitly specify the character encoding whenever you construct a Reader or Writer object. The example code in this reading always specifies UTF-8. Blocking Input/output streams exhibit blocking behavior. For example, for socket streams: When an incoming socket’s buffer is empty, calling read blocks until data are available. When the destination socket’s buffer is full, calling write blocks until space is available. Blocking is very convenient from a programmer’s point of view, because the programmer can write code as if the read (or write) call will always work, no matter what the timing of data arrival. If data (or for write, space) is already available in the buffer, the call might return very quickly. But if the read or write can’t succeed, the call blocks. The operating system takes care of the details of delaying that thread until read or write can succeed. As we’ve seen, blocking happens throughout concurrent programming, not just in I/O. Concurrent modules don’t work in lockstep, like sequential programs do, so they typically have to wait for each other to catch up when coordinated action is required. Using network sockets in Java Let’s look at the nuts and bolts of socket programming in Java. For the sake of introduction, we’ll look at a simple EchoServer that just echoes everything the client sends it, and an EchoClient that takes console input from the user and sends it to the EchoServer. You can find the full code for EchoClient and EchoServer on GitHub. Some of the code snippets in this reading are simplified for presentation purposes, but the link in the corner of each snippet points to the full version. Client code First we’ll look at the client point of view. The client opens a connection to a hostname and port by constructing a Socket object: EchoClient.java line 20 String hostname = "localhost";
int port = 4589;
Socket socket = new Socket(hostname, port); If there is a server process running on the given hostname (in this case localhost, meaning the same machine that the client process is running on) and listening for connections to the designated port, then this constructor will succeed and produce an open Socket object. If there is no server process listening on that port, then the connection fails and new Socket() throws an IOException. Assuming the connection is successful, the client can now obtain two byte streams that communicate with the server: EchoClient.java line 44 OutputStream outToServer = socket.getOutputStream();
InputStream inFromServer = socket.getInputStream(); With sockets, remember that the output of one process is the input of another process. If a client and a server have a socket connection, then the client has an output stream that flows to the server’s input stream, and vice versa. A client usually wants more powerful operations than the simple InputStream and OutputStream interfaces provide. For EchoClient, we want to use streams of characters instead of bytes, using the Reader and Writer interfaces described earlier. We also want to read and write full lines of characters, terminated by newlines, which is functionality provided by BufferedReader and PrintWriter. So we wrap the streams in classes that provide those operations: EchoClient.java line 44 PrintWriter writeToServer =
new PrintWriter(new OutputStreamWriter(outToServer, StandardCharsets.UTF_8));
BufferedReader readFromServer =
new BufferedReader(new InputStreamReader(inFromServer, StandardCharsets.UTF_8)); Note the explicit specification of UTF-8 character encoding, the best choice for portable network communication. The basic loop of EchoClient prepares a message for the server by letting the user type it at the keyboard, then sends the message to the server, and then waits for a reply: EchoClient.java line 52 BufferedReader readFromUser =
new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
while (true) {
String message = readFromUser.readLine();
...
writeToServer.println(message);
...
String reply = readFromServer.readLine();
...
} Note that all three of these method calls are potentially blocking. First the client process will block until the user types something at the console and presses Enter. Then it will send it to the server with println(), but if the server’s buffer happens to be full, this println() call will block until it can fit the message into the buffer. Then the process will block until the server has sent its reply. It should not surprise us that this code has a very similar flavor to code for implementing message passing with blocking queues. To send a request to the server, we write the request to the socket output stream, just as we would use BlockingQueue.put() in the queue paradigm. To receive a reply, we read it from the input stream, where we would have used BlockingQueue.take(). Both sending and receiving use blocking calls. Two important details are hiding in the ... parts of the code sketch above. First, readLine() returns null if the stream it is reading from has been closed by the other side. For a socket stream, this would indicate that the server has closed its side of the connection. EchoClient responds to that by exiting the loop: EchoClient.java line 66 String reply = readFromServer.readLine();
if (reply == null) break; // server closed the connection Second, println() initially puts the message into a buffer inside the PrintWriter object, on the client’s side of the connection. The message content isn’t sent to the server until the buffer fills up or the client closes its side of the connection. So it’s critically important to flush the buffers after writing, forcing all buffer contents to be sent: EchoClient.java line 62 writeToServer.println(message);
writeToServer.flush(); // important! otherwise the line may just sit in a buffer, unsent PrintWriter has a constructor that enables automatic flushing, but it only works for some operations. With autoflush turned on, println(message) automatically flushes the buffer, but the seemingly-equivalent print(message + "\n") may sit in the buffer, unsent. We all learned this as children, but it’s worth restating: always remember to flush. Finally, after exiting the loop, we close the streams and the socket, which both signals to the server that the client is done, and frees buffer memory and other resources associated with the streams: EchoClient.java line 78 readFromServer.close();
writeToServer.close();
socket.close(); Server code The server starts with a listening socket, represented by a ServerSocket object. The server creates a ServerSocket object to listen for incoming client connections on a particular port number: EchoServer.java line 25 int port = 4589;
ServerSocket serverSocket = new ServerSocket(port); If another listening socket is already listening to this port, possibly in a different process, then this constructor will throw a BindException to tell you that the address is already in use. Note that no hostname is needed to create this socket, because by default the server socket listens for connections to any network interface of the machine that the server process is running on. Unlike an ordinary Socket, the ServerSocket does not provide byte streams to read or write. Instead, it produces a sequence of new client connections. Each time a client opens a connection to the specified port, the ServerSocket yields up a new Socket object for the new connection. The next client connection can be obtained using accept(): EchoServer.java line 33 Socket socket = serverSocket.accept(); The accept() method is blocking. If no client connection is pending, then accept() waits until a client arrives before returning a Socket object for that client connection. Once the server has a connection to a client, it uses the socket’s input and output streams in much the same way the client did: receiving messages from the client, and preparing and sending replies back. EchoServer.java line 54 PrintWriter writeToClient =
new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
BufferedReader readFromClient =
new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
while (true) {
// read a message from the client
String message = readFromClient.readLine();
if (message == null) break; // client closed its side of the connection
if (message.equals("quit")) break; // client sent a quit message
// prepare a reply, in this case just echoing the message
String reply = "echo: " + message;
// write the reply
writeToClient.println(reply);
writeToClient.flush(); // important! otherwise the reply may just sit in a buffer, unsent
}
// close the streams and socket
readFromServer.close();
writeToServer.close();
socket.close(); The server implements two different ways to stop communication between client and server. One way we have already seen in message-passing with queues: the client sends a poison pill message, in this case "quit". But another way that the client can stop is simply to close its end of the connection. readLine() recognizes this event as the end of the socket input stream, and signals it by returning null. Multithreaded server code The server code we’ve written to this point has the limitation that it can handle only one client at a time. The server loop is dedicated to a single client, blocking on readFromClient.readLine() and repeatedly reading and replying to messages from that one client, until the client disconnects. Only then does the server go back to its ServerSocket and accept a connection from the next client waiting in line. If we want to handle multiple clients at once, using blocking I/O, then the server needs a new thread to handle I/O with each new client. While each client-specific thread is working with its own client, another thread (perhaps the main thread) stands ready to accept a new connection. Here is how the multithreaded EchoServer works. The connection-accepting loop is run by the main thread: EchoServer.java line 31 while (true) {
// get the next client connection
Socket socket = serverSocket.accept();
// handle the client in a new thread, so that the main thread
// can resume waiting for another client
new Thread(new Runnable() {
public void run() {
handleClient(socket);
}
}).start();
} And then the client-handling loop is run by the new thread created for each new connection: EchoServer.java line 53 private static void handleClient(Socket socket) {
// same server loop code as above:
// open readFromClient and writeToClient streams
// while (true) {
// read message from client
// prepare reply
// write reply to client
// }
// close streams and socket
} Closing streams and sockets with try-with-resources One new bit of Java syntax is particularly useful for working with streams and sockets: the try-with-resources statement. This statement automatically calls close() on variables declared in its parenthesized preamble: try (
// preamble: declare variables initialized to objects that need closing after use
) {
// body: runs with those variables in scope
} catch(...) {
// catch clauses: optional, handles exceptions thrown by the preamble or body
} finally {
// finally clause: optional, runs after the body and any catch clause
}
// no matter how the try statement exits, it automatically calls
// close() on all variables declared in the preamble For example, here is how it can be used to ensure that a client socket connection is closed: EchoClient.java line 23 try (
Socket socket = new Socket(hostname, port);
) {
// read and write to the socket
} catch (IOException ioe) {
ioe.printStackTrace();
} // socket.close() is automatically called here The try-with-resources statement is useful for any object that should be closed after use: byte streams: InputStream, OutputStream character streams: Reader, Writer files: FileInputStream, FileOutputStream, FileReader, FileWriter sockets: Socket, ServerSocket The Python with statement has similar semantics. reading exercises Network sockets 1 Alice has a connected socket with Bob. How does she send a message to Bob? write to her socket’s input stream(missing answer) write to her socket’s output stream(missing answer) write to Bob’s socket’s input stream(missing answer) write to Bob’s socket’s output stream(missing answer) (missing explanation) check explain Network sockets 2 Which of these are necessary for a client to know in order to connect to and communicate with a server? server IP address(missing answer) server hostname(missing answer) server port number(missing answer) server process name(missing answer) wire protocol*(missing answer) * defines the messages sent and received, more details in the next section (missing explanation) check explain Echo echo echo echo In the EchoClient example, which of these might block? socket.getInputStream()(missing answer) new BufferedReader(new InputStreamReader(...))(missing answer) readFromUser.readLine()(missing answer) readFromServer.readLine()(missing answer) (missing explanation) And in EchoServer, which of these might block? new ServerSocket(...)(missing answer) serverSocket.accept()(missing answer) readFromClient.readLine()(missing answer) ioe.printStackTrace()(missing answer) (missing explanation) check explain Block block block block Since BufferedReader.readLine() is a blocking method, which of these is true: When a thread calls readLine, all other threads block until readLine returns(missing answer) When a thread calls readLine, that thread blocks until readLine returns(missing answer) When a thread calls readLine, the call can be blocked and an exception is thrown(missing answer) BufferedReader has its own thread for readLine, which runs a block of code passed in by the client(missing answer) (missing explanation) check explain Wire protocols Now that we have our client and server connected up with sockets, what do they pass back and forth over those sockets? Unlike the in-memory objects sent and received using synchronized queues in Message-Passing, here we send and receive streams of bytes. Instead of choosing or designing an abstract data type for our messages, we will choose or design a protocol. A protocol is a set of messages that can be exchanged by two communicating parties. A wire protocol in particular is a set of messages represented as byte sequences, like hello world and bye (assuming we’ve agreed on a way to encode those characters into bytes). Many Internet applications use simple ASCII-based wire protocols. You can use a program called Telnet to check them out. Telnet client telnet is a utility that allows you to make a direct network connection to a listening server and communicate with it via a terminal interface. Windows, Linux, and Mac OS X can all run telnet, although more recent operating systems no longer have it installed by default. You should first check if telnet is installed by running the command telnet on the command line. If you don’t have it, then look for instructions about how to install it (Linux, Windows, Mac OS). On Windows, an alternative telnet client is PuTTY, which has a graphical user interface. Let’s look at some examples of wire protocols. HTTP Hypertext Transfer Protocol (HTTP) is the language of the World Wide Web. We already know that port 80 is the well-known port for speaking HTTP to web servers, so let’s talk to one on the command line. Try using your telnet client with the commands below. User input is shown in green, and for input to the telnet connection, newlines (pressing enter) are shown with ↵. (If you are using PuTTY on Windows, you will enter the hostname and port in PuTTY's connection dialog, and you should also select Connection type: Raw, and Close window on exit: Never. The last option will prevent the window from disappearing as soon as the server closes its end of the connection.) $ telnet www.eecs.mit.edu 80
Trying 18.62.0.96...
Connected to eecsweb.mit.edu.
Escape character is '^]'.
GET /↵
... lots of output ...
Homepage | MIT EECS
... lots more output ...
The GET command gets a web page. The / is the path of the page you want on the site. So this command fetches the page at http://www.eecs.mit.edu:80/. Since 80 is the default port for HTTP, this is equivalent to visiting http://www.eecs.mit.edu/ in your web browser. The result is HTML code that your browser renders to display the EECS homepage. Internet protocols are defined by RFC specifications (RFC stands for “request for comments”, and some RFCs are eventually adopted as standards). RFC 1945 defined HTTP version 1.0, and was superseded by HTTP 1.1 in RFC 2616. So for many web sites, you might need to speak HTTP 1.1 if you want to talk to them. For example: $ telnet web.mit.edu 80
Trying 18.9.22.69...
Connected to web.mit.edu.
Escape character is '^]'.
GET /about/ HTTP/1.1↵
Host: web.mit.edu↵
↵
HTTP/1.1 200 OK
Date: Tue, 18 Apr 2017 15:25:23 GMT
... more headers ...
9b7
... more HTML ...
About MIT | MIT - Massachusetts Institute of Technology
... lots more HTML ...