Network Programming: Creating Clients and Servers with Java Sockets
- Implementing a Client
- Parsing Strings by Using StringTokenizer
- Example: A Client to Verify E-Mail Addresses
- Example: A Network Client That Retrieves URLs
- The URL Class
- WebClient: Talking to Web Servers Interactively
- Implementing a Server
- Example: A Simple HTTP Server
- RMI: Remote Method Invocation
- Summary
Topics in This Chapter
Implementing a generic network client
Processing strings with StringTokenizer
Network programming involves two distinct pieces: a client and a server.
Core Note
Remember that the security manager of most browsers prohibits applets from making network connections to machines other than the one from which they are loaded.
In this chapter, you'll see how to directly implement clients and servers by using network "sockets." Although this is the lowest-level type of network programming in the Java platform, if you've used sockets in other languages, you may be surprised at how simple they are to use in Java technology. The URL class helps hide the details of network programming by providing methods for opening connections and binding input/output streams with sockets. In addition to sockets, which can communicate with general-purpose programs in arbitrary languages, Java provides two higher-level packages for communicating with specific types of systems: Remote Method Invocation (RMI) and database connectivity (JDBC). The RMI package lets you easily access methods in remote Java objects and transfer serializable objects across network connections. RMI is covered in Section 17.9. JDBC lets you easily send SQL statements to remote databases. Java Database Connectivity is covered in Chapter 22.
17.1 Implementing a Client
The client is the program that initiates a network connection. Implementing a client consists of five basic steps:
Create a Socket object.
Create an output stream that can be used to send information to the Socket.
Create an input stream to read the response from the server.
Do I/O with input and output streams.
Close the Socket when done.
Each of these steps is described in the sections that follow. Note that most of the methods described in these sections throw an IOException and need to be wrapped in a try/catch block.
Create a Socket object.
A Socket is the Java object corresponding to a network connection. A client connects to an existing server that is listening on a numbered network port for a connection. The standard way of making a socket is to supply a hostname or IP address and port as follows:
Socket client = new Socket("hostname", portNumber);
or
Socket client = new Socket("IP address", portNumber);
If you are already familiar with network programming, note that this approach creates a connection-oriented socket. The Java programming language also supports connectionless (UDP) sockets through the Data_gramSocket class.
Create an output stream that can be used to send information to the Socket.
The Java programming language does not have separate methods to send data to files, sockets, and standard output. Instead, Java starts with different underlying objects, then layers standard output streams on top of them. So, any variety of OutputStream available for files is also available for sockets. A common one is PrintWriter. This stream lets you use print and println on the socket in exactly the same way as you would print to the screen. The PrintWriter constructor takes a generic Out_putStream as an argument, which you can obtain from the Socket by means of getOutputStream. In addition, you should specify true in the constructor to force autoflush. Normally, the contents written to the stream will remain in a buffer until the buffer becomes completely full. Once the buffer is full, the contents are flushed out the stream. Autoflush guarantees that the buffer is flushed after every println, instead of waiting for the buffer to fill. Here's an example:
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
You can also use an ObjectOutputStream to send complex Java objects over the network to be reassembled at the other end. An ObjectOutputStream connected to the network is used in exactly the same way as one connected to a file; simply use writeObject to send a serializable object and all referenced serializable objects. The server on the other end would use an ObjectInputStream's readObject method to reassemble the sent object. Note that all AWT components are automatically serializable, and making other objects serializable is a simple matter of declaring that they implement the Serializ_able interface. See Section 13.9 (Serializing Windows) for more details and an example. Also see Section 17.9 (RMI: Remote Method Invocation) for a high-level interface that uses serialization to let you distribute Java objects across networks.
Create an input stream to read the response from the server.
Once you send data to the server, you will want to read the server's response. Again, there is no socket-specific way of doing this; you use a standard input stream layered on top of the socket. The most common one is an InputStreamReader, for handling character-based data. Here is a sample:
InputStreamReader in = new InputStreamReader(client.getInputStream());
Although this approach is the simplest, in most cases a better approach is to wrap the socket's generic InputStream inside a BufferedReader. This approach causes the system to read the data in blocks behind the scenes, rather than reading the underlying stream every time the user performs a read. This approach usually results in significantly improved performance at the cost of a small increase in memory usage (the buffer size, which defaults to 512 bytes). Here's the idea:
BufferedReader in = new BufferedReader (new InputStreamReader(client.getInputStream()));
Core Performance Tip
If you are going to read from a socket multiple times, a buffered input stream can speed things up considerably.
In a few cases, you might want to send data to a server but not read anything back. You could imagine a simple e-mail client working this way. In that case, you can skip this step. In other cases, you might want to read data without sending anything first. For instance, you might connect to a network "clock" to read the time. In such a case, you would skip the output stream and just follow this step. In most cases, however, you will want to both send and receive data, so you will follow both steps. Also, if the server is sending complex objects and is written in the Java programming language, you will want to open an ObjectInputStream and use readObject to receive data.
Do I/O with input and output streams.
A PrintStream has print and println methods that let you send a single primitive value, a String, or a string representation of an Object over the network. If you send an Object, the object is converted to a String by calling the toString method of the class. Most likely you are already familiar with these methods, since System.out is in fact an instance of Print_Stream. PrintStream also inherits some simple write methods from OutputStream. These methods let you send binary data by sending an individual byte or an array of bytes.
PrintWriter is similar to PrintStream and has the same print and println methods. The main difference is that you can create print writers for different Unicode character sets, and you can't do that with PrintStream.
BufferedReader has two particularly useful methods: read and readLine. The read method returns a single char (as an int); readLine reads a whole line and returns a String. Both of these methods are blocking; they do not return until data is available. Because readLine will wait until receiving a carriage return or an EOF (the server closed the connection), readLine should be used only when you are sure the server will close the socket when done transmitting or when you know the number of lines that will be sent by the server. The readLine method returns null upon receiving an EOF.
Close the Socket when done.
When you are done, close the socket with the close method:
client.close();
This method closes the associated input and output streams as well.
Example: A Generic Network Client
Listing 17.1 illustrates the approach outlined in the preceding section. Processing starts with the connect method, which initiates the connection, then passes the socket to handleConnection to do the actual communication. This version of handleConnection simply reports who made the connection, sends a single line to the server ("Generic Network Client"), reads and prints a single response line, and exits. Real clients would override handleConnection to implement their desired behavior but could leave connect unchanged.
Listing 17.1 NetworkClient.java
import java.net.*; import java.io.*; /** A starting point for network clients. You'll need to * override handleConnection, but in many cases connect can * remain unchanged. It uses SocketUtil to simplify the * creation of the PrintWriter and BufferedReader. */ public class NetworkClient { protected String host; protected int port; /** Register host and port. The connection won't * actually be established until you call * connect. */ public NetworkClient(String host, int port) { this.host = host; this.port = port; } /** Establishes the connection, then passes the socket * to handleConnection. */ public void connect() { try { Socket client = new Socket(host, port); handleConnection(client); } catch(UnknownHostException uhe) { System.out.println("Unknown host: " + host); uhe.printStackTrace(); } catch(IOException ioe) { System.out.println("IOException: " + ioe); ioe.printStackTrace(); } } /** This is the method you will override when * making a network client for your task. * The default version sends a single line * ("Generic Network Client") to the server, * reads one line of response, prints it, then exits. */ protected void handleConnection(Socket client) throws IOException { PrintWriter out = SocketUtil.getWriter(client); BufferedReader in = SocketUtil.getReader(client); out.println("Generic Network Client"); System.out.println ("Generic Network Client:\n" + "Made connection to " + host + " and got '" + in.readLine() + "' in response"); client.close(); } /** The hostname of the server we're contacting. */ public String getHost() { return(host); } /** The port connection will be made on. */ public int getPort() { return(port); } }
The SocketUtil class is just a simple interface to the BufferedReader and PrintWriter constructors and is given in Listing 17.2.
Listing 17.2 SocketUtil.java
import java.net.*; import java.io.*; /** A shorthand way to create BufferedReaders and * PrintWriters associated with a Socket. */ public class SocketUtil { /** Make a BufferedReader to get incoming data. */ public static BufferedReader getReader(Socket s) throws IOException { return(new BufferedReader( new InputStreamReader(s.getInputStream()))); } /** Make a PrintWriter to send outgoing data. * This PrintWriter will automatically flush stream * when println is called. */ public static PrintWriter getWriter(Socket s) throws IOException { // Second argument of true means autoflush. return(new PrintWriter(s.getOutputStream(), true)); } }
Finally, the NetworkClientTest class, shown in Listing 17.3, provides a way to use the NetworkClient class with any hostname and any port.
Listing 17.3 NetworkClientTest.java
/** Make simple connection to host and port specified. */ public class NetworkClientTest { public static void main(String[] args) { String host = "localhost"; int port = 8088; if (args.length > 0) { host = args[0]; } if (args.length > 1) { port = Integer.parseInt(args[1]); } NetworkClient nwClient = new NetworkClient(host, port); nwClient.connect(); } }
Output: Connecting to an FTP Server
Let's use the test program in Listing 17.3 to connect to Netscape's public FTP server, which listens on port 21. Assume > is the DOS or Unix prompt.
> java NetworkClientTest ftp.netscape.com 21 Generic Network Client: Made connection to ftp.netscape.com and got '220 ftp26 FTP server (UNIX(r) System V Release 4.0) ready.' in response