Learning Network Programming with Java

4.8 (4 reviews total)
By Richard M Reese
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Getting Started with Network Programming

About 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.

Publication date:
December 2015
Publisher
Packt
Pages
292
ISBN
9781785885471

 

Chapter 1. Getting Started with Network Programming

Access to networks (the Internet in particular) is becoming an important and often necessary feature of applications. Applications frequently need to access and provide services. As the Internet of Things (IoT) connects more and more devices, understanding how to access networks becomes crucial.

The important factors that have been the driving forces for more network applications include the availability of faster networks with greater bandwidth. This has made it possible to transmit wider ranges of data, such as video streams. In recent years, we have seen an increase in connectivity, whether it has been for new services, more extensive social interactions, or games. Knowing how to develop network applications is an important development skill.

In this chapter, we will cover the basics of Network programming:

  • Why networking is important

  • The support that Java provides

  • Simple programs to address basic network operations

  • Basic networking terminology

  • A simple server/client application

  • Using a thread to support a server

Throughout this book, you will be exposed to many network concepts, ideas, patterns, and implementation strategies using both older and newer Java technologies. Network connections occur at a low level using sockets, and at a much higher level using a multitude of protocols. Communications can be synchronous requiring careful coordination of requests and responses, or they can be asynchronous where other activities are performed until the response has been submitted.

These and other concepts are addressed through a series of chapters, each focusing on a specific topic. The chapters complement each other by elaborating on concepts that were previously introduced, whenever possible. Numerous code examples are used whenever possible to further your understanding of the topic.

Central to accessing a service is knowing or discovering its address. This address may be human readable, such as www.packtpub.com, or in the form of an IP address such as 83.166.169.231. Internet Protocol (IP) is a low-level addressing scheme that is used to access information on the Internet. Addressing has long used IPv4 to access resources. However, these addresses are all but gone. The newer IPv6 is available to provide a larger range of addresses. The basics of network addressing and how they can be managed in Java is the focus of Chapter 2, Network Addressing.

The intent of network communication is to transfer information to and from other applications. This is facilitated using buffers and channels. Buffers hold information temporarily until it can be processed by an application. Channels are an abstraction that simplifies communications between applications. The NIO and NIO.2 packages provide much of the support for buffers and channels. We will explore these techniques along with other techniques, such as blocking and non-blocking IO, in Chapter 3, NIO Support for Networking.

Services are provided by servers. An example of this is the simple echo server, which retransmits what it was sent. More sophisticated servers, such as HTTP servers, can support extensive services to meet a wide range of needs. The client/server model and its Java support are covered in Chapter 3, NIO Support for Networking.

Another service model is the peer-to-peer (P2P) model. In this architecture, there is no central server, but rather a network of applications that communicate to provide a service. This model is represented by applications, such as BitTorrent, Skype, and BBC's iPlayer. While much of the support that is required for the development of these types of applications is beyond the scope of this book, Chapter 4, Client/Server Development, explores P2P issues and the support provided by Java and JXTA.

IP is used at a low level to send and receive packets of information across a network. We will also demonstrate the use of User Datagram Protocol (UDP) and Transmission Control Protocol (TCP) communication protocols. These protocols are layered on top of IP. UDP is used to broadcast short packets or messages with no guarantee of reliable delivery. TCP is used more commonly and provides a higher level of service than that of UDP. We will cover the use of these related technologies in Chapter 5, Peer-to-Peer Networks.

A service will often be faced with varying levels of demand placed on it due to a number of factors. Its load may vary by the time of the day. As it becomes more popular, its overall demand will also increase. The server will need to scale to meet increases and decreases in its load. Threads and thread pools have been used to support this effort. These and other technologies are the focus of Chapter 6, UDP and Multicasting.

Increasingly, applications need to be secure against attacks by hackers. When it is connected to a network, this threat increases. In Chapter 7, Network Scalability, we will explore many of the techniques available to support secure Java applications. Among these is the Secure Socket Level (SSL), and how Java supports it.

Applications rarely work in isolation. Hence, they need to use networks to access other applications. However, not all applications are written in Java. Networking with these applications can pose special problems ranging from how the bytes of a data type are organized to the interface supported by the application. It is common to work with specialized protocols, such as HTTP, and WSDL. The last chapter of this book examines these issues from a Java perspective.

