Reader small image

You're reading from  Java Coding Problems - Second Edition

Product typeBook
Published inMar 2024
PublisherPackt
ISBN-139781837633944
Edition2nd Edition
Right arrow
Author (1)
Anghel Leonard
Anghel Leonard
author image
Anghel Leonard

Anghel Leonard is a Chief Technology Strategist and independent consultant with 20+ years of experience in the Java ecosystem. In daily work, he is focused on architecting and developing Java distributed applications that empower robust architectures, clean code, and high-performance. Also passionate about coaching, mentoring and technical leadership. He is the author of several books, videos and dozens of articles related to Java technologies.
Read more about Anghel Leonard

Right arrow

Socket API and Simple Web Server

This chapter includes 11 problems covering the Socket API and 8 problems covering JDK 18 Simple Web Server (SWS). In the first 11 problems we will discuss implementing socket-based applications such as blocking/non-blocking server/client applications, datagram-based applications, and multicast applications. In the second part of this chapter, we discuss SWS as a command-line tool and a suite of API points.

At the end of this chapter, you’ll know how to write applications via the Socket API and how to use SWS for testing, debugging, and prototyping tasks.

Problems

Use the following problems to test your advanced programming prowess in Socket API and SWS. I strongly encourage you to give each problem a try before you turn to the solutions and download the example programs:

  1. Introducing socket basics: Provide a brief but meaningful introduction to socket basics and related context (TCP, UDP, IP, etc.).
  2. Introducing TCP server/client applications: Introduce the knowledge needed for writing a blocking/non-blocking TCP server/client application.
  3. Introducing the Java Socket API: Highlight the main Socket API (NIO.2) needed for writing socket-based applications in Java.
  4. Writing a blocking TCP server/client application: Provide a detailed example (theory and code) of a blocking TCP server/client application.
  5. Writing a non-blocking TCP server/client application: Provide a detailed example (theory and code) of a non-blocking TCP server/client application.
  6. Writing UDP server/client applications: Write a...

258. Introducing socket basics

The socket notion was introduced in the ‘80s. This notion was introduced on Berkeley Software Distribution (BSD) (a Unix flavor) as a solution for network communication between processes via Internet Protocol (IP). Java introduced its first cross-platform API for sockets in 1996 (JDK 1.0). As you’ll see soon, with just a few notions such as network interface, IP address and port, a Java developer can write applications that communicate via sockets.

At the IP level, data travels from source to destination in chunks (packets) of data. Each packet is seen as an individual entity and there is no guarantee that all packets sent from a source will hit the destination. Nevertheless, on top of IP, we have other protocols that are more popular such as Transmission Control Protocol (TCP) and User Datagram Protocol (UDP). Moreover, on top of these protocols, we have the well-known HTTP, DNS, Telnet, and so on. Machine communication via sockets...

259. Introducing TCP server/client applications

We don’t need to be TCP experts in order to write a Java server/client TCP-based application. While this topic (TCP) is detailed (very well-documented) in dedicated books and articles, let’s have a brief overview of TCP principles.

TCP’s goal is to provide a point-to-point communication mechanism between two endpoints. Once the connection between these two endpoints is established (via sockets) it remains open during the communication until one of the sides closes it (usually, the client). In other words, two processes that are on different machines or the same machine can communicate with each other as in a telephone connection. In the following figure, you can see a classical server-client session based on sockets:

Figure 13.1.png

Figure 13.1: Server/client session based on sockets (TCP)

A server/client TCP connection is represented by certain coordinates as follows:

  • The server side is represented by...

260. Introducing the Java Socket API

Java Socket API support in Java has constantly evolved from JDK 1.0 to JDK 7. Starting with JDK 7 and NIO.2, sockets support has been seriously improved with a new API (new classes and interfaces) for easily writing complex TCP/UDP-based applications. For instance, the NetworkChannel interface was introduced as a common implementation point for all network channel classes. Any class that implements NetworkChannel has access to common methods useful for handling channels to network sockets. Such classes are SocketChannel, ServerSocketChannel, and DatagramChannel. These classes take advantage of methods for handling local addresses and for configuring socket options via SocketOption<T> (interface) and StandardSocketOptions (class). Moreover, this API exposes methods for accessing remote addresses, checking connection status, and shutting down sockets.

One of the most important subinterfaces of NetworkChannel is MulticastChannel. This interface...

261. Writing a blocking TCP server/client application

In this problem, we will write a blocking TCP server/client application. More precisely, let’s start with a single-thread blocking TCP echo server.

Writing a single-thread blocking TCP echo server

In order to write a single-thread blocking TCP echo server, we will follow these steps:

  1. Create a new server socket channel
  2. Configure the blocking mechanism
  3. Set the server socket channel options
  4. Bind the server socket channel
  5. Accept connections
  6. Transmit data over a connection
  7. Close the channel

So, let’s start with the first step.

Creating a new server socket channel

Creating and opening a new server socket channel (stream-oriented listening socket) can be done via the thread-safe java.nio.channels.ServerSocketChannel API as follows:

ServerSocketChannel serverSC = ServerSocketChannel.open();

The resulting server socket channel is not bound/connected...

262. Writing a non-blocking TCP server/client application

In this problem, we will write a non-blocking TCP server/client application.

A non-blocking socket (or a socket in non-blocking mode) allows us to perform I/O operations on socket channels without blocking the processes that are using it. The major steps of a non-blocking application are exactly the same as for a blocking application. The server is opened and bound to a local address ready to handle incoming clients. A client is opened and connected to the server. From this point forward, the server and the client can exchange data packets in a non-blocking fashion.

When we refer to exchanging data in a non-blocking fashion, we refer to the pillar of non-blocking technology, which is the java.nio.channels.Selector class. The role of the Selector is to orchestrate data transfer across multiple available socket channels. Basically, a Selector can monitor every single recorded socket channel and detect the channels that...

263. Writing UDP server/client applications

UDP is a protocol built on top of IP. Via UDP, we can send data packets of at most 65,507 bytes (that is, 65,535-byte IP packet size – plus the minimum IP header of 20 bytes – plus the 8-byte UDP header = 65,507 bytes total). In UDP, data packets are seen as individual entities. In other words, no packet is aware of others. Data packets may arrive in any order or may not arrive at all. The sender will not be informed about the lost packets, so it will not know what to resend. Moreover, data packets may arrive too fast or too slow, so processing them may be a real challenge.

While TCP is famous for high-reliability data transmissions, UDP is famous for low-overhead transmissions. So, UDP is more like sending a letter (remember that TCP is like a phone call). You write on the envelope the address of the receiver (here, the remote IP and port) and your address (here, local IP and port) and send it (here, over the wires). You...

264. Introducing multicasting

Multicasting is like a flavor of internet broadcasting. We know that a TV station can broadcast (share) its signal from the source to all its subscribers or to everybody in a certain area. The exceptions are represented by the people who don’t have the proper receiver (equipment) or aren’t interested in this TV station.

From a computer perspective, the TV station can be considered a source that sends datagrams to a group of listeners/subscribers or simply destination hosts. While point-to-point communication is possible via the unicast transport service, in multicasting (sending datagrams from a source to multiple destinations in a single call) we have the multicast transport service. In the case of the unicast transport service, sending the same data to multiple points is possible via the so-called replicated unicast (practically each point receives a copy of the data).

In multicasting terms, the receivers of multicasted datagrams...

265. Exploring network interfaces

In Java, a network interface is represented by the NetworkInterface API. Basically, a network interface is identified by a name and a list of IPs that are assigned to it. Via this information, we can associate a network interface with different network tasks such as a multicast group.

The following snippet of code lists all network interfaces that are available on your machine:

public class Main {
  public static void main(String[] args) 
         throws SocketException {
    Enumeration allNetworkInterfaces
      = NetworkInterface.getNetworkInterfaces();
    while (allNetworkInterfaces.hasMoreElements()) {
      NetworkInterface ni = (NetworkInterface) 
        allNetworkInterfaces.nextElement();
      System.out.println("\nDisplay Name: " 
        + ni.getDisplayName());
      System.out.println(ni.getDisplayName() 
        + " is up and running ? " + ni.isUp());
      System.out.println(ni.getDisplayName() 
       ...

266. Writing a UDP multicast server/client application

In Problem 263, we developed a UDP server/client application. So, based on that experience, we can go further and highlight the main aspects that can transform a classical UDP-based application into a multicast one.

For instance, let’s assume that we want to write a multicast server that sends to the group (to all members interested in receiving datagrams from this server) a datagram that encapsulates the current date-time on the server. This datagram is sent every 10 seconds.

Writing a UDP multicast server

Writing a UDP multicast server starts with a new DatagramChannel instance obtained via the open() method. Next, we set the IP_MULTICAST_IF option (used to indicate the multicast network interface) and the SO_REUSEADDR option (used to allow multiple members to bind to the same address – this should be done before binding the socket):

private static final String 
  MULTICAST_NI_NAME = "ethernet_32775...

267. Adding KEM to a TCP server/client application

In this problem, we attempt to write a TCP server/client application that communicates with each other via encrypted messages. The server side is referred to as the sender and the client as the receiver.

In this context, a sender can encrypt a message using its private key, and the receiver decrypts it using the sender’s public key. In case you didn’t recognize this scenario, then allow me to mention that we are talking about Authenticated Key Exchange (AKE) within Public Key Encryption (PKE) or, in short, about encrypting/decrypting messages based on the key exchange algorithms.

AKE within PKE is a popular choice, but it is not secure. In other words, AKE vulnerabilities can be speculated by quantum computers that are capable of altering most key exchange algorithms. JDK 21 can prevent such issues via the newly introduced KEM (https://en.wikipedia.org/wiki/Key_encapsulation_mechanism). This is a final feature...

268. Reimplementing the legacy Socket API

The Socket API has been improved over time and it is still receiving attention for potential further improvements.

Prior to JDK 13, the Socket API (java.net.ServerSocket and java.net.Socket) relied on PlainSocketImpl. Starting with JDK 13 (JEP 353), this API has been replaced by NioSocketImpl.

As its name suggests, NioSocketImpl is based on the NIO infrastructure. The new implementation doesn’t rely on the thread stack being capable of taking advantage of buffer cache mechanisms. Moreover, sockets can be closed via the java.lang.ref.Cleaner mechanism, which gives special attention to how socket objects are garbage collected.

Starting with JDK 15 (JEP 373, follow-on of JEP 353), the internal Socket API has been reimplemented at DatagramSocket and MulticastSocket APIs. The goal was to make these APIs simpler and easier to adapt to work with Project Loom (virtual threads).

Whenever you prefer to go for the old PlainSocketImpl...

269. Quick overview of SWS

SWS was added in JDK 18 under JEP 408 in the jdk.httpserver module. Basically, SWS is a minimalist implementation of a static file server capable of serving a single directory hierarchy. If the request points to a file, then SWS will serve that file. If the request points to a directory containing an index file, then the index file will be served; otherwise, the directory content will be listed.

SWS is very easy to set up and is available as a command-line tool (jwebserver) and as a suite of programmatic API points (com.sun.net.httpserver). SWS supports HTTP 1.1 only (not HTTPS or HTTP/2) and it can respond only to the idempotent HEAD and GET requests (any other request type will return a 405 or 501 HTTP code).

In addition, MIME types are set up automatically by SWS and no security mechanism (for instance, OAuth) is available.

Key abstractions of SWS

To better understand how SWS works behind the scenes, consider the following diagram:

...

270. Exploring the SWS command-line tool

The only prerequisite for running the SWS command-line tool (jwebserver) is JDK 18 and the following syntax:

Figure 13.5.png

Figure 13.7: jwebserver command-line tool syntax

The options of jwebserver are straightforward. Here is a short description of the most useful ones:

  • -b addr: This is the binding address. It defaults to the loopback, 127.0.0.1 or ::1. For all interfaces, we can use -b 0.0.0.0 or -b ::. The –b addr is similar to--bind-address addr.
  • -p port: This specifies the port on which the SWS will listen for incoming requests. The default port is 8000. The –p port option is similar to--port port.
  • -d dir: The dir variable points out the directory to be served. The default is the current directory. The –d dir is similar to--directory dir.
  • -o level: The level variable can be none, info (default), or verbose and it specifies the output format. The –o level is similar to --output level...

271. Introducing the com.sun.net.httpserver API

Since 2006, next to the SWS command-line tool, we have the programmatic bridge represented by the com.sun.net.httpserver API. Practically, the goal of this API is to allow us to programmatically launch an SWS instance in a very easy way.

First, we have the SimpleFileServer, which is the main API for creating an SWS instance via three static methods, createFileServer(), createFileHandler(), and createOutputFilter(). We are especially interested in the createFileServer(InetSocketAddress addr, Path rootDirectory, SimpleFileServer.OutputLevel outputLevel) method. As you can see, via this method, we can create an SWS instance with a given port, root directory (this should be an absolute path), and output level as follows:

HttpServer sws = SimpleFileServer.createFileServer(
  new InetSocketAddress(9009),
  Path.of("./docs").toAbsolutePath(),
  OutputLevel.VERBOSE);
sws.start();

After running this application, an SWS...

272. Adapting request/exchange

Adapting a request can be useful for testing and debugging purposes. Practically, we can adapt the request (com.sun.net.httpserver.Request) before the handler sees it, so we can modify the initial request and pass the result to the handler. For this, we can rely on the pre-processing Filter.adaptRequest(String description, UnaryOperator<Request> requestOperator) method. Besides the description, this method gets the effective request state of the exchange as UnaryOperator<Request>.

Here is an example that adds to each request the header Author next to a post-processing filter that logs the request details to the console:

HttpHandler fileHandler = ...;
Filter preFilter = Filter.adaptRequest(
  "Add 'Author' header", r -> r.with(
    "Author", List.of("Anghel Leonard")));
Filter postFilter = SimpleFileServer.createOutputFilter(
  out, SimpleFileServer.OutputLevel.VERBOSE);
HttpServer sws = HttpServer...

273. Complementing a conditional HttpHandler with another handler

Let’s assume that we want to choose between two HttpHandler instances based on a condition. For instance, for all GET requests, we want to use the following well-known HttpHandler:

HttpHandler fileHandler = SimpleFileServer.createFileHandler(
  Path.of("./docs").toAbsolutePath()); 

For all other requests, we want to use an HttpHandler that always returns the same resource (for instance, the text No data available). Defining a HttpHandler that always returns the same code and resource (so, a canned response) can be done via the HttpHandlers.of(int statusCode, Headers headers, String body) method as in the following example:

HttpHandler complementHandler = HttpHandlers.of(200,   
  Headers.of("Content-Type", "text/plain"), 
    "No data available");

In addition, the HttpHandler class exposes a method that can decide between two HttpHandler instances based...

274. Implementing SWS for an in-memory file system

We already know that SWS can serve files from the default local file system. While this file system fits many scenarios, there are also use cases (for instance, testing scenarios) where it will be more practical to mock a directory structure in order to simulate certain expectations. In such scenarios, an in-memory file system will be more suitable than the local file system since we can avoid the creation/deletion of resources and we can use different platforms.

An in-memory file system implementation for Java 8 (based on the java.nio.file API) is provided by the Google project named Jimfs (https://github.com/google/jimfs). By following the instructions from the GitHub example, we wrote the following code for a simple in-memory file system:

private static Path inMemoryDirectory() throws IOException {
  FileSystem fileSystem
    = Jimfs.newFileSystem(Configuration.forCurrentPlatform());
  Path docs = fileSystem.getPath("...

275. Implementing SWS for a zip file system

Using a ZIP file system can also be a common use case for SWS. The following snippet of code creates a ZIP file system via the java.nio.file API and returns the corresponding path:

private static Path zipFileSystem() throws IOException {
  Map<String, String> env = new HashMap<>();
  env.put("create", "true");
  Path root = Path.of("./zips").toAbsolutePath();
  Path zipPath = root.resolve("docs.zip")
    .toAbsolutePath().normalize();
  FileSystem zipfs = FileSystems.newFileSystem(zipPath, env);
  Path externalTxtFile = Paths.get("./docs/books.txt");
  Path pathInZipfile = zipfs.getPath("/bookszipped.txt");
  // copy a file into the zip file
  Files.copy(externalTxtFile, pathInZipfile,
             StandardCopyOption.REPLACE_EXISTING);
  return zipfs.getPath("/");
}

The result is an archive named docs.zip with a single file named bookszipped.txt—...

276. Implementing SWS for a Java runtime directory

Starting with JDK 9 (JEP 220), the runtime images have been restructured to support modules and become more performant and secure. Moreover, naming stored modules, classes, and resources has received a new URI scheme (jrt). Via the jrt scheme, we can reference modules, classes, and resources contained in runtime images without touching the internal structure of the image. A jrt URL looks as follows:

jrt:/[$MODULE[/$PATH]]

Here, $MODULE is a module name (optional) and $PATH (optional) represents the path to a certain class/resource file within that module. For instance, to point out the File class, we write the following URL:

jrt:/java.base/java/io/File.class

In the jrt file system, there is a top-level modules directory that contains one subdirectory for each module in the image. So, we can fetch the proper path for SWS as follows:

private static Path jrtFileSystem() {
  URI uri = URI.create("jrt...

Summary

This chapter covered 19 problems with Socket API and SWS. In the first part of this chapter, we covered NIO.2 features dedicated to TCP/UDP server/client applications. In the second part, we covered JDK 18 SWS as a command-line tool and as a suite of API points.

Leave a review!

Enjoyed this book? Help readers like you by leaving an Amazon review. Scan the QR code below for a 20% discount code.

*Limited Offer

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Java Coding Problems - Second Edition
Published in: Mar 2024Publisher: PacktISBN-13: 9781837633944
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Anghel Leonard

Anghel Leonard is a Chief Technology Strategist and independent consultant with 20+ years of experience in the Java ecosystem. In daily work, he is focused on architecting and developing Java distributed applications that empower robust architectures, clean code, and high-performance. Also passionate about coaching, mentoring and technical leadership. He is the author of several books, videos and dozens of articles related to Java technologies.
Read more about Anghel Leonard