An overview of the

In this article, we will write a client and server application based on the Netty implementation. We believe that by studying this example, we will have a more complete understanding of the Netty API

This figure shows multiple clients connected to a server at the same time. After a client establishes a connection, it sends one or more messages to the server, which in turn sends each message back to the client

Writing the Echo Server

All Netty servers require the following two parts:

  • At least one CHannelHandler

    This component implements the server’s processing of the data received from the client, its business logic

  • guide

    Configure the server’s startup code to bind the server to the port on which it listens for connection requests

1. ChannelHandler and service logic

ChannelHandler is the parent of a family of interfaces whose implementation is responsible for receiving and responding to event notifications, that is, processing logic to contain data. Our Echo server needs to respond to incoming messages, so we need to implement the ChannelHandler interface to define methods that respond to incoming events, and since only a few methods are needed, it is sufficient to inherit the ChannelHandlerAdapter class. It provides a default implementation of ChannelHandler

The methods we are interested in are:

  • channelRead()

    Called for each incoming message

  • channelReadComplete()

    Notifying ChannelHandler that the last call to channelRead() is the last message to be read in the current batch

  • exceptionCaught

    Called when an exception is thrown during a read operation

The ChannelHandler for the Echo server implements the EchoServerHandler as follows

@ChannelHandler.Sharable // Identifies a ChannelHandler that can be safely shared by multiple channels
public class EchoServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server receiver: " + in.toString(CharsetUtil.UTF_8));
        // Write the received message to the sender
        ctx.write(in);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // Flush the remaining messages to the remote node and close the CHannel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); }}Copy the code

The application implements or extends the ChannelHandler to hook into the event lifecycle and provide custom application logic. ChannelHandler helps keep the business logic separate from the network processing code, simplifying the development process

2. Boot the server

Having written the core business logic for the EchoServerHandler implementation, we now explore the process of booting the server as follows:

  • The interface on which the binding server listens and receives incoming connection requests
  • Configure a Channel to deliver inbound messages to the EchoServerHandler instance

The complete code for the EchoServer class is shown below

public class EchoServer {

    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        if(args.length ! =1) {
            System.err.println("Usage: " + EchoServer.class.getSimpleName() + " <port>");
            return;
        }
        int port = Integer.parseInt(args[0]);
        new EchoServer(port).start();
    }

    public void start(a) throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                    // Specify the NIO transport Channel to use
                    .channel(NioServerSocketChannel.class)
                    // Sets the socket address with the specified port
                    .localAddress(new InetSocketAddress(port))
                    // Add an EchoServerHandler to the ChannelPipeline of the child Handler
                    .childHandler(new ChannelInitializer<>() {

                        @Override
                        protected void initChannel(Channel ch) { ch.pipeline().addLast(serverHandler); }});// Bind the server asynchronously, calling sync() to block and wait until the bind is complete
            ChannelFuture f = b.bind().sync();
            // Gets the CloseFuture of the Channel and blocks the current thread until it completes
            f.channel().closeFuture().sync();
        } finally {
            // Close the EventLoopGroup to release all resourcesgroup.shutdownGracefully().sync(); }}}Copy the code

To that end, let’s review a few important steps in server implementation:

  • EchoServerHandler implements the business logic
  • The main() method boots the server

The important steps in the boot server process are as follows:

  • Create an instance of ServerBootstrap to boot and bind the server
  • Create and assign a NioEventLoopGroup instance to handle events, such as accepting new connections and reading and writing data
  • Specifies the local InetSocketAddress to which the server is bound
  • Initialize each new Channel with an Instance of EchoServerHandler
  • Call the serverbootstrap.bind () method to bind the server

Write the Echo client

Echo client functions:

  • Connecting to the server
  • Send one or more messages
  • For each message, wait and receive the response back from the server
  • Close the connection

As with the server, the main part of the code involved in writing the client is the business logic and boot

1. ChannelHandler and client logic

The client also should have a ChannelHandler used to process the data, select SimpleChannelInboundHandler class to handle all the necessary tasks, require rewrite the following method:

  • channelActive()

    Called when a connection to the server has been established

  • messageReceived()

    Called when a message is received from the server

  • exceptionCaught()

    Called when an exception is thrown during processing

@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // When a connection is established, a message is sent
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) {
        // Records the dump of received messages
        System.out.println("Client received: " + msg.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        When an exception occurs, log an error and close a Channelcause.printStackTrace(); ctx.close(); }}Copy the code

2. Start the client

The boot client is similar to the server, except that the client uses the host and port parameters to connect to the remote address

public class EchoClient {

    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        if(args.length ! =1) {
            System.err.println("Usage: " + EchoClient.class.getSimpleName() + "<host> <port>");
            return;
        }
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }

    public void start(a) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            / / create the Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(newEchoClientHandler()); }});// Connect to the remote node, block and wait until the connection is complete
            ChannelFuture future = bootstrap.connect().sync();
            // block until the Channel is closed
            future.channel().closeFuture().sync();
        } finally{ group.shutdownGracefully().sync(); }}}Copy the code

To that end, let’s review a few important steps in client implementation:

  • Create a Bootstrap instance
  • Create and assign a NioEventLoopGroup instance for event processing, which includes creating new connections and processing inbound and outbound data
  • Create an InetSocketAddress instance for the server connection
  • When the connection is established, an instance of EchoClientHandler is installed into the Channel’s ChannelPipeline
  • Call the bootstrap.connect () method to connect the remote node

Run the client and server

The project in this article is built using Maven, with the server up and ready to accept connections. The client is then started, and once the client establishes a connection, the message is sent. The server receives the message, and the console prints the following information:

Server receiver: Netty rocks!
Copy the code

At the same time, it is sent back to the client, and the client console also prints the following message and exits:

Client received: Netty rocks!
Copy the code