Netty entry series -- using netty to build the server and client

introduction

Previously, we introduced some basic concepts of the network. Although it is difficult to say these, we should at least understand them. With the previous foundation, it will be much easier for us to officially unveil the mysterious veil of Netty.

Server

public class PrintServer {

    public void bind(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();						//1
        EventLoopGroup workerGroup = new NioEventLoopGroup();					//2
        try {
            ServerBootstrap b = new ServerBootstrap();							//3
            b.group(bossGroup, workerGroup)										//4											
                    .channel(NioServerSocketChannel.class)						//5
                    .option(ChannelOption.SO_BACKLOG, 1024)						//6
                    .childHandler(new ChannelInitializer<SocketChannel>() {		//7
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new PrintServerHandler());
                        }
                    });

            ChannelFuture f = b.bind(port).sync();				//8
            
            f.channel().closeFuture().sync();					//9
        } finally {
            // Exit gracefully to release the resources of the thread pool
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        int port = 8080;
        new TimeServer().bind(port);
    }
}

Let's analyze the above code (each point below corresponds to the above comment)

1~2: first, we created two NioEventLoopGroup instances, which are a thread group containing NIO encapsulated by Netty. Why create two? I think everyone should know after the previous study. Yes, because the underlying layer of Netty is IO multiplexing, and the bossGroup is used to receive client connections. The principle is to implement the Reactor thread of the Selector. The workerGroup is used for network reading and writing of SocketChannel.

3: Create a ServerBootstrap object, which can be imagined as the entry of Netty. Start Netty through this class, and pass the required parameters to this class, which greatly reduces the difficulty of development.

4: Bind two NioEventLoopGroup instances to the ServerBootstrap object.

5: Create channels (typical channels include NioSocketChannel, nioserversocketchannel, OioSocketChannel, OioServerSocketChannel, EpollSocketChannel, EpollServerSocketChannel). Here, the nioserversocketchannel is created. Its function can be understood as that when receiving the connection request from the client, the TCP handshake is completed three times, and the TCP physical link is successfully established. And associate the "channel" with a thread of the workerGroup thread group.

6: Set parameters, so set here_ Backlog means that the length of the client connection waiting queue is 1024

7: The specific Handler after the connection is established. It refers to the specific operations after we receive data, such as recording logs, decoding and encoding information, etc.

8: Binding port, synchronization waiting succeeded

9: Wait for the listening port on the server to close

Bind the Handler of the server

public class PrintServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
	    throws Exception {
	ByteBuf buf = (ByteBuf) msg;										//1
	byte[] req = new byte[buf.readableBytes()];	
	buf.readBytes(req); //Copy the byte array of the cache to the newly created req array
	String body = new String(req, "UTF-8");
	System.out.println(body);
	String response= "Printing succeeded";
	ByteBuf resp = Unpooled.copiedBuffer(response.getBytes());						
	ctx.write(resp);													//2
    }	

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
	ctx.flush();														//3
    }


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

PrintServerHandler inherits the ChannelHandlerAdapter. Here, its function is to print the data sent by the client and return the client to print successfully.

We only need to implement channelRead and exceptionguess. The former is the implementation of the specific logic for accepting messages, and the latter is the implementation of the specific logic after exceptions occur.

1: We can see that the accepted message is encapsulated as an Object, which we convert to ByteBuf. The previous chapter also explained the role of this class. The data we need to read is in this cache class.

2~3: We package the written data into ByteBuf, and then write it back to the client through the write method. The function of calling the flush method in 3 here is to prevent frequent data sending. The write method does not directly write the data into the SocketChannel, but puts the data to be sent into the send cache array, and then calls the flush method to send the data.

client

public class PrintClient {

    public void connect(int port, String host) throws Exception {
	EventLoopGroup group = new NioEventLoopGroup();                 //1
	try {
	    Bootstrap b = new Bootstrap();                              //2
	     b.group(group)                                             //3
	        .channel(NioSocketChannel.class)                        //4
		    .option(ChannelOption.TCP_NODELAY, true)                //5
		    .handler(new ChannelInitializer<SocketChannel>() {      //6
			@Override
			public void initChannel(SocketChannel ch)               
				throws Exception {
			    ch.pipeline().addLast(new PrintClientHandler());
			}
		    });

	    ChannelFuture f = b.connect(host, port).sync();             //7
	    f.channel().closeFuture().sync();                           //8
	} finally {
	    // Exit gracefully and release NIO thread group
	    group.shutdownGracefully();
	}
    }

    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
	int port = 8080;
	new TimeClient().connect(port, "127.0.0.1");
    }
}

Let's continue to analyze the above code (each point below corresponds to the above comment)

1: Different from the server, we only create a NioEventLoopGroup instance on the client, because you do not need to use the I/O multiplexing model on the client, and you need a Reactor to accept requests. Simply read and write data

2: Different from the server side, we only need to create a Bootstrap object on the client side. It is a client-side auxiliary startup class, and its function is similar to ServerBootstrap.

3: Bind the NioEventLoopGroup instance to the Bootstrap object.

4: Create channels (typical channels include NIOSocketChannel, NioServerSocketChannel, OioSocketChannel, OioServerSocketChannel, EpollSocketChannel, EpollServerSocketChannel), which are different from the server. NIOSocketChannel is created here

5: Set parameters. TCP set here_ Nodelay is true, which means that delayed sending is turned off and messages are sent as soon as they occur. The default value is false.

6: The specific handler after the connection is established. Note the difference between the server and the handler(), instead of childHandler(). The difference between handler and childHandler is that handler is the executor before receiving or sending; childHandler is the executor after the connection is established.

7: Initiate asynchronous connection operation

8: Contemporary client link down

Bind the Handler of this client

public class PrintClientHandler extends ChannelHandlerAdapter {

    private static final Logger logger = Logger
	    .getLogger(TimeClientHandler.class.getName());

    private final ByteBuf firstMessage;

    /**
     * Creates a client-side handler.
     */
    public TimeClientHandler() {
	byte[] req = "Hello, server".getBytes();
	firstMessage = Unpooled.buffer(req.length);                                 //1
	firstMessage.writeBytes(req);

    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
	ctx.writeAndFlush(firstMessage);                                            //2             
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)              //3
	    throws Exception {
	ByteBuf buf = (ByteBuf) msg;    
	byte[] req = new byte[buf.readableBytes()];
	buf.readBytes(req);
	String body = new String(req, "UTF-8");
	System.out.println("Server response message : " + body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {   //4
	// Free resources
	System.out.println("Unexpected exception from downstream : "
		+ cause.getMessage());
	ctx.close();
    }
}

PrintClientHandler inherits the ChannelHandlerAdapter. Its function here is to send data and print the data sent by the server.

We only need to implement channelActive, channelRead and exceptioncaution. The first one is to execute immediately after the connection is established. The second two and one are to implement the specific logic of receiving messages, and the other is to implement the specific logic of exceptions.

1: Encapsulate the sent information into ByteBuf.

2: Send a message.

3: Accept client messages and print

4: When an exception occurs, print the exception information to release the client resources

summary

This is an introductory program. Corresponding to the I/O multiplexing model and NIO characteristics mentioned above, you can effectively understand the programming mode of this mode. If these pieces of code look hard, you can take a look at the Netty Foundation series of previous bloggers.

If there is something wrong with the blogger, I hope you can put it forward and make progress together~

Posted by Scip on Tue, 31 May 2022 06:40:03 +0530