In this chapter we’ll show you how to build a Netty-based client and server. The applications are simple—the client sends messages to the server, and the server echoes them back—but the exercise is important for two reasons.
First, it will provide a test bed for setting up and verifying your development tools and environment, which is essential if you plan to work with the book’s sample code in preparation for your own development efforts.
Second, you’ll acquire hands-on experience with a key aspect of Netty, touched on in the previous chapter: building application logic with ChannelHandlers. This will prepare you for the in-depth study of the Netty API we’ll begin in chapter 3.
To compile and run the book’s examples, the only tools you need are the JDK and Apache Maven, both freely available for download.
We’ll also assume that you’re going to want to tinker with the example code and soon start writing your own. Although you can get by with a plain text editor, we strongly recommend the use of an integrated development environment (IDE) for Java.
Your OS may already have a JDK installed. To find out, type the following on the command line:
javac -version
If you get back javac 1.7... or 1.8... you’re all set and can skip this step.[1]
A restricted feature set of Netty will run with JDK 1.6 but JDK 7 or higher is required for compilation, as well as for running the latest version of Maven.
Otherwise, get version 8 of the JDK from http://java.com/en/download/manual.jsp. Be careful to download the JDK and not the Java Runtime Environment (JRE), which can run Java applications but not compile them. An installer executable is provided for each platform. Should you need installation instructions, you’ll find them on the same site.
It’s a good idea to do the following:
The following are the most widely used Java IDEs, all freely available:
All three have full support for Apache Maven, the build tool we’ll use. NetBeans and Intellij are distributed as installer executables. Eclipse is usually distributed as a zip archive, although there are a number of customized versions that have self-installers.
Even if you’re already familiar with Maven, we recommend that you at least skim this section.
Maven is a widely used build-management tool developed by the Apache Software Foundation (ASF). The Netty project uses it, as do this book’s examples. You don’t need to be a Maven expert to build and run the examples, but if you want to expand on them, we recommend reading the Maven introduction in the appendix.
Eclipse and NetBeans come with an embedded Maven installation that will work fine for our purposes out of the box. If you’ll be working in an environment that has its own Maven repository, your administrator probably has a Maven installation package preconfigured to work with it.
At the time of this book’s publication, the latest Maven version was 3.3.3. You can download the appropriate tar.gz or zip file for your system from http://maven.apache.org/download.cgi. Installation is simple: extract the contents of the archive to any folder of your choice (we’ll call this <install_dir>). This will create the directory <install_dir>apache-maven-3.3.3.
As with the Java environment,
This will enable you to run Maven by executing mvn.bat (or mvn) on the command line.
If you have set the JAVA_HOME and M2_HOME system variables as recommended, you may find that when you start your IDE it has already discovered the locations of your Java and Maven installations. If you need to perform manual configuration, all the IDE versions we’ve listed have menu items for setting these variables under Preferences or Settings. Please consult the documentation for details.
This completes the setup of your development environment. In the next sections we’ll present the details of the first Netty applications you’ll build, and we’ll get deeper into the framework APIs. After that you’ll use the tools you’ve just set up to build and run the Echo server and client.
Figure 2.1 presents a high-level view of the Echo client and server you’ll be writing. While your main focus may be writing web-based applications to be accessed by browsers, you’ll definitely gain a more complete understanding of the Netty API by implementing both the client and server.
Although we’ve spoken of the client, the figure shows multiple clients connected simultaneously to the server. The number of clients that can be supported is limited, in theory, only by the system resources available (and any constraints that might be imposed by the JDK version in use).
The interaction between an Echo client and the server is very simple; after the client establishes a connection, it sends one or more messages to the server, which in turn echoes each message to the client. While this may not seem terribly useful by itself, it exemplifies the request-response interaction that’s typical of client/server systems.
We’ll begin this project by examining the server-side code.
All Netty servers require the following:
In the remainder of this section we’ll describe the logic and bootstrapping code for the Echo server.
In chapter 1 we introduced Futures and callbacks and illustrated their use in an event-driven design. We also discussed ChannelHandler, the parent of a family of interfaces whose implementations receive and react to event notifications. In Netty applications, all data-processing logic is contained in implementations of these core abstractions.
Because your Echo server will respond to incoming messages, it will need to implement interface ChannelInboundHandler, which defines methods for acting on inbound events. This simple application will require only a few of these methods, so it will be sufficient to subclass ChannelInboundHandlerAdapter, which provides a default implementation of ChannelInboundHandler.
The following methods interest us:
The Echo server’s ChannelHandler implementation is EchoServerHandler, shown in the following listing.
ChannelInboundHandlerAdapter has a straightforward API, and each of its methods can be overridden to hook into the event lifecycle at the appropriate point. Because you need to handle all received data, you override channelRead(). In this server you simply echo the data to the remote peer.
Overriding exceptionCaught() allows you to react to any Throwable subtypes—here you log the exception and close the connection. A more elaborate application might try to recover from the exception, but in this case simply closing the connection signals to the remote peer that an error has occurred.
Every Channel has an associated ChannelPipeline, which holds a chain of ChannelHandler instances. By default, a handler will forward the invocation of a handler method to the next one in the chain. Therefore, if exceptionCaught() is not implemented somewhere along the chain, exceptions received will travel to the end of the ChannelPipeline and will be logged. For this reason, your application should supply at least one ChannelHandler that implements exceptionCaught(). (Section 6.4 discusses exception handling in detail.)
In addition to ChannelInboundHandlerAdapter, there are many ChannelHandler subtypes and implementations to learn about, and we’ll cover these in detail in chapters 6 and 7. For now, please keep these key points in mind:
Having discussed the core business logic implemented by EchoServerHandler, we can now examine the bootstrapping of the server itself, which involves the following:
In this section you’ll encounter the term transport. In the standard, multilayered view of networking protocols, the transport layer is the one that provides services for end-to-end or host-to-host communications.
Internet communications are based on the TCP transport. NIO transport refers to a transport that’s mostly identical to TCP except for server-side performance enhancements provided by the Java NIO implementation.
Transports will be discussed in detail in chapter 4.
The following listing shows the complete code for the EchoServer class.
In you create a ServerBootstrap instance. Because you’re using the NIO transport, you specify the NioEventLoopGroup to accept and handle new connections and the NioServerSocketChannel as the channel type. After this you set the local address to an InetSocketAddress with the selected port . The server will bind to this address to listen for new connection requests.
In you make use of a special class, ChannelInitializer. This is key. When a new connection is accepted, a new child Channel will be created, and the ChannelInitializer will add an instance of your EchoServerHandler to the Channel’s ChannelPipeline. As we explained earlier, this handler will receive notifications about inbound messages.
Although NIO is scalable, its proper configuration, especially as regards multithreading, is not trivial. Netty’s design encapsulates most of the complexity, and we’ll discuss the relevant abstractions (EventLoopGroup, SocketChannel, and ChannelInitializer) in more detail in chapter 3.
Next you bind the server and wait until the bind completes. (The call to sync() causes the current Thread to block until then.) At , the application will wait until the server’s Channel closes (because you call sync() on the Channel’s CloseFuture). You can then shut down the EventLoopGroup and release all resources, including all created threads .
NIO is used in this example because it’s currently the most widely used transport, thanks to its scalability and thoroughgoing asynchrony. But a different transport implementation could be used as well. If you wished to use the OIO transport in your server, you’d specify OioServerSocketChannel and OioEventLoopGroup. We’ll explore transports in greater detail in chapter 4.
In the meantime, let’s review the important steps in the server implementation you just completed. These are the primary code components of the server:
The following steps are required in bootstrapping:
At this point the server is initialized and ready to be used. In the next section we’ll examine the code for the client application.
The Echo client will
1. Connect to the server
2. Send one or more messages
3. For each message, wait for and receive the same message back from the server
4. Close the connection
Writing the client involves the same two main code areas you saw in the server: business logic and bootstrapping.
Like the server, the client will have a ChannelInboundHandler to process the data. In this case, you’ll extend the class SimpleChannelInboundHandler to handle all the needed tasks, as shown in listing 2.3. This requires overriding the following methods:
First you override channelActive(), invoked when a connection has been established. This ensures that something is written to the server as soon as possible, which in this case is a byte buffer that encodes the string "Netty rocks!".
Next you override the method channelRead0(). This method is called whenever data is received. Note that the message sent by the server may be received in chunks. That is, if the server sends 5 bytes, there’s no guarantee that all 5 bytes will be received at once. Even for such a small amount of data, the channelRead0() method could be called twice, first with a ByteBuf (Netty’s byte container) holding 3 bytes, and second with a ByteBuf holding 2 bytes. As a stream-oriented protocol, TCP guarantees that the bytes will be received in the order in which they were sent by the server.
The third method you override is exceptionCaught(). Just as in EchoServerHandler (listing 2.2), Throwable is logged and the channel is closed, in this case terminating the connection to the server.
You may be wondering why we used SimpleChannelInboundHandler in the client instead of the ChannelInboundHandlerAdapter used in the EchoServerHandler. This has to do with the interaction of two factors: how the business logic processes messages and how Netty manages resources.
In the client, when channelRead0() completes, you have the incoming message and you’re done with it. When the method returns, SimpleChannelInboundHandler takes care of releasing the memory reference to the ByteBuf that holds the message.
In EchoServerHandler you still have to echo the incoming message to the sender, and the write() operation, which is asynchronous, may not complete until after channelRead() returns (shown in listing 2.1). For this reason EchoServerHandler extends ChannelInboundHandlerAdapter, which doesn’t release the message at this point.
The message is released in channelReadComplete() in the EchoServerHandler when writeAndFlush() is called (listing 2.1).
Chapters 5 and 6 will cover message resource management in detail.
As you’ll see in the next listing, bootstrapping a client is similar to bootstrapping a server, with the difference that instead of binding to a listening port the client uses host and port parameters to connect to a remote address, here that of the Echo server.
As before, the NIO transport is used. Note that you could use different transports in the client and server; for example, NIO transport on the server side and OIO transport on the client side. In chapter 4 we’ll examine the factors and scenarios that would lead you to select a specific transport for a specific use case.
Let’s review the important points introduced in this section:
Having finished the client, you can proceed to build the system and test it out.
In this section we’ll cover all the steps needed to compile and run the Echo server and client.
This book’s appendix uses the configuration of the Echo client/server project to explain in detail how multimodule Maven projects are organized. This isn’t required reading for building and running the applications, but it’s recommended for gaining a better understanding of the book’s examples and of the Netty project itself.
To build the Echo client and server, go to the chapter2 directory under the code samples root directory and execute the following command:
mvn clean package
This should produce something very much like the output shown in listing 2.5 (we’ve edited out a few nonessential steps in the build).
[INFO] Scanning for projects... [INFO] ------------------------------------------------------------------- [INFO] Reactor Build Order: [INFO] [INFO] Chapter 2. Your First Netty Application - Echo App [INFO] Chapter 2. Echo Client [INFO] Chapter 2. Echo Server [INFO] [INFO] ------------------------------------------------------------------- [INFO] Building Chapter 2. Your First Netty Application - 2.0-SNAPSHOT [INFO] ------------------------------------------------------------------- [INFO] [INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ chapter2 --- [INFO] [INFO] ------------------------------------------------------------------- [INFO] Building Chapter 2. Echo Client 2.0-SNAPSHOT [INFO] ------------------------------------------------------------------- [INFO] [INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ echo-client --- [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ echo-client --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 1 resource [INFO] [INFO] --- maven-compiler-plugin:3.3:compile (default-compile) @ echo-client --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 2 source files to etty-in-actionchapter2Client argetclasses [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ echo-client --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory etty-in-actionchapter2Clientsrc est esources [INFO] [INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile) @ echo-client --- [INFO] No sources to compile [INFO] [INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ echo-client --- [INFO] No tests to run. [INFO] [INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ echo-client --- [INFO] Building jar: etty-in-actionchapter2Client argetecho-client-2.0-SNAPSHOT.jar [INFO] [INFO] ------------------------------------------------------------------- [INFO] Building Chapter 2. Echo Server 2.0-SNAPSHOT [INFO] ------------------------------------------------------------------- [INFO] [INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ echo-server --- [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ echo-server --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 1 resource [INFO] [INFO] --- maven-compiler-plugin:3.3:compile (default-compile) @ echo-server --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 2 source files to etty-in-actionchapter2Server argetclasses [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ echo-server --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory etty-in-actionchapter2Serversrc est esources [INFO] [INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile) @ echo-server --- [INFO] No sources to compile [INFO] [INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ echo-server --- [INFO] No tests to run. [INFO] [INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ echo-server --- [INFO] Building jar: etty-in-actionchapter2Server argetecho-server-2.0-SNAPSHOT.jar [INFO] ------------------------------------------------------------------- [INFO] Reactor Summary: [INFO] [INFO] Chapter 2. Your First Netty Application ... SUCCESS [ 0.134 s] [INFO] Chapter 2. Echo Client .................... SUCCESS [ 1.509 s] [INFO] Chapter 2. Echo Ser........................ SUCCESS [ 0.139 s] [INFO] ------------------------------------------------------------------- [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------- [INFO] Total time: 1.886 s [INFO] Finished at: 2015-11-18T17:14:10-05:00 [INFO] Final Memory: 18M/216M [INFO] -------------------------------------------------------------------
Here are the main steps recorded in the preceding build log:
The Maven Reactor Summary shows that all projects have been successfully built. A listing of the target directories in the two subprojects should now resemble the following listing.
Directory of niachapter2Client arget 03/16/2015 09:45 PM <DIR> classes 03/16/2015 09:45 PM 5,614 echo-client-1.0-SNAPSHOT.jar 03/16/2015 09:45 PM <DIR> generated-sources 03/16/2015 09:45 PM <DIR> maven-archiver 03/16/2015 09:45 PM <DIR> maven-status Directory of niachapter2Server/target 03/16/2015 09:45 PM <DIR> classes 03/16/2015 09:45 PM 5,629 echo-server-1.0-SNAPSHOT.jar 03/16/2015 09:45 PM <DIR> generated-sources 03/16/2015 09:45 PM <DIR> maven-archiver 03/16/2015 09:45 PM <DIR> maven-status
To run the application components, you could use the Java command directly. But in the POM file, the exec-maven-plugin is configured to do this for you (see the appendix for details).
Open two console windows side by side, one logged into the chapter2Server directory and the other into chapter2Client.
In the server’s console, execute this command:
mvn exec:java
You should see something like the following:
[INFO] Scanning for projects... [INFO] [INFO] ---------------------------------------------------------------------- [INFO] Building Echo Server 1.0-SNAPSHOT [INFO] ---------------------------------------------------------------------- [INFO] [INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ echo-server >>> [INFO] [INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ echo-server <<< [INFO] [INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-server --- nia.chapter2.echoserver.EchoServer started and listening for connections on /0:0:0:0:0:0:0:0:9999
The server has been started and is ready to accept connections. Now execute the same command in the client’s console:
mvn exec:java
You should see the following:
[INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------- [INFO] Building Echo Client 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------- [INFO] [INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ echo-client >>> [INFO] [INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ echo-client <<< [INFO] [INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-client --- Client received: Netty rocks! [INFO] ------------------------------------------------------------------- [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------- [INFO] Total time: 2.833 s [INFO] Finished at: 2015-03-16T22:03:54-04:00 [INFO] Final Memory: 10M/309M [INFO] -------------------------------------------------------------------
And in the server console you should see this:
Server received: Netty rocks!
You’ll see this log statement in the server’s console every time you run the client.
Here’s what happens:
1. As soon as the client is connected, it sends its message: Netty rocks!
2. The server reports the received message and echoes it to the client.
3. The client reports the returned message and exits.
What you’ve seen is the expected behavior; now let’s see how failures are handled. The server should still be running, so type Ctrl-C in the server console to stop the process. Once it has terminated, start the client again with
mvn exec:java
This shows the output you should see from the client when it’s unable to connect to the server.
[INFO] Scanning for projects... [INFO] [INFO] -------------------------------------------------------------------- [INFO] Building Echo Client 1.0-SNAPSHOT [INFO] -------------------------------------------------------------------- [INFO] [INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ echo-client >>> [INFO] [INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ echo-client <<< [INFO] [INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-client --- [WARNING] java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) . . . Caused by: java.net.ConnectException: Connection refused: no further information: localhost/127.0.0.1:9999 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl .finishConnect(SocketChannelImpl.java:739) at io.netty.channel.socket.nio.NioSocketChannel .doFinishConnect(NioSocketChannel.java:208) at io.netty.channel.nio .AbstractNioChannel$AbstractNioUnsafe .finishConnect(AbstractNioChannel.java:281) at io.netty.channel.nio.NioEventLoop .processSelectedKey(NioEventLoop.java:528) at io.netty.channel.nio.NioEventLoop. processSelectedKeysOptimized(NioEventLoop.java:468) at io.netty.channel.nio.NioEventLoop .processSelectedKeys(NioEventLoop.java:382) at io.netty.channel.nio.NioEventLoop .run(NioEventLoop.java:354) at io.netty.util.concurrent.SingleThreadEventExecutor$2 .run(SingleThreadEventExecutor.java:116) at io.netty.util.concurrent.DefaultThreadFactory $DefaultRunnableDecorator.run(DefaultThreadFactory.java:137) . . . [INFO] -------------------------------------------------------------------- [INFO] BUILD FAILURE [INFO] -------------------------------------------------------------------- [INFO] Total time: 3.801 s [INFO] Finished at: 2015-03-16T22:11:16-04:00 [INFO] Final Memory: 10M/309M [INFO] -------------------------------------------------------------------- [ERROR] Failed to execute goal org.codehaus.mojo: exec-maven-plugin:1.2.1:java (default-cli) on project echo-client: An exception occured while executing the Java class. null: InvocationTargetException: Connection refused: no further information: localhost/127.0.0.1:9999 -> [Help 1]
What happened? The client tried to connect to the server, which it expected to find running at localhost:9999. This failed (as expected) because the server had been stopped previously, causing a java.net.ConnectException in the client. This exception triggered the exceptionCaught()method of the EchoClientHandler, which prints out the stack trace and closes the channel (see listing 2.3.)
In this chapter you set up your development environment and built and ran your first Netty client and server. Although this is a simple application, it will scale to several thousand concurrent connections—many more messages per second than a plain vanilla socket-based Java application would be able to handle.
In the following chapters, you’ll see many more examples of how Netty simplifies scalability and concurrency. We’ll also go deeper into Netty’s support for the architectural principle of separation of concerns. By providing the right abstractions for decoupling business logic from networking logic, Netty makes it easy to keep pace with rapidly evolving requirements without jeopardizing system stability.
In the next chapter, we’ll provide an overview of Netty’s architecture. This will give you the context for the in-depth and comprehensive study of Netty’s internals that will follow in subsequent chapters.