We will demonstrate both older and newer Java technologies. Understanding the older technologies may be necessary in order to maintain older code, and it can provide insight into why the newer technologies were developed. We will also complement our examples using many of the Java 8 functional programming techniques. Using Java 8 examples along with pre-Java 8 implementations, we can learn how to use Java 8 and be better informed as to when it can and should be used.

It is not the intent to fully explain the newer Java 8 technologies, such as lambda expressions, and streams. However, the use of Java 8 examples will provide an insight into how they can be used to support networked applications.

The remainder of this chapter touches on many of the network technologies that are explored in this book. You will be introduced to the basics of these techniques, and you should find them easy to understand. However, there are a few places where time does not permit us to fully explore and explain these concepts. These issues will be addressed in subsequent chapters. So, let's begin our exploration with network addressing.

 

Network addressing using the InetAddress class


An IP address is represented by the InetAddress class. Addresses can be either unicast where it identifies a specific address, or it can be multicast, where a message is sent to more than one address.

The InetAddress class has no public constructors. To get an instance, use one of the several static get type methods. For example, the getByName method takes a string representing the address as shown next. The string in this case is a Uniform Resource Locator (URL):

    InetAddress address = 
        InetAddress.getByName("www.packtpub.com");
    System.out.println(address);

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

This will display the following results:

www.packtpub.com/83.166.169.231

The number attached to the end of the name is the IP address. This address uniquely identifies an entity on the Internet.

If we need other information about the address, we can use one of several methods, as illustrated here:

    System.out.println("CanonicalHostName: " 
        + address.getCanonicalHostName());
    System.out.println("HostAddress: " + 
        address.getHostAddress());
    System.out.println("HostName: " + address.getHostName());

This produces the following output when executed:

CanonicalHostName: 83.166.169.231

HostAddress: 83.166.169.231

HostName: www.packtpub.com

To test to see whether this address is reachable, use the isReachable method as shown next. Its argument specifies how long to wait before deciding that the address cannot be reached. The argument is the number of milliseconds to wait:

    address.isReachable(10000);

There are also the Inet4Address and Inet6Address classes that support IPv4 and IPv6 addresses, respectively. We will explain their use in Chapter 2, Network Addressing.

Once we have obtained an address, we can use it to support network access, such as with servers. Before we demonstrate its use in this context, let's examine how we can obtain and process data from a connection.

 

NIO support


The java.io, java.nio, and java.nio subpackages provide most of the Java support for IO processing. We will examine the support that these packages provide for network access in Chapter 3, NIO Support for Networking. Here, we will focus on the basic aspects of the java.nio package.

There are three key concepts used in the NIO package:

  • Channel: This represents a stream of data between applications

  • Buffer: This works with a channel to process data

  • Selector: This is a technology that allows a single thread to handle multiple channels

A channel and a buffer are typically associated with each other. Data may be transferred from a channel to a buffer or from a buffer to a channel. The buffer, as its name implies, is a temporary repository for information. The selector is useful in supporting application scalability, and this will be discussed in Chapter 7, Network Scalability.

There are four primary channels:

  • FileChannel: This works with a file

  • DatagramChannel: This supports UDP communications

  • SocketChannel: This is used with a TCP client

  • ServerSocketChannel: This is used with a TCP server

There are several buffer classes that support primitive data types, such as character, integer, and float.

Using the URLConnection class

A simple way of accessing a server is to use the URLConnection class. This class represents a connection between an application and a URL instance. A URL instance represents a resource on the Internet.

In the next example, a URL instance is created for the Google website. Using the URL class' openConnection method, a URLConnection instance is created. A BufferedReader instance is used to read lines from the connection that is then displayed:

    try {
        URL url = new URL("http://www.google.com");
        URLConnection urlConnection = url.openConnection();
        BufferedReader br = new BufferedReader(
                new InputStreamReader(
                    urlConnection.getInputStream()));
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    } catch (IOException ex) {
        // Handle exceptions
    }

The output is rather lengthy, so only part of the first line is shown here:

<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" ...

The URLConnection class hides some of the complexity of accessing HTTP servers.

Using the URLConnection class with buffers and channels

We can rework the previous example to illustrate the use of channels and buffers. The URLConnection instance is created as before. We will create a ReadableByteChannel instance and then a ByteBuffer instance, as illustrated in the next example. The ReadableByteChannel instance allows us to read from the site using its read method. A ByteBuffer instance receives data from the channel and is used as the argument of the read method. The buffer created holds 64 bytes at a time.

