- 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
17.8 Example: A Simple HTTP Server
In Listing 17.19 we adapt the NetworkServer class to act as an HTTP server. Rather than returning files, however, we have the server simply echo back the received input by storing all of the input lines, then transmit back an HTML file that shows the sent line. Although writing programs that output HTML seems odd, in Chapter 19 (Server-Side Java: Servlets) and Chapter 24 (JavaScript: Adding Dynamic Content to Web Pages) you'll see that this is actually common practice. Furthermore, having a program that can act as an HTTP server but returns a Web page showing the received input is a useful debugging tool when you are working with HTTP clients and servlet or JSP programming. You'll see this class used many times in the HTTP and servlet chapters.
Listing 17.19 EchoServer.java
import java.net.*; import java.io.*; import java.util.StringTokenizer; /** A simple HTTP server that generates a Web page showing all * of the data that it received from the Web client (usually * a browser). To use this server, start it on the system of * your choice, supplying a port number if you want something * other than port 8088. Call this system server.com. Next, * start a Web browser on the same or a different system, and * connect to http://server.com:8088/whatever. The resultant * Web page will show the data that your browser sent. For * debugging in servlet or CGI programming, specify * http://server.com:8088/whatever as the ACTION of your HTML * form. You can send GET or POST data; either way, the * resultant page will show what your browser sent. */ public class EchoServer extends NetworkServer { protected int maxRequestLines = 50; protected String serverName = "EchoServer"; /** Supply a port number as a command-line * argument. Otherwise, use port 8088. */ public static void main(String[] args) { int port = 8088; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch(NumberFormatException nfe) {} } new EchoServer(port, 0); } public EchoServer(int port, int maxConnections) { super(port, maxConnections); listen(); } /** Overrides the NetworkServer handleConnection method to * read each line of data received, save it into an array * of strings, then send it back embedded inside a PRE * element in an HTML page. */ public void handleConnection(Socket server) throws IOException{ System.out.println (serverName + ": got connection from " + server.getInetAddress().getHostName()); BufferedReader in = SocketUtil.getReader(server); PrintWriter out = SocketUtil.getWriter(server); String[] inputLines = new String[maxRequestLines]; int i; for (i=0; i<maxRequestLines; i++) { inputLines[i] = in.readLine(); if (inputLines[i] == null) // Client closed connection. break; if (inputLines[i].length() == 0) { // Blank line. if (usingPost(inputLines)) { readPostData(inputLines, i, in); i = i + 2; } break; } } printHeader(out); for (int j=0; j<i; j++) { out.println(inputLines[j]); } printTrailer(out); server.close(); } // Send standard HTTP response and top of a standard Web page. // Use HTTP 1.0 for compatibility with all clients. private void printHeader(PrintWriter out) { out.println ("HTTP/1.0 200 OK\r\n" + "Server: " + serverName + "\r\n" + "Content-Type: text/html\r\n" + "\r\n" + "<HTML>\n" + "<!DOCTYPE HTML PUBLIC " + "\"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" + "<HEAD>\n" + " <TITLE>" + serverName + " Results</TITLE>\n" + "</HEAD>\n" + "\n" + "<BODY BGCOLOR=\"#FDF5E6\">\n" + "<H1 ALIGN=\"CENTER\">" + serverName + " Results</H1>\n" + "Here is the request line and request headers\n" + "sent by your browser:\n" + "<PRE>"); } // Print bottom of a standard Web page. private void printTrailer(PrintWriter out) { out.println ("</PRE>\n" + "</BODY>\n" + "</HTML>\n"); } // Normal Web page requests use GET, so this server can simply // read a line at a time. However, HTML forms can also use // POST, in which case we have to determine the number of POST // bytes that are sent so we know how much extra data to read // after the standard HTTP headers. private boolean usingPost(String[] inputs) { return(inputs[0].toUpperCase().startsWith("POST")); } private void readPostData(String[] inputs, int i, BufferedReader in) throws IOException { int contentLength = contentLength(inputs); char[] postData = new char[contentLength]; in.read(postData, 0, contentLength); inputs[++i] = new String(postData, 0, contentLength); } (continued) // Given a line that starts with Content-Length, // this returns the integer value specified. private int contentLength(String[] inputs) { String input; for (int i=0; i<inputs.length; i++) { if (inputs[i].length() == 0) break; input = inputs[i].toUpperCase(); if (input.startsWith("CONTENT-LENGTH")) return(getLength(input)); } return(0); } private int getLength(String length) { StringTokenizer tok = new StringTokenizer(length); tok.nextToken(); return(Integer.parseInt(tok.nextToken())); } }
Figure 172 shows the EchoServer in action, displaying the header lines sent by Netscape 4.7 on Windows 98.
Figure 172 The EchoServer shows data sent by the browser.
ThreadedEchoServer: Adding Multithreading
The problem with the EchoServer is that the service can only accept one connection at a time. If, for instance, it takes 0.001 seconds to establish a connection but 0.01 seconds for the client to transmit the request and 0.01 seconds for the server to return the results, then the entire process takes about 0.02 seconds, and the server can only handle about 50 connections per second. By doing the socket processing in a separate thread, establishing the connection becomes the rate-limiting step, and the server could handle about 1,000 connections per second with these example times.
Listing 17.20 shows how to convert the EchoServer into a multithreaded version. Section 16.4 (Creating a Multithreaded Method) discusses in detail the process for converting a single-threaded method to a multithreaded method. The basic idea is that the new version's handleConnection starts up a thread, which calls back to the original handleConnection. The problem is how to get the Socket object from handleConnection to run, because placing the Socket object in an instance variable would subject it to race conditions. So, a Connection class, which is simply a Thread with a place to store the Socket object, is used.
Listing 17.20 ThreadedEchoServer.java
import java.net.*; import java.io.*; /** A multithreaded variation of EchoServer. */ public class ThreadedEchoServer extends EchoServer implements Runnable { public static void main(String[] args) { int port = 8088; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch(NumberFormatException nfe) {} } ThreadedEchoServer echoServer = new ThreadedEchoServer(port, 0); echoServer.serverName = "Threaded EchoServer"; } public ThreadedEchoServer(int port, int connections) { super(port, connections); } /** The new version of handleConnection starts a thread. This * new thread will call back to the <I>old</I> version of * handleConnection, resulting in the same server behavior * in a multithreaded version. The thread stores the Socket * instance since run doesn't take any arguments, and since * storing the socket in an instance variable risks having * it overwritten if the next thread starts before the run * method gets a chance to copy the socket reference. */ public void handleConnection(Socket server) { Connection connectionThread = new Connection(this, server); connectionThread.start(); } public void run() { Connection currentThread = (Connection)Thread.currentThread(); try { super.handleConnection(currentThread.getSocket()); } catch(IOException ioe) { System.out.println("IOException: " + ioe); ioe.printStackTrace(); } } } /** This is just a Thread with a field to store a Socket object. * Used as a thread-safe means to pass the Socket from * handleConnection to run. */ class Connection extends Thread { private Socket serverSocket; public Connection(Runnable serverObject, Socket serverSocket) { super(serverObject); this.serverSocket = serverSocket; } public Socket getSocket() { return serverSocket; } }
This server gives the same results as the EchoServer but allows multiple simultaneous connections.