This week you will not need to submit your solutions.

  • Open the IntelliJ IDE
  • Choose Check out from Version Control -> git in the welcome window, or choose VCS -> Check out from Version Control -> git
    • Enter the git URL: https://github.com/cs2113f18/template-j-6-io.git
    • Keep hitting “Next” or “Yes” until it opens a new window with the project you cloned.
    • Note: Always use one of the two options listed above. Do NOT try to do File->New… It won’t work correctly!
  • The full slides are available here - This includes intro and conclusion slides not shown below!
  • A solved version of the KnockKnock Server is available here.

Objectives

  • To understand the similarities and differences of file and network IO
  • To learn the basic architecture of a networked application using sockets
  • To appreciate Object Oriented design in Java’s IO libraries

1. Input and Output Streams

To start with, we will divide the Java I/O classes into four categories:

Classes that work at the byte level like OutputStream (for writing) and InputStream (for reading)

  • OutputStream itself is an abstract class from which many output-related classes derive.
  • InputStream itself is an abstract class from which many input-related classes derive.
  • When do you need to work at the byte level? One example is when you read a data file (like an image) from a disk.

Classes that work at the Unicode level like Writer and Reader.

  • Both are abstract classes, from which useful classes are derived.
  • Here, Unicode is the multi-byte representation of characters.
  • Thus, instead of doing conversion from bytes to Unicode, these classes offer a way to directly read char by char.

Classes that work at the String level like PrintWriter and BufferedReader.

  • These are the practical classes you end up using for most applications because they can perform IO at a more useful granularity like a line from a file.

Miscellaneous classes:

  • A number of unrelated classes are also bundled in java.io for various uses. Examples include:
  • File: a class for handling files, especially with directory-related actions.
  • RandomAccessFile: a class for allowing random read or writes to a file.
  • StreamTokenizer: a class that facilitates breaking of input into tokens.
  • ZipOutputStream and ZipInputStream to create or open zip files. These are in the package java.util.zip.

Java’s IO libraries illustrate several Object Oriented Design principles, which are collectively known as Design Patterns.

  • The Decorator Pattern “wraps” another class in order to provide additional functionality.
  • A BufferedReader wraps a FileReader to provide line-by-line instead of byte-by-byte reading.
    • A BufferedReader is not a subclass of FileReader (although that is another way to add functionality). Instead, a BufferedReader object takes a Reader (which interestingly is the parent class of both BufferedReader and FileReader) as a parameter in its constructor.
  • The Decorator pattern is also used to literally “decorate” GUI classes. For example, if you had a simple, unstyled button class, you might use the Decorator pattern to wrap it with another class that knows how to draw a fancy frame and shadow around that button.

Here is a basic example of File I/O:

package fileio;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

public class RandomLine {

 public static void main(String[] args) {

  BufferedReader inputStream = null;

  try {
   inputStream = new BufferedReader(new FileReader("files/hamlet.txt"));
   String line;

   // print all lines in the file
   while ((line = inputStream.readLine()) != null) {
    System.out.println(line);
   }
  } catch (IOException e) {
   /* If there was an error, print a stack trace */
   e.printStackTrace();
  } finally {
   /* Once the try finishes, clean things up */
   if (inputStream != null) {
    try {
     inputStream.close();
    } catch (IOException e) {
     e.printStackTrace();
    }
   }
  }
 }
}

Practice: Modify the example so that it stores all non-zero length lines into a list. Then print out a random entry from the list.

Try and Catch

Java uses try and catch blocks to help with error handling.

  • The code inside a try block will be executed but…
  • if an exception occurs, execution will immediately switch to the catch block
    • Java defines many types of exceptions - you are surely familiar with a few like NullPointerException and ArrayIndexOutOfBounds. Generally these two types of exceptions indicate a bug in the program’s logic, so we don’t write code to try to catch them – ideally this type of exception would be found during development and resolved with the proper logical checks, such as looking at the size of an array.
    • IO based exceptions are different – these occur because of something outside of the programmer’s control: a file is missing from the disk, the Internet is currently down, etc. We need to catch these to ensure the code operates in a correct manner because of an external issue outside our code.
  • A finally block can optionally be included. This code will always be executed after either the try or catch block finishes.
    - This code is usually used to clean up resources after some kind of IO finishes.

Writing to Files

Writing to files is similar to reading. Here is the simplest example:

public class WriteHello {
    public static void main(String[] args) throws IOException {
        PrintWriter outputStream = null;
        try {
            outputStream =
                new PrintWriter(new FileWriter("file.txt"));
                outputStream.println("Hello");
                outputStream.println("World");
            }
        }
    }
}

Note that the PrintWriter outputStream variable in this code acts almost the same as the System.out object you are used to for printing out text to the screen. Instead of printing to the screen, it prints to a file.


2. Networking

Networking allows us to write programs that communicate across links such as the Internet. Reading or writing to a file is very similar to reading or writing to a network connection, so much of what you know for file IO will still apply.

Networking Basics:

A Socket represents the connection between a client and a server

  • Servers wait around for incoming connections. Servers generally listen so that any client can connect to them.
  • Clients proactively initiate the connection to the server. A client must specify the exact server it is connecting to.

We address a server (or client) by using a hostname or IP address and port number.

  • A hostname like google.com will always be resolved (converted) to an IP address like 209.85.201.102
    • For debugging we often run both the client and server on one computer. In this case we can specify the server as localhost or 127.0.0.1.
  • We need to use a port number in addition to the name/address because each computer typically only has one IP address, but it might need to run many different servers on it at the same time.
    • The port number lets us specify exactly what service we want to connect to
    • While the number itself is meaningless, we rely on standards so that clients and servers know what port to use, e.g., port 80 is generally used for web servers. Here’s a full list of common port numbers.
    • Port numbers 0 to 1023 are reserved for “well known” services. Trying to use one of these ports often requires special privileges on your computer (e.g., Administrator mode or root access). You should use port numbers greater than 1023 to avoid these issues.

Here is a simple client that sends a message to the server and then waits for a response.

package netsimple;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

public class SimpleClient {

  public static void main(String[] args) {

    String host = "127.0.0.1";
    int portnum = 5555;

    Socket s;
    try {
      // create socket, input and output streams
      s = new Socket(host, portnum);
      PrintWriter out = new PrintWriter(s.getOutputStream(), true);
      BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));

      // send a message
      String name = "Hello server";
      out.println(name);

      // Make sure messages are sent
      out.flush();

      // receive and print confirmation
      System.out.println(in.readLine());

      // clean up
      out.close();
      s.close();

    } catch (UnknownHostException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

}

Here is a server that waits for a message from the client and then sends a response.

package netsimple;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleServer {
  public static void main(String[] args) {

    String host = "127.0.0.1";
    int portnum = 5555;

    ServerSocket server = null;
    try {
      server = new ServerSocket(portnum);
    } catch (IOException e) {
      e.printStackTrace();
    }

    while (true) {
      try {
        Socket sock = server.accept(); // wait for a call
        BufferedReader in = new BufferedReader(new InputStreamReader
            (sock.getInputStream()));
        PrintWriter out = new PrintWriter(sock.getOutputStream(), true);

        String input = in.readLine(); // read a message
        out.println("Message received: " + input);
        out.println("Hello Client! You sent me: " + input);
        out.close();
        in.close();
        sock.close(); // hang up

      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

Sending Data:

A common source of errors is to not flush your PrintWriter correctly.

  • If the autoFlush parameter is set to false, then calling println will not immediately result in the data being sent.
    • Java knows it is more efficient to send lots of data at once, so it will build up a buffer of lines and then send them all at once.
    • This is problematic if your code expects to send a message and then wait for a response – you might be waiting for the response without your data having been sent (flushed)!

Client / Server Protocols:

Humans speak languages so they can understand each other. Computers need the same thing! But we call this a protocol.

  • A protocol specifies the order and structure of messages sent between clients and servers.
  • Often these are pretty simple and are designed to be easily understood by humans (or at least computer scientists)

For example, to request a web page, your web browser sends these words:

GET /timwood/simple.html HTTP/1.1
Host: faculty.cs.gwu.edu

This protocol is quite specific:

  • GET tells the web server we are trying to download a file. The HTTP protocol also supports POST, which is used when filling out a form which requires pushing data to the server.
  • An empty line is required after the Host line. This tells the server we have finished sending our message.

Practice: Write a program that acts a simple web browser. Have it connect to 185.199.109.153 port 80 and send the GET message listed above. Keep receiving lines and printing them to the screen until no more data is available.


Previous - Next

© 2003, Rahul Simha (revised 2017). Further revised by Tim Wood 2018.