The read method returns the number of bytes read. The ByteBuffer class' array method returns an array of bytes, which is used as the argument of the String class' constructor. This is used to display the data read. The clear method is used to reset the buffer so that it can be used again:

    try {
        URL url = new URL("http://www.google.com");
        URLConnection urlConnection = url.openConnection();
        InputStream inputStream = urlConnection.getInputStream();
        ReadableByteChannel channel = 
            Channels.newChannel(inputStream);
        ByteBuffer buffer = ByteBuffer.allocate(64);
        String line = null;
        while (channel.read(buffer) > 0) {
            System.out.println(new String(buffer.array()));
            buffer.clear();
        }
        channel.close();
    } catch (IOException ex) {
        // Handle exceptions
    }

The first line of output is shown next. This produces the same output as before, but it is restricted to displaying 64 bytes at a time:

<!doctype html><html itemscope="" itemtype="http://schema.org/We

The Channel class and its derived classes provide an improved technique to access data found on a network than data provided by older technologies. We will be seeing more of this class.

 

The client/server architecture


There are several ways of creating servers using Java. We will illustrate a couple of simple approaches and postpone a detailed discussion of these techniques until Chapter 4, Client/Server Development. Both a client and a server will be created.

A server is installed on a machine with an IP address. It is possible for more than one server to be running on a machine at any given time. When the operating system receives a request for a service on a machine, it will also receive a port number. The port number will identify the server to where the request should be forwarded. A server is, thus, identified by its combination of IP address and port number.

Typically, a client will issue a request to a server. The server will receive the request and send back a response. The nature of the request/response and the protocol used for communication is dependent on the client/server. Sometimes a well-documented protocol, such as the Hypertext Transfer Protocol (HTTP), is used. For simpler architectures, a series of text messages are sent back and forth.

For the server to communicate with an application making a request, specialized software is used to send and receive messages. This software is called a socket. One socket is found on the client side, and the other socket is located on the server side. When they connect, communication is possible. There are several different types of sockets. These include datagram sockets; stream sockets, which frequently use TCP; and raw sockets, which normally work at the IP level. We will focus on TCP sockets for our client/server application.

Specifically, we will create a simple echo server. This server will receive a text message from a client and will immediately send it back to that client. The simplicity of this server allows us to focus on the client-server basics.

 

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.

 

UDP and multicasting


Multicasting is a useful technique to use if you need to send messages to a group on a periodic basis. It uses a UDP server and one or more UDP clients. To illustrate this capability, we will create a simple time server. The server will send a date and time string to clients every second.

Multicasting will send an identical message to every member of a group. A group is identified by a multicast address. A multicast address must use the following IP address range: 224.0.0.0 through 239.255.255.255. The server will send a message mark with this address. Clients must join the group before they can receive any multicast messages.

Creating a multicast server

A MulticastServer class is declared next, where a DatagramSocket instance is created. The try-catch blocks will handle exceptions as they occur:

public class MulticastServer {
    public static void main(String args[]) {
        System.out.println("Multicast  Time Server");
        DatagramSocket serverSocket = null;
        try {
            serverSocket = new DatagramSocket();
            ...
            }
        } catch (SocketException ex) {
            // Handle exception
        } catch (IOException ex) {
            // Handle exception
        }
    }
}

The body of the try block uses an infinite loop to create an array of bytes to hold the current date and time. Next, an InetAddress instance representing the multicast group is created. Using the array and the group address, a DatagramPacket is instantiated and used as an argument to the DatagramSocket class' send method. The data and time sent is then displayed. The server then pauses for one second:

    while (true) {
        String dateText = new Date().toString();
        byte[] buffer = new byte[256];
        buffer = dateText.getBytes();

        InetAddress group = InetAddress.getByName("224.0.0.0");
        DatagramPacket packet;
        packet = new DatagramPacket(buffer, buffer.length, 
            group, 8888);
        serverSocket.send(packet);
        System.out.println("Time sent: " + dateText);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            // Handle exception
        }
    }

This server only broadcasts messages. It never receives messages from a client.

Creating the multicast client

The client is created using the following MulticastClient class. In order to receive a message, the client must use the same group address and port number. Before it can receive messages, it must join the group using the joinGroup method. In this implementation, it receives 5 date and time messages, displays them, and then terminates. The trim method removes leading and trailing white space, from a string. Otherwise, all 256 bytes of the message will be displayed:

public class MulticastClient {
    public static void main(String args[]) {
        System.out.println("Multicast  Time Client");
        try (MulticastSocket socket = new MulticastSocket(8888)) {
            InetAddress group = 
                InetAddress.getByName("224.0.0.0");
            socket.joinGroup(group);
            System.out.println("Multicast  Group Joined");

            byte[] buffer = new byte[256];
            DatagramPacket packet = 
                new DatagramPacket(buffer, buffer.length);

            for (int i = 0; i < 5; i++) {
                socket.receive(packet);
                String received = new String(packet.getData());
                System.out.println(received.trim());
            }
            
            socket.leaveGroup(group);
        } catch (IOException ex) {
            // Handle exception
        }
        System.out.println("Multicast  Time Client Terminated");
    }
}

When the server is started, the messages sent are displayed as shown here:

Multicast Time Server

Time sent: Thu Jul 09 13:19:49 CDT 2015

Time sent: Thu Jul 09 13:19:50 CDT 2015

Time sent: Thu Jul 09 13:19:51 CDT 2015

Time sent: Thu Jul 09 13:19:52 CDT 2015

Time sent: Thu Jul 09 13:19:53 CDT 2015

Time sent: Thu Jul 09 13:19:54 CDT 2015

Time sent: Thu Jul 09 13:19:55 CDT 2015

...

The client output will look similar to the following:

Multicast Time Client

Multicast Group Joined

Thu Jul 09 13:19:50 CDT 2015

Thu Jul 09 13:19:51 CDT 2015

Thu Jul 09 13:19:52 CDT 2015

Thu Jul 09 13:19:53 CDT 2015

Thu Jul 09 13:19:54 CDT 2015

Multicast Time Client Terminated

Note

If the example is executed on a Mac, you may receive an exception indicating that it cannot assign the requested address. This can be fixed by using the JVM option -Djava.net.preferIPv4Stack=true.

There are numerous other multicast capabilities, which will be explored in Chapter 6, UDP and Multicasting.

 

Scalability


When the demand on a server increases and decreases, it is desirable to change the resources dedicated to the server. The options available range from the use of manual threads to allow concurrent behavior to those embedded in specialized classes to handle thread pools and NIO channels.

Creating a threaded server

In this section, we will use threads to augment our simple echo server. The definition of the ThreadedEchoServer class is as follows. It implements the Runnable interface to create a new thread for each connection. The private Socket variable will hold the client socket for a specific thread:

public class ThreadedEchoServer implements Runnable {
    private static Socket clientSocket;

    public ThreadedEchoServer(Socket clientSocket) {
        this.clientSocket = clientSocket;
    }
    ...
}

Note

A thread is a block of code that executes concurrently with other blocks of code in an application. The Thread class supports threads in Java. While there are several ways of creating threads, one way is to pass an object that implements the Runnable interface to its constructor. When the Thread class' start method is invoked, the thread is created and the Runnable interface's run method executes. When the run method terminates, so does the thread.

Another way of adding the thread is to use a separate class for the thread. This can be declared separate from the ThreadedEchoServer class or as an inner class of the ThreadedEchoServer class. Using a separate class, better splits the functionality of the application.

The main method creates the server socket as before, but when a client socket is created, the client socket is used to create a thread, as shown here:

    public static void main(String[] args) {
        System.out.println("Threaded Echo Server");
        try (ServerSocket serverSocket = new ServerSocket(6000)) {
            while (true) {
                System.out.println("Waiting for connection.....");
                clientSocket = serverSocket.accept();
                ThreadedEchoServer tes = 
                    new ThreadedEchoServer(clientSocket);
                new Thread(tes).start();
            }

        } catch (IOException ex) {
            // Handle exceptions
        }
        System.out.println("Threaded Echo Server Terminating");
    }

The actual work is performed in the run method as shown next. It is essentially the same implementation as the original echo server, except that the current thread is displayed to clarify which threads are being used:

    @Override
    public void run() {
        System.out.println("Connected to client using [" 
            + Thread.currentThread() + "]");
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(
                    clientSocket.getInputStream()));
                PrintWriter out = new PrintWriter(
                        clientSocket.getOutputStream(), true)) {
            String inputLine;
            while ((inputLine = br.readLine()) != null) {
                System.out.println("Client request [" 
                    + Thread.currentThread() + "]: " + inputLine);
                out.println(inputLine);
            }
            System.out.println("Client [" + Thread.currentThread() 
                + " connection terminated");
        } catch (IOException ex) {
            // Handle exceptions
        }
    }

Using the threaded server

The following output shows the interaction between the server and two clients. The original echo client was started twice. As you can see, each client interaction is performed with a different thread:

Threaded Echo Server

Waiting for connection.....

Waiting for connection.....

Connected to client using [Thread[Thread-0,5,main]]

Client request [Thread[Thread-0,5,main]]: Hello from client 1

Client request [Thread[Thread-0,5,main]]: Its good on this side

Waiting for connection.....

Connected to client using [Thread[Thread-1,5,main]]

Client request [Thread[Thread-1,5,main]]: Hello from client 2

Client request [Thread[Thread-1,5,main]]: Good day!

Client request [Thread[Thread-1,5,main]]: quit

Client [Thread[Thread-1,5,main] connection terminated

Client request [Thread[Thread-0,5,main]]: So long

Client request [Thread[Thread-0,5,main]]: quit

The following interaction is from the first client's perspective:

Simple Echo Client

Waiting for connection.....

Connected to server

Enter text: Hello from client 1

Server response: Hello from client 1

Enter text: Its good on this side

Server response: Its good on this side

Enter text: So long

Server response: So long

Enter text: quit

Server response: quit

The following interaction is from the second client's perspective:

Simple Echo Client

Waiting for connection.....

Connected to server

Enter text: Hello from client 2

Server response: Hello from client 2

Enter text: Good day!

Server response: Good day!

Enter text: quit

Server response: quit

This implementation permits multiple clients to be handled at a time. Clients are not blocked because another client is using the server. However, it also allows a large number of threads to be created. If there are too many threads in existence, then server performance can degrade. We will address these issues in Chapter 7, Network Scalability.

 

Security


Security is a complex topic. In this section, we will demonstrate a few simple aspects of this topic, as it relates to networks. Specifically, we will create a secure echo server. Creating a secure echo server is not that much different from the non-secure echo server that we developed earlier. However, there is a lot going on behind the scenes to make it work. We can ignore many of these details for now, but we will delve more deeply into it in Chapter 8, Network Security.

We will be using the SSLServerSocketFactory class to instantiate secure server sockets. In addition, it is necessary to create keys that the underlying SSL mechanism can use to encrypt the communications.

Creating a SSL server

An SSLServerSocket class is declared in the following example to serve as the echo server. As it is similar to the previous echo server, we will not explain its implementation, except for its relation to the use of the SSLServerSocketFactory class. Its static getDefault method returns an instance of ServerSocketFactory. Its createServerSocket method returns an instance of a ServerSocket bound to port 8000 that is capable of supporting secure communications. Otherwise, it is organized and functions similarly to the previous echo server:

public class SSLServerSocket {

    public static void main(String[] args) {
        try {
            SSLServerSocketFactory ssf =  (SSLServerSocketFactory) 
                SSLServerSocketFactory.getDefault();
            ServerSocket serverSocket = 
                ssf.createServerSocket(8000);
            System.out.println("SSLServerSocket Started");
            try (Socket socket = serverSocket.accept();
                    PrintWriter out = new PrintWriter(
                            socket.getOutputStream(), true);
                    BufferedReader br = new BufferedReader(
                        new InputStreamReader(
                        socket.getInputStream()))) {
                System.out.println("Client socket created");
                String line = null;
                while (((line = br.readLine()) != null)) {
                    System.out.println(line);
                    out.println(line);
                }
                br.close();
                System.out.println("SSLServerSocket Terminated");
            } catch (IOException ex) {
                // Handle exceptions
            }
        } catch (IOException ex) {
            // Handle exceptions
        }
    }
}

Creating an SSL client

The secure echo client is also similar to the previous non-secure echo client. The SSLSocketFactory class' getDefault returns an SSLSocketFactory instance whose createSocket creates a socket that is connected to the secure echo server. The application is as follows:

public class SSLClientSocket {

    public static void main(String[] args) throws Exception {
        System.out.println("SSLClientSocket Started");
        SSLSocketFactory sf = 
            (SSLSocketFactory) SSLSocketFactory.getDefault();
        try (Socket socket = sf.createSocket("localhost", 8000);
                PrintWriter out = new PrintWriter(
                       socket.getOutputStream(), true);
                BufferedReader br = new BufferedReader(
                       new InputStreamReader(
                       socket.getInputStream()))) {
            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);
                System.out.println("Server response: " + 
                    br.readLine());
            }
            System.out.println("SSLServerSocket Terminated");
        }
    }
}

If we executed this server followed by the client, they will abort with a connection error. This is because we have not provided a set of keys that the applications can share and use to protect the data passed between them.

Generating secure keys

To provide the necessary keys, we need to create a keystore to hold the keys. When the applications execute, the keystore must be available to the applications. First, we will demonstrate how to create a keystore, and then we will show you which runtime parameters must be supplied.

Within the Java SE SDK's bin directory is a program titled keytool. This is a command-level program that will generate the necessary keys and store them in a key file. In Windows, you will need to bring up a command window and navigate to the root directory of your source files. This directory will contain the directory holding your application's package.

Note

On a Mac, you may have problems generating a key pair. More information about using this tool on a Mac is found at https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/keytool.1.html.

You will also need to set the path to the bin directory using a command that is similar to the following one. This command is needed to find and execute the keytool application:

    set path= C:\Program Files\Java\jdk1.8.0_25\bin;%path%

Next, enter the keytool command. You will be prompted for a password and other information that is used to create the keys. This process is shown here, where a password of 123456 is used although it is not displayed as it is entered:

Enter keystore password:
Re-enter new password:
What is your first and last name?
  [Unknown]:  First Last
What is the name of your organizational unit?
  [Unknown]:  packt
What is the name of your organization?
  [Unknown]:  publishing
What is the name of your City or Locality?
  [Unknown]:  home
What is the name of your State or Province?
  [Unknown]:  calm
What is the two-letter country code for this unit?
  [Unknown]:  me
Is CN=First Last, OU=packt, O=publishing, L=home, ST=calm, C=me correct?
  [no]:  y

Enter key password for <mykey>
        (RETURN if same as keystore password):

With the keystore created, you can run the server and client applications. How these applications are started depends on how your projects have been created. You may be able to execute it from an IDE, or you may need to start them from a command window.

Next are the commands that can be used from a command window. The two arguments to the java command are the location of the keystore and a password. They need to be executed from the root directory of your package's directory:

java -Djavax.net.ssl.keyStore=keystore.jks -Djavax.net.ssl.keyStorePassword=123456 packt.SSLServerSocket
java -Djavax.net.ssl.trustStore=keystore.jks -Djavax.net.ssl.trustStorePassword=123456 packt.SSLClientSocket

If you want to use an IDE, then use the equivalent settings for your runtime command arguments. The following one illustrates one possible interchange between the client and the server. The output of the server window is shown first, followed by that of the client:

SSLServerSocket Started

Client socket created

Hello echo server

Safe and secure

SSLServerSocket Terminated

SSLClientSocket Started

Enter text: Hello echo server

Server response: Hello echo server

Enter text: Safe and secure

Server response: Safe and secure

Enter text: quit

SSLServerSocket Terminated

There is more to learn about SSL than what is shown here. However, this provides an overview of the process with more details presented in Chapter 8, Network Security.

 

Summary


Network enabled applications fulfill an increasingly important role in our society today. With more and more devices being connected to the Internet, it is important to understand how to build applications that can communicate with other applications.

We briefly identified and explained several of the technologies that Java uses to connect to a network. We illustrated how the InetAddress class can represent an IP address, and we used this class for several examples. The basic elements of the client/server architecture were demonstrated using UDP, TCP, and SSL technologies. They provide different types of support. UDP is fast but not as reliable or as capable as TCP. TCP is a reliable and convenient way of communicating, but is not secure unless used with SSL.

The NIO support for buffers and channels was illustrated. These techniques can result in more efficient communications. The scalability of an application is critical for many applications, specifically the client/server model. We also saw how threads can support scalability.

Each of these topics will be addressed in more detail in later chapters. This includes the support NIO provides for scalability, how P2P applications work, and the myriad of interoperability technologies that are available for use with Java.

We'll start with a detailed examination of networks, and network addressing, in particular, in the next chapter.

About the Author

  • Richard M Reese

    Richard M Reese has worked in both industry and academia. For 17 years, he worked in the telephone and aerospace industries, serving in several capacities, including research and development, software development, supervision, and training. He currently teaches at Tarleton State University. Richard has written several Java books and a C Pointer book. He uses a concise and easy-to-follow approach to topics at hand. His Java books have addressed EJB 3.1, updates to Java 7 and 8, certification, functional programming, jMonkeyEngine, and natural language processing.

    Browse publications by this author

Latest Reviews

(4 reviews total)
The purchase was easy and fast, and the book was available immediately. No hassles, no inconveniences.
Can't beat a $5 sale price! Love the multiple format options and lack of annoying DRM.
Good book to start with. It has nice examples. Good reading.
Book Title
Unlock this book and the full library for FREE
Start free trial