Articles Catalogue
Summary
What is Netty?
Netty is a NIO client server framework, which supports rapid and simple development of network applications and can quickly build TCP and UDP service programs.
Why don't Netty 2 use NIO directly?
The following is excerpted from the second edition of the Netty Authoritative Guide
- NIO class libraries and API s are complex and difficult to use. It requires proficiency in Selector, Server Socket Channel, Socket Channel, ByteBuffer, etc.
- Other additional skills are needed to pave the way, such as familiarity with Java multithreading programming. This is because NIO programming design to Reactor mode, you must be very familiar with multi-threading and network programming, in order to write high-quality NIO programs;
- Reliability ability ability is complementary, workload and difficulty are very large. For example, clients are faced with problems such as reconnection, network interruption, half-packet read-write, failure cache, network congestion and abnormal bit stream processing. NIO programming is characterized by relatively easy function development, but the workload and difficulty of reliability capability complementation are very large.
- JDK NIO bugs, such as the notorious epoll bug, can lead to Selector empty polling and eventually CPU 100%. Officials claim to fix the problem in JDK version 1.6 update 8, but it still exists until JDK version 1.7, except that the probability of the bug occurring is reduced a little, and it has not been fundamentally solved.
For these reasons, in most scenarios, it is not recommended to use JDK's NIO libraries directly unless you are proficient in NIO programming or have special requirements. In most business scenarios, we can use the NIO framework Netty for NIO programming. It can be used as both a client and a server, while supporting UDP and asynchronous file transfer. It is very powerful.
API usage
Using Netty to Create Server-side Programs
The following describes how to use the API provided by Netty to build a basic server-side program.
I. Body Code
private void bind(int port) { // Initialize two thread rents EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup childGroup = new NioEventLoopGroup(); try { // Server Channel bootstrap to make Server Channel easier to use ServerBootstrap bootstrap = new ServerBootstrap(); /* bossGroup It is two thread groups with workerGroup, which receives requests from clients. workerGroup Network Read and Write for Processing Received Socket Channel */ bootstrap.group(bossGroup, childGroup) .channel(NioServerSocketChannel.class)// Setting up Channel instances that need to be started /* Setting the TCP parameters of NioServerSocket Channel, backlog For an analysis, see https://www.cnblogs.com/qiumingcheng/p/9492962.html */ .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChildChannelHandler());// Setting the processing class to process the received request ChannelFuture future = bootstrap.bind(port).sync();// Binding listening ports for ServerBootstrap future.channel().closeFuture().sync();// Close NioServer Socket Channel } catch (InterruptedException e) { e.printStackTrace(); } finally { // Release Thread Group bossGroup.shutdownGracefully(); childGroup.shutdownGracefully(); } } public static void main(String[] args) { SimpleTimeServer server = new SimpleTimeServer(); server.bind(8088); }
childHandler
Server Bootstrap's childHandler function requires an instance that implements io.netty.channel.ChannelHandler. ChildHandler is a time callback that provides threads in the child group. As you can see from the name, we pass in an instance of ChildChannelHandler, which is a class we implemented ourselves. It is not a class that handles SocketChannel directly, but inherits ChannelInitializer to organize more actual processing classes.
ChildChannelHandler code:
/** * ServerBootstrap The processing class of the ChildChannel received in the childGroup in the. * Used to process received requests */ private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel socketChannel) { /* Here the Socket Channel is provided by Netty, pipeline is a list of ChildHandler. That is, we can use multiple handlers to process the received Socket Channel. */ socketChannel.pipeline() .addLast(new ActualHandler1(), new ActualHandler2()); } }
Actual Handler
As mentioned above, ChildChannelHandler is actually just an aggregation management class used to organize actual processing classes. The actual processing classes are ActualHandler 1 and ActualHandler 2 in the code. Let's see how the next ActualHandler should be implemented.
private class ActualHandler extends ChannelHandlerAdapter { /** * When a new connection is created, this function is called back first. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { System.out.println("Create a new connection"); } /** * Read the data in the received Server Channel * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf reqBuf = (ByteBuf) msg; byte[] bytes = new byte[reqBuf.readableBytes()]; reqBuf.readBytes(bytes); String reqMsg = new String(bytes, StandardCharsets.UTF_8); System.out.println("Receive the request message:" + reqMsg); ByteBuf respBuf = Unpooled.copiedBuffer("Hello, I have received your message.".getBytes(StandardCharsets.UTF_8)); ctx.write(respBuf); } /** * read Functions called after completion, some data read after the completion of the operation * * @param ctx */ @Override public void channelReadComplete(ChannelHandlerContext ctx) { System.out.println("Read out"); ctx.flush(); } /** * Callback function in case of exception * * @param ctx * @param cause */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { System.out.println("There's something unusual!"); cause.printStackTrace(); ctx.close(); } }
Analysis:
Actual Handler is the real processing class used to process the received SocketChannel, where we can read, write and decode messages and so on.
Our Actual Handler inherits ChannelHandler Adapter and ChannelHandler Adapter inherits ChannelHandler. There are many kinds of callback functions, which can be selected flexibly according to their own needs.
Using Netty to Create Client Programs
The method of creating server-side program was introduced before, and the client and the server are very similar in fact.
I. Body Code
/** * Connect to the specified remote service, similar to the bind on the Server side * * @param host Remote service ip * @param port Remote Service Port */ public void connect(String host, int port) { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new ActualHandler()); } }); // Initiate an asynchronous connection operation ChannelFuture future = bootstrap.connect(host, port).sync(); // Waiting for Client Link to Close future.channel().closeFuture().sync(); } catch (Throwable t) { t.printStackTrace(); group.shutdownGracefully(); } } public static void main(String[] args) { new SimpleTimeClient().connect("localhost", 8088); }
handler
childHandler is set on the server side, and handler needs to be set on the client side. The principle is consistent with that on the server side.
Here we use anonymous inner classes directly to initialize ChannelInitializer instances.
Actual Handler
The principle is consistent with the server.
private class ActualHandler extends ChannelHandlerAdapter { /** * When the connection is successful, this function is called back, where we can send messages to the server. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { byte[] reqMsg = "Hello~".getBytes(StandardCharsets.UTF_8); ByteBuf buf = Unpooled.copiedBuffer(reqMsg); ctx.writeAndFlush(buf); } /** * This function is called back when the server replies to the message * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); String respMsg = new String(bytes, StandardCharsets.UTF_8); System.out.println("Receive the return message from the server:" + respMsg); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
Using Netty to Solve the Sticking Pack Problem
In network programming, sticky package problem is often unavoidable. Usually we deal with sticky package in the following ways:
- Use a specific identifier character as the terminator;
- Fixed message length;
- The message is divided into header and body, and the total length of the message is declared in the header.
In the past, we need to implement the unpacking code manually, which is tedious and tedious. Netty provides us with several ready-made unpacking classes, which are very easy to use.
- LineBasedFrameDecoder;
- DelimiterBasedFrameDecoder;
- Fixed Length Frame Decoder uses fixed bytes as message length for unpacking.
LineBasedFrameDecoder
LineBasedFrameDecoder unpacks using line breaks as terminators
Usage method:
-
LineBasedFrameDecoder is added to SocketChannel.pipeline of the receiving party. The constructor parameter is the maximum length of each line of message. If the maximum length has not been received, an exception will be thrown.
socketChannel.pipeline().addLast( new LineBasedFrameDecoder(1024) , new StringDecoder() , new LineBasedTimeServerHandler());
We also added String Decoder after Line Based Frame Decoder, which is very simple, that is, to convert the bytecode into a string, so that the message we receive is not ByteBuf, but String.
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String respMsg = (String) msg;// Received directly is String System.out.println("Current time:" + respMsg); ctx.close(); }
-
Add a newline character to the end of the message
byte[] msg = ("I'm the news." + System.getProperty("line.separator")) .getBytes(StandardCharsets.UTF_8);
Complete these two steps, based on the newline character unpacking will be done, whether fried chicken is simple, saving us a lot of unpacking work.
DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder allows us to unpackage using specified characters as terminators. In fact, newline characters are a special kind of specified characters. So the use of DelimiterBasedFrameDecoder is exactly the same as that of LineBasedFrameDecoder. It only needs the sender to change the newline characters to other specified characters.
byte[] msg = ("I'm the news." + "$")// End with $ .getBytes(StandardCharsets.UTF_8);
FixedLengthFrameDecoder
Fixed Length Frame Decoder allows us to split messages using a fixed number of bytes.
-
Message Receiver:
socketChannel.pipeline() .addLast( new FixedLengthFrameDecoder(39)// frameLength is the byte data length of a complete message , new StringDecoder() , new FixedLengthTimeServerHandler());
-
The sender of the message only needs to send the message according to the agreed byte length.
Simple Time Service Written with Netty
The following program is a simple time service based on Netty. There are server and client programs. Because the most basic byte-based data and three decoders are integrated together, the code is relatively long. It is recommended that the code be read in the IDE, and the necessary annotations are included in the code.
The program is stored in Gitee and the warehouse address is https://gitee.com/imdongrui/study-repo.git.
ioserver and ioclient engineering in warehouse
Server-side program
package com.dongrui.study.ioserver.nettyserver; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.FixedLengthFrameDecoder; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import java.nio.charset.StandardCharsets; import java.util.Date; /** * Simple Time Service Based on Netty */ public class SimpleTimeServer { private int counter = 0; private String[] modes = {"simple", "lineBased", "delimiterBased", "fixedLength"}; private String mode = modes[3]; /** * Initialize ServerBootstrap and bind the specified port * * @param port Ports to listen on */ private void bind(int port) { // Initialize two thread rents EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // Server Channel bootstrap to make Server Channel easier to use ServerBootstrap bootstrap = new ServerBootstrap(); /* bossGroup It is two thread groups with workerGroup, which receives requests from clients. workerGroup Network Read and Write for Processing Received Socket Channel */ bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class)// Setting up Channel instances that need to be started /* Setting the TCP parameters of NioServerSocket Channel, backlog For an analysis, see https://www.cnblogs.com/qiumingcheng/p/9492962.html */ .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChildChannelHandler());// Setting the processing class to process the received request ChannelFuture future = bootstrap.bind(port).sync();// Binding listening ports for ServerBootstrap future.channel().closeFuture().sync();// Close NioServer Socket Channel } catch (InterruptedException e) { e.printStackTrace(); } finally { // Release Thread Group bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } /** * ServerBootstrap The processing class of the ChildChannel received in the childGroup in the. * Used to process received requests */ private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel socketChannel) { /* Here the Socket Channel is provided by Netty, pipeline is a list of ChildHandler. That is, we can use multiple handlers to process the received Socket Channel. */ switch (mode) { case "simple": socketChannel.pipeline() .addLast(new SimpleTimeServerHandler()); break; case "lineBased": socketChannel.pipeline() // Using LineBasedFrameDecoder, messages can be split with line breaks as end identifiers. The constructor parameter is the maximum length of each line of messages. If the maximum length has not been received, an exception will be thrown. .addLast(new LineBasedFrameDecoder(1024)) // StringDecoder can transcode messages to String .addLast(new StringDecoder()) .addLast(new LineBasedTimeServerHandler()); break; case "delimiterBased": socketChannel.pipeline() // Using DelimiterBasedFrameDecoder, messages can be split with a designated identifier as the end identifier .addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8)))) // StringDecoder can transcode messages to String .addLast(new StringDecoder()) .addLast(new DelimiterBasedTimeServerHandler()); break; case "fixedLength": socketChannel.pipeline() // frameLength is the byte data length of a complete message .addLast(new FixedLengthFrameDecoder(39)) .addLast(new StringDecoder()) .addLast(new FixedLengthTimeServerHandler()); break; } } } /** * Handler, the actual processing time service, does not process messages in rows, that is, it does not solve the sticky package problem. */ private class SimpleTimeServerHandler extends ChannelHandlerAdapter { /** * When a new connection is created, this function is called back first. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { System.out.println("Create a new connection"); } /** * Read the data in the received Server Channel * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // Writing without using LineBasedFrameDecoder and StringDecoder to process sticky packages, the received msg is ByteBuf ByteBuf reqBuf = (ByteBuf) msg; byte[] bytes = new byte[reqBuf.readableBytes()]; reqBuf.readBytes(bytes); String reqMsg = new String(bytes, StandardCharsets.UTF_8); System.out.println("counter: " + ++counter); System.out.println("Receive the request message:" + reqMsg); ByteBuf respBuf = Unpooled.copiedBuffer((new Date().getTime() + "").getBytes(StandardCharsets.UTF_8)); ctx.write(respBuf); } /** * read Functions called after completion, some data read after the completion of the operation * * @param ctx */ @Override public void channelReadComplete(ChannelHandlerContext ctx) { System.out.println("Read out"); ctx.flush(); } /** * Callback function in case of exception * * @param ctx * @param cause */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } /** * Handler, the actual processing time service, processes messages in rows to solve the sticky package problem */ private class LineBasedTimeServerHandler extends ChannelHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("New connections"); } /** * Read the data in the received Server Channel * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // Writing when processing sticky packages using LineBasedFrameDecoder and StringDecoder, the received msg is String String reqMsg = (String) msg; System.out.println("counter: " + ++counter); System.out.println("Receive the request message:" + reqMsg); // When the server uses the LineBasedFrameDecoder, the line.separator is added at the end of each message, otherwise the message reading on the server cannot be completed. ByteBuf respBuf = Unpooled.copiedBuffer((new Date().getTime() + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8)); ctx.write(respBuf); } /** * read Functions called after completion, some data read after the completion of the operation * * @param ctx */ @Override public void channelReadComplete(ChannelHandlerContext ctx) { System.out.println("Read out"); ctx.flush(); } /** * Callback function in case of exception * * @param ctx * @param cause */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } private class DelimiterBasedTimeServerHandler extends ChannelHandlerAdapter { /** * Read the data in the received Server Channel * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String reqMsg = (String) msg; System.out.println("counter: " + ++counter); System.out.println("Receive the request message:" + reqMsg); ByteBuf respBuf = Unpooled.copiedBuffer((new Date().getTime() + "$").getBytes(StandardCharsets.UTF_8)); ctx.write(respBuf); } /** * read Functions called after completion, some data read after the completion of the operation * * @param ctx */ @Override public void channelReadComplete(ChannelHandlerContext ctx) { System.out.println("Read out"); ctx.flush(); } /** * Callback function in case of exception * * @param ctx * @param cause */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } private class FixedLengthTimeServerHandler extends ChannelHandlerAdapter { /** * Read the data in the received Server Channel * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String reqMsg = (String) msg; System.out.println("counter: " + ++counter); System.out.println("Receive the request message:" + reqMsg); ByteBuf respBuf = Unpooled.copiedBuffer((new Date().getTime() + "").getBytes(StandardCharsets.UTF_8)); ctx.write(respBuf); } /** * read Functions called after completion, some data read after the completion of the operation * * @param ctx */ @Override public void channelReadComplete(ChannelHandlerContext ctx) { System.out.println("Read out"); ctx.flush(); } /** * Callback function in case of exception * * @param ctx * @param cause */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } public static void main(String[] args) { SimpleTimeServer server = new SimpleTimeServer(); server.bind(8088); } }
Client program
package com.dongrui.study.ioclient.nettyclient; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; 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.DelimiterBasedFrameDecoder; import io.netty.handler.codec.FixedLengthFrameDecoder; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import java.nio.charset.StandardCharsets; public class SimpleTimeClient { private String[] modes = {"simple", "lineBased", "delimiterBased", "fixedLength"}; private String mode = modes[3]; /** * Connect to the specified remote service, similar to the bind on the Server side * * @param host Remote service ip * @param port Remote Service Port */ public void connect(String host, int port) { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { switch (mode) { case "simple": ch.pipeline() .addLast(new SimpleTimeClientHandler()); break; case "lineBased": ch.pipeline() // Using LineBasedFrameDecoder, messages can be split with line breaks as end identifiers. The constructor parameter is the maximum length of each line of messages. If the maximum length has not been received, an exception will be thrown. .addLast(new LineBasedFrameDecoder(1024)) // StringDecoder can transcode messages to String .addLast(new StringDecoder()) .addLast(new LineBasedTimeClientHandler()); break; case "delimiterBased": ch.pipeline() // Using DelimiterBasedFrameDecoder, messages can be split with a designated identifier as the end identifier .addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8)))) // StringDecoder can transcode messages to String .addLast(new StringDecoder()) .addLast(new DelimiterBasedTimeClientHandler()); break; case "fixedLength": ch.pipeline() .addLast(new FixedLengthFrameDecoder(13)) .addLast(new StringDecoder()) .addLast(new FixedLengthTimeClientHandler()); break; } } }); // Initiate an asynchronous connection operation ChannelFuture future = bootstrap.connect(host, port).sync(); // Waiting for Client Link to Close future.channel().closeFuture().sync(); } catch (Throwable t) { t.printStackTrace(); group.shutdownGracefully(); } } private class SimpleTimeClientHandler extends ChannelHandlerAdapter { /** * When the connection is successful, this function is called back, where we can send messages to the server. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { for (int i = 0; i < 100; i++) { byte[] reqMsg = ("Tell me the time or kill you!" + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8); ByteBuf buf = Unpooled.copiedBuffer(reqMsg); ctx.writeAndFlush(buf); } } /** * This function is called back when the server replies to the message * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; byte[] bytes = new byte[buf.readableBytes()]; buf.readBytes(bytes); String respMsg = new String(bytes, StandardCharsets.UTF_8); System.out.println("Current time:" + respMsg); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } private class LineBasedTimeClientHandler extends ChannelHandlerAdapter { /** * When the connection is successful, this function is called back, where we can send messages to the server. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { for (int i = 0; i < 100; i++) { // When the server uses the LineBasedFrameDecoder, the line.separator is added at the end of each message, otherwise the message reading on the server cannot be completed. byte[] reqMsg = ("Tell me the time or kill you!" + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8); ByteBuf buf = Unpooled.copiedBuffer(reqMsg); ctx.writeAndFlush(buf); } } /** * This function is called back when the server replies to the message * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String respMsg = (String) msg; System.out.println("Current time:" + respMsg); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } private class DelimiterBasedTimeClientHandler extends ChannelHandlerAdapter { /** * When the connection is successful, this function is called back, where we can send messages to the server. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { for (int i = 0; i < 100; i++) { // When the server uses the LineBasedFrameDecoder, the line.separator is added at the end of each message, otherwise the message reading on the server cannot be completed. byte[] reqMsg = ("Tell me the time or kill you!" + "$").getBytes(StandardCharsets.UTF_8); ByteBuf buf = Unpooled.copiedBuffer(reqMsg); ctx.writeAndFlush(buf); } } /** * This function is called back when the server replies to the message * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String respMsg = (String) msg; System.out.println("Current time:" + respMsg); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } private class FixedLengthTimeClientHandler extends ChannelHandlerAdapter { /** * When the connection is successful, this function is called back, where we can send messages to the server. * * @param ctx */ @Override public void channelActive(ChannelHandlerContext ctx) { for (int i = 0; i < 100; i++) { // When the server uses the LineBasedFrameDecoder, the line.separator is added at the end of each message, otherwise the message reading on the server cannot be completed. byte[] reqMsg = "Tell me the time or kill you!".getBytes(StandardCharsets.UTF_8); ByteBuf buf = Unpooled.copiedBuffer(reqMsg); ctx.writeAndFlush(buf); } } /** * This function is called back when the server replies to the message * * @param ctx * @param msg */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String respMsg = (String) msg; System.out.println("Current time:" + respMsg); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } public static void main(String[] args) { new SimpleTimeClient().connect("localhost", 8088); } }