Creating a simple echo server
We will start with the definition of the SimpleEchoServer
class as shown next. In the main
method, an initial server message will be displayed:
public class SimpleEchoServer { public static void main(String[] args) { System.out.println("Simple Echo Server"); ... } }
The remainder of the method's body consists of a series of try blocks to handle exceptions. In the first try block, a ServerSocket
instance is created using 6000
as its parameter. The ServerSocket
class is a specialized socket that is used by a server to listen for client requests. Its argument is its port number. The IP of the machine on which the server is located is not necessarily of interest to the server, but the client will ultimately need to know this IP address.
In the next code sequence, an instance of the ServerSocket
class is created and its accept
method is called. The ServerSocket
will block this call until it receives a request from a client. Blocking means that the program is suspended until the method returns. When a request is received, the accept
method will return a Socket
class instance, which represents the connection between that client and the server. They can now send and receive messages:
try (ServerSocket serverSocket = new ServerSocket(6000)){ System.out.println("Waiting for connection....."); Socket clientSocket = serverSocket.accept(); System.out.println("Connected to client"); ... } catch (IOException ex) { // Handle exceptions }
After this client socket has been created, we can process the message sent to the server. As we are dealing with text, we will use a BufferedReader
instance to read the message from the client. This is created using the client socket's getInputStream
method. We will use a PrintWriter
instance to reply to the client. This is created using the client socket's getOutputStream
method, shown as follows:
try (BufferedReader br = new BufferedReader( new InputStreamReader( clientSocket.getInputStream())); PrintWriter out = new PrintWriter( clientSocket.getOutputStream(), true)) { ... } }
The second argument to the PrintWriter
constructor is set to true
. This means that text sent using the out
object will automatically be flushed after each use.
When text is written to a socket, it will sit in a buffer until either the buffer is full or a flush method is called. Performing automatic flushing saves us from having to remember to flush the buffer, but it can result in excessive flushing, whereas a single flush issued after the last write is performed, will also do.
The next code segment completes the server. The readLine
method reads a line at a time from the client. This text is displayed and then sent back to the client using the out
object:
String inputLine; while ((inputLine = br.readLine()) != null) { System.out.println("Server: " + inputLine); out.println(inputLine); }
Before we demonstrate the server in action, we need to create a client application to use with it.
Creating a simple echo client
We start with the declaration of a SimpleEchoClient
class where in the main
method, a message is displayed indicating the application's start that is shown as follows:
public class SimpleEchoClient { public static void main(String args[]) { System.out.println("Simple Echo Client"); ... } }
A Socket
instance needs to be created to connect to the server. In the following example, it is assumed that the server and the client are running on the same machine. The InetAddress
class' static getLocalHost
method returns this address, which is then used in the Socket
class's constructor along with port 6000
. If they are located on different machines, then the server's address needs to be used instead. As with the server, an instance of the PrintWriter
and BufferedReader
classes are created to allow text to be sent to and from the server:
try { System.out.println("Waiting for connection....."); InetAddress localAddress = InetAddress.getLocalHost(); try (Socket clientSocket = new Socket(localAddress, 6000); PrintWriter out = new PrintWriter( clientSocket.getOutputStream(), true); BufferedReader br = new BufferedReader( new InputStreamReader( clientSocket.getInputStream()))) { ... } } catch (IOException ex) { // Handle exceptions }
Note
Localhost refers to the current machine. This has a specific IP address: 127.0.0.1
. While a machine may be associated with an additional IP address, every machine can reach itself using this localhost address.
The user is then prompted to enter text. If the text is the quit command, then the infinite loop is terminated, and the application shuts down. Otherwise, the text is sent to the server using the out
object. When the reply is returned, it is displayed as shown next:
System.out.println("Connected to server"); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("Enter text: "); String inputLine = scanner.nextLine(); if ("quit".equalsIgnoreCase(inputLine)) { break; } out.println(inputLine); String response = br.readLine(); System.out.println("Server response: " + response); }
These programs can be implemented as two separate projects or within a single project. Either way, start the server first and then start the client. When the server starts, you will see the following displayed:
Simple Echo Server
Waiting for connection.....
When the client starts, you will see the following:
Simple Echo Client
Waiting for connection.....
Connected to server
Enter text:
Enter a message, and watch how the client and the server interact. The following is one possible series of input from the client's perspective:
Enter text: Hello server
Server response: Hello server
Enter text: Echo this!
Server response: Echo this!
Enter text: quit
The server's output is shown here after the client has entered the quit
command:
Simple Echo Server
Waiting for connection.....
Connected to client
Client request: Hello server
Client request: Echo this!
This is one approach to implement the client and server. We will enhance this implementation in later chapters.
Using Java 8 to support the echo server and client
We will be providing examples of using many of the newer Java 8 features throughout this book. Here, we will show you alternative implementations of the previous echo server and client applications.
The server uses a while loop to process a client's request as duplicated here:
String inputLine; while ((inputLine = br.readLine()) != null) { System.out.println("Client request: " + inputLine); out.println(inputLine); }
We can use the Supplier
interface in conjunction with a Stream
object to perform the same operation. The next statement uses a lambda expression to return a string from the client:
Supplier<String> socketInput = () -> { try { return br.readLine(); } catch (IOException ex) { return null; } };
An infinite stream is generated from the Supplier
instance. The following map
method gets input from the user and then sends it to the server. When quit
is entered, the stream will terminate. The allMatch
method is a short-circuit method, and when its argument evaluates to false
, the stream is terminated:
Stream<String> stream = Stream.generate(socketInput); stream .map(s -> { System.out.println("Client request: " + s); out.println(s); return s; }) .allMatch(s -> s != null);
While this implementation is lengthier than the traditional implementation, it can provide more succinct and simple solutions to more complex problems.
On the client side, we can replace the while loop as duplicated here with a functional implementation:
while (true) { System.out.print("Enter text: "); String inputLine = scanner.nextLine(); if ("quit".equalsIgnoreCase(inputLine)) { break; } out.println(inputLine); String response = br.readLine(); System.out.println("Server response: " + response); }
The functional solution also uses a Supplier
instance to capture console input as shown here:
Supplier<String> scannerInput = () -> scanner.next();
An infinite stream is generated, as shown next, with a map
method providing the equivalent functionality:
System.out.print("Enter text: "); Stream.generate(scannerInput) .map(s -> { out.println(s); System.out.println("Server response: " + s); System.out.print("Enter text: "); return s; }) .allMatch(s -> !"quit".equalsIgnoreCase(s));
A functional approach is often a better solution to many problems.
Note that an additional prompt, Enter text:, was displayed on the client side after the quit
command was entered. This is easily corrected by not displaying the prompt if the quit
command was entered. This correction is left as an exercise for the reader.