Book Image

Learning Network Programming with Java

Book Image

Learning Network Programming with Java

Overview of this book

Network-aware applications are becoming more prevalent and play an ever-increasing role in the world today. Connecting and using an Internet-based service is a frequent requirement for many applications. Java provides numerous classes that have evolved over the years to meet evolving network needs. These range from low-level socket and IP-based approaches to those encapsulated in software services. This book explores how Java supports networks, starting with the basics and then advancing to more complex topics. An overview of each relevant network technology is presented followed by detailed examples of how to use Java to support these technologies. We start with the basics of networking and then explore how Java supports the development of client/server and peer-to-peer applications. The NIO packages are examined as well as multitasking and how network applications can address practical issues such as security. A discussion on networking concepts will put many network issues into perspective and let you focus on the appropriate technology for the problem at hand. The examples used will provide a good starting point to develop similar capabilities for many of your network needs
Table of Contents (16 chapters)
Learning Network Programming with Java
Credits
About the Author
About the Reviewer
www.PacktPub.com
Preface
Index

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.