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.
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.
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:
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:
There are several buffer classes that support primitive data types, such as character, integer, and float.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 } }
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 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.
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 } } }
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.
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.
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.