A simple client and server in Java: the "conversation" server-side

So far, we showed the skeleton of our simple Java server, which sits in a loop calling ServerSocket.accept(). This method gets woken up with a Socket object each time a client makes a connection, and we now need to process that connection.

In our example "calculation" server, whenever the server receives a connection from a client, it will expect the client to send it a string containing a command plus decimal number. Then the server will send back to the client either the result of the operation, if it could be performed, or if not, an error message. For the time being, we're going to send the data in string form from both ends. For example, to ask the server to calculate the square root of 4.4, it would send:

SQRT 4.4

and the server would send back the following line:

RESULT: 2.0976176963403033

String or binary data?

We'll be sending and receiving data from the socket via standard Java streams, so we can either send binary data, or put an appropriate wrapper object around the stream to send/receive textual data. We'll do the latter here. Sending in string form means that we can test the server with a telnet client before writing the client.

Step 1: getting the Socket input/output streams

The first couple of lines of our handleConnection() method will get the InputStream and OutputStream from which we can receive and send data over the connection. Because we're dealing with textual data, we will wrap the input stream in an InputStreamReader (which converts bytes to characters), then a BufferedReader (which buffers the data and handles line breaks). For writing the result, similarly wrap the output stream. For our simple example, the issue of character encoding actually isn't too crucial (we never expect to send any "exotic" characters beyond standard ASCII), but for other applications it is important to make sure that both client and server are "on the same wavelength" and send and receive characters in the same encoding (or in more sophisticated cases, can negotiate which encoding they're going to use on a given connection).

private static final String ENCODING = "ISO-8859-1";

private void handleConnection(Socket s) throws IOException {
  InputStream in = s.getInputStream();
  OutputStream out = s.getOutputStream();
  BufferedReader br = new BufferedReader(new InputStreamReader(in, ENCODING));
  out = new BufferedOutputStream(out);
  PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, ENCODING));
  ...
}

Step 2: reading and parsing the client's request

Now that we have our BufferedReader, this next step is actually fairly trivial. We expect the client to send a single line in the format above. So we simply use normal string manipulation methods to find the index of the space and separate the string into command and number, then parse the number. To make our program flow a little easier, we're going to take the slightly unusual step of explicitly throwing an exception if we encounter invalid syntax. Then, in a single try/catch block, we can catch either an exception that we throw, or one thrown from one of the other library methods that we're calling (e.g. if Double.parseDouble() detects an invalid number). This will actually make our program flow easier. If an IOException is thrown inside this block, we throw it up to the caller of our method (if we get an I/O error, there's a good chance we can't write an error message to the connection!), but for other exceptions, we write an error message line to the client. The code looks like this:

String line = br.readLine();
try {
  int ixSpace = line.indexOf(' ');
  if (ixSpace == -1)
    throw new Exception("Illegal syntax");
  String command = line.substring(0, ixSpace).trim();
  double d = Double.parseDouble(line.substring(ixSpace+1).trim());
  ... send result ...
} catch (IOException ioex) {
  throw ioex;
} catch (Exception ex) {
  pw.println("ERROR: " + ex.getMessage());
} finally {
  pw.close();
}

Closing/flushing the PrintWriter

Notice that in the above example, we close the PrintWriter in a finally clause. This will happen either after writing the result (in the case of success), or in the case of an error.

Parsing more complex requests

If we needed to parse more complex requests than our simple example, we could consider using regular expressions, which are supported in the standard Java API.

Step 3: sending the server's output to the client

Sending our server's output to the client is now straightforward. Anything printed to the PrintWriter will be sent down the socket to the client. So the following lines go in place of the "send result" comment above:

double result;
if ("SQRT".equalsIgnoreCase(command)) {
  result = Math.sqrt(d);
} else if ("SIN".equalsIgnoreCase(command)) {
  result = Math.sin(d);
} else {
  throw new Exception("Unrecognised command '" + command + "'");
}
pw.println("RESULT: " + result);

Testing our server

Because we're only sending and receiving textual data, we can test our server using a telnet client. While running our Java server, we can open a terminal window (in Windows, go to Start and type cmd), then type the following to connect to our server:

telnet localhost 80

Now if we type a line in the required format (e.g. "SQRT 100") and press RETURN, we should get back the server's response. (Note that you may not be able to see the line as you are typing it, because we're not actually implementing the telnet protocl properly, but that doesn't matter— the server should still receive the command and you should see the response sent back.)


Written by Neil Coffey. Copyright © Javamex UK 2008. All rights reserved.