Java 6: I/O Streams
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 chooseVCS -> 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
andZipInputStream
to create or open zip files. These are in the packagejava.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 aFileReader
to provide line-by-line instead of byte-by-byte reading.- A
BufferedReader
is not a subclass ofFileReader
(although that is another way to add functionality). Instead, aBufferedReader
object takes aReader
(which interestingly is the parent class of bothBufferedReader
andFileReader
) as a parameter in its constructor.
- A
- 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.
- More details on this design pattern are available in the Best Practices Software Engineering Guide.
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 thecatch
block- Java defines many types of exceptions - you are surely familiar with a few like
NullPointerException
andArrayIndexOutOfBounds
. 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.
- Java defines many types of exceptions - you are surely familiar with a few like
- A
finally
block can optionally be included. This code will always be executed after either thetry
orcatch
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 like209.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
or127.0.0.1
.
- For debugging we often run both the client and server on one computer. In this case we can specify the server as
- 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 callingprintln
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 supportsPOST
, 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
port80
and send the GET message listed above. Keep receiving lines and printing them to the screen until no more data is available.