Getting started with Netty

Netty is a client/server framework that provides a simplified layer over NIO networking. This makes it a good candidate to create low-level nonblocking network applications.

Overview of Netty

Before we begin with a practical example, let’s see the main highlights of Netty framework:

  • Ease of use: Netty is simpler to use than plain Java NIO and has an extensive set of examples covering most use cases
  • Minimal dependency: As we will see in a minute, you can get the whole framework with just a single dependency
  • Performance: Netty has better throughput and reduced latency than core Java APIs. It is also scalable thanks to its internal pooling of resources.
  • Security: Complete SSL/TLS and StartTLS support.

Netty Building Blocks

In Java-based networking, the fundamental construct is the class Socket . Netty’s Channel interface provides an API that greatly simplifies the complexity of working directly with Socket. To work with TCP/IP Channels, we will deal with SocketChannel which represents the TCP connection between client and servers:

netty tutorial

SocketChannels are managed by EventLoop which is looking for new events, such as incoming data.. When an event occurs, it is eventually passed on to the appropriate Handler for example a ChannelHandler.

Next, to share resources like threads, Netty groups each EventLoop into an EventLoopGroup.

Finally, to handle the bootstrapping of Netty and its resources, you can use the BootStrap class.

Let’s see how to use the above Classes with a simple Server echo example

Your first Netty example

As an example, we will use pick up a simple one from the Netty distribution. To make it ready to run, we will bundle it in a single Maven application with just the base netty dependency.

Firstly, bootstrap a Maven project with your IDE or from the Command Line:

$ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4

Next, create the following EchoServer Class:

package io.netty.example;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.*;

public final class EchoServer {


    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
         
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                      
                     
                     p.addLast(serverHandler);
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
 @Sharable
 class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

As you can see, our example uses two EventLoopGroup. The first to boostrap the server, the second is a Worker Group. This is a common best practice so that you can tune a specific Group for high load and detect bottlenecks.

The ServerBootStrap bootstraps the applications. We have attached two Handlers to its ChannelPipeLine:

  1. A LoggingHandler in the first slot that logs all events using a logging framework.
  2. A Custom Handler named EchoServerHandler in the last slot.

The EchoServerHandler implements a set of methods:

  • channelRead: is triggered when data is received. As you can see, in our implementation we use the ChannelHandlerContext to write the same data back the Client
  • channelReadComplete: is triggered once there is no more data to read from the underlying transport.
  • exceptionCaught: closes the Connection in case there’s an Exception.

Finally, notice the @Sharable annotation on the Handler: it means that, you can register and share it with multiple clients.

Running the application

To build and run the application you need to include the following “umbrella” dependency in your project:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.52.Final</version>
</dependency>

Next, start your EchoServer from your IDE (“Run as Java application“) or the Command Line:

Jan 01, 2022 9:48:38 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x346609bb] REGISTERED
Jan 01, 2022 9:48:38 AM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0x346609bb] BIND: 0.0.0.0/0.0.0.0:8007
Jan 01, 2022 9:48:38 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x346609bb, L:/0:0:0:0:0:0:0:0:8007] ACTIVE

To connect, it is sufficient any TCP Client like telnet. We will connect to the Netty Server running on port 8007:

$ telnet 127.0.0.1 8007
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello There!
Hello There!

As you can see, the Server is echoing our messages back to the Client (telnet).

Building a Client/Server application

In our second example, we will learn how to write a Netty Client with its own Handler. We will also demonstrate how to send a simple Java object (a String) through the wire.

Create a new Java class and name it NettyServer:

package io.netty.example;

import java.util.ArrayList;
import java.util.List;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public final class NettyServer {
	// Port where chat server will listen for connections.
	static final int PORT = 8007;

	public static void main(String[] args) throws Exception {

		EventLoopGroup bossGroup = new NioEventLoopGroup(1);
		EventLoopGroup workerGroup = new NioEventLoopGroup();

		try {
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup) // Set boss & worker groups
					.channel(NioServerSocketChannel.class)
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline p = ch.pipeline();

							p.addLast(new StringDecoder());
							p.addLast(new StringEncoder());

							p.addLast(new ServerHandler());
						}
					});

			// Start the server.
			ChannelFuture f = b.bind(PORT).sync();
			System.out.println("Netty Server started.");

			// Wait until the server socket is closed.
			f.channel().closeFuture().sync();
		} finally {
			// Shut down all event loops to terminate all threads.
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}
}

@Sharable
class ServerHandler extends SimpleChannelInboundHandler<String> {

	static final List<Channel> channels = new ArrayList<Channel>();

	@Override
	public void channelActive(final ChannelHandlerContext ctx) {
		System.out.println("Client joined - " + ctx);
		channels.add(ctx.channel());
	}

	@Override
	public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
		System.out.println("Message received: " + msg);
		for (Channel c : channels) {
			c.writeAndFlush("Hello " + msg + '\n');
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		System.out.println("Closing connection for client - " + ctx);
		ctx.close();
	}
}

Firstly, notice some differences. In our first example, the communication happens in Byte Streams through the ByteBuf interface. To manage the communication in Strings, we are adding to the ChannelPipeLine a StringEncoder and a StringDecoder.

Then, our Handler extends SimpleChannelInboundHandler which allows to explicit only handle a specific type of messages. In our case, String messages.

Within the channelRead0 method, new messages are captured, printed on the Console and sent back to the Client with an “Hello” response at the beginning.

Our Client application is similar to our Server as it uses the same building blocks:

package io.netty.example;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;


 
public final class NettyClient {

	static final String HOST = "127.0.0.1";
	static final int PORT = 8007;
 
 
	public static void main(String[] args) throws Exception { 
	 
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group) // Set EventLoopGroup to handle all eventsf for client.
					.channel(NioSocketChannel.class)// Use NIO to accept new connections.
					.handler(new ChannelInitializer<SocketChannel>() {
						@Override
						public void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline p = ch.pipeline();

							p.addLast(new StringDecoder());
							p.addLast(new StringEncoder());
 
							// This is our custom client handler which will have logic for chat.
							p.addLast(new  ClientHandler());
 
						}
					});
 
			// Start the client.
			ChannelFuture f = b.connect(HOST, PORT).sync();
 
			 
				String input = "Frank";
				Channel channel = f.sync().channel();
				channel.writeAndFlush(input);
				channel.flush();
			 
			// Wait until the connection is closed.
			f.channel().closeFuture().sync();
		} finally {
			// Shut down the event loop to terminate all threads.
			group.shutdownGracefully();
		}
	}
}
  class  ClientHandler extends SimpleChannelInboundHandler<String> {  
 
		@Override
		protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
			System.out.println("Message from Server: " + msg);
	 
		}
	 
	}

The main differences are:

  • A ChannelFuture starts the connection to the Server and is notified when the handshake is completed.
  • The ClientHandler, which, merely prints the Message from the Server.

Now, start the Server application and wait to see this Message on the Console:

Netty Server started.

Next, starts the Client and check on the Console for this Message:

Message from Server: Hello Frank

Conclusion

At the end of this Netty tutorial, you should be familiar with Netty network framework and how to create a simple Echo Server and a Client/Server application. The source code (derived from the netty examples) is available here: https://github.com/fmarchioni/mastertheboss/tree/master/netty/basic

Found the article helpful? if so please follow us on Socials