Originally, I wanted to learn about Netty components first, then learn about component applications. Then, learning more and more makes me feel weird. I always feel that there is something missing. I don't know how to use components when I learn them. Think about learning from Netty applications first.
Own technology learning method: first learn the application of technology, and gradually raise problems in the application, such as how to realize this function, contact the underlying principles with problems, and then solve problems.
1. The most basic implementation of Netty application -- implementation of request and response
1. First, the environment configuration (jdk) should be guaranteed to be OK. Second, we should introduce the jar of Netty and use the jar of netty-5.0.
2. Before starting development with Netty, think about the main steps needed to develop the server using NIO native class library in jdk:
- First, create the ServerSocketChannel and set it to non blocking mode.
- Bind the listening port and set the TCP connection parameters.
- Create a separate IO thread to poll the multiplexer Selector.
- Create a Selector, register the created ServerSocketChannel into the Selector, and listen for the selectionkey.op'u accept event on ServerSocketChannel.
- Start the independent IO thread, execute the Selector.select() method in the loop body, and get the ready Channel.
- When the Channel is acquired, the Channel status needs to be judged,
- If it's OP? Accept, it means that it's a new client access. Call ServerSocketChannel's accept() method to create a new connection, that is, SocketChannel object. After creating the SocketChannel object, you can set the TCP connection parameters and set them to non blocking mode. After setting, register the SocketChannel in the Selector and listen for the op ﹣ read event.
- If it is op "read", it means that there are ready data in SocketChannel to read. At this time, the ByteBuffer object needs to be constructed to read.
- If it is op write, it means that the SocketChannel has not sent any data, so it needs to continue sending.
3. Just a simple data sending and receiving is so complicated, and Netty is really much simpler. Take a server-side time information query as an example. The client sends a string message to the server, and the server returns the current time data as a response. The code is as follows
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; /** * @ClassName TimeServer * @Description: Netty Example of time server server server implemented * @Author * @Date 2019/11/19 14:41 * @Modified By: * @Version V1.0 */ public class TimeServer { public static void main(String[] args) { try { new TimeServer().bind(8080); } catch (InterruptedException e) { e.printStackTrace(); } } //Binding port public void bind(int port) throws InterruptedException { //Think about Netty's thread model. First, we need to create a thread group in Reactor mode. There are two thread groups EventLoopGroup bossGroup = new NioEventLoopGroup();//Main thread group, used to receive connection requests and establish connections EventLoopGroup workGroup = new NioEventLoopGroup();//Worker thread group, which performs IO operations through SocketChannel try { //Startup class configuration of Netty server ServerBootstrap serverBootstrap = new ServerBootstrap();//Create a server startup class, which is a class to assist the server startup. The main purpose is to reduce the development complexity. We only need to configure the relevant parameters serverBootstrap.group(bossGroup, workGroup);//Configure thread pool //To configure the NioServerSocketChannel of the server, since it is a server, it must be a ServerSocketChannel to receive client connection requests and establish connections serverBootstrap.channel(NioServerSocketChannel.class); //Configure the parameters of NioServerSocketChannel, or the corresponding TCP connection parameters of NioServerSocketChannel. For example, the parameter is the connection timeout parameter, which is 10000 milliseconds serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); //As the above line of code, configure the backlog parameter of the TCP connection to 1024 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); //Finally, bind the processing class ChildChannelHandler of IO events. This class is the Handler in the Reactor model, which is used to handle IO events. However, the Handler here is specially used to handle ACCEPT events, that is, to establish a connection and create a SocketChannel serverBootstrap.childHandler(new ChildChannelHandler()); //After the auxiliary startup class of the server is configured, you can bind the listening port. The binding result will be obtained through ChannelFuture ChannelFuture future1 = serverBootstrap.bind(port); //This line of code indicates that the current thread is blocked until the binding operation result can be obtained in future1. After the binding is completed, the server has started to run future1.sync(); //closeFuture means that when the Channel connection is closed (for example, the client disconnects), a ChannelFuture object will be returned ChannelFuture future2 = future1.channel().closeFuture(); //Block the current thread until future2 can always get the result of closing the connection future2.sync(); } finally { //Release thread pool resources bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } //This class is mainly used to listen for the ServerSocketChannel to receive connection requests and establish connection events, which is equivalent to the ACCEPT event in NIO private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { //The SocketChannel created after the connection is established will be processed in this method, and the handler handler of the response will be added ch.pipeline().addLast(new TimeServerHandler()); } } //Handle IO operations in SocketChannel for SocketChannel processors private class TimeServerHandler extends ChannelHandlerAdapter { //Read and process data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //Take out all the read data ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); //Coding analysis String body = new String(req, "utf-8"); System.out.println("server received:" + body);//output //Response data String res = "server current time:" + System.currentTimeMillis(); ByteBuf data = Unpooled.copiedBuffer(res.getBytes()); ctx.write(data);//Note that the write operation here is not sent immediately, but will be saved in a buffer first } //This method represents the operation after the completion of data reading @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush();//Call this method to send all data in the buffer to the client } //Exception occurs during data reading. Subsequent processing to be performed is in this method @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
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.nio.NioSocketChannel; import java.nio.Buffer; /** * @ClassName TimeClient * @Description: Client * @Author * @Date 2019/11/19 16:39 * @Modified By: * @Version V1.0 */ public class TimeClient { public void connect(int port, String host) { //Create NIO thread groups for clients EventLoopGroup workGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap();//Create a client secondary startup class try { bootstrap.group(workGroup);//Bind NIO thread group bootstrap.channel(NioSocketChannel.class);//Channel type initially created bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeChannelHandler()); } });//Set Handler //bootstrap.bind("8081"); ChannelFuture future = bootstrap.connect(host, port);//Establish a connection with the server future.sync();//Synchronous blocking waiting for connection to be established successfully ChannelFuture f = future.channel().closeFuture();//Wait for the connection to be closed and disconnected f.sync();//Synchronization blocking waiting for connection closure to complete } catch (InterruptedException e) { e.printStackTrace(); }finally { workGroup.shutdownGracefully();//Cleaning up resources } } public static void main(String[] args) { new TimeClient().connect(8080, "localhost"); } private class TimeChannelHandler extends ChannelHandlerAdapter{ //This method is executed after the connection is established, indicating that the connection establishment event is triggered @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { String message = "query server time"; ByteBuf req = Unpooled.copiedBuffer(message.getBytes()); ctx.writeAndFlush(req);//Send data to server } //Read the data sent by the server. If the server sends the data and the Channel triggers a readable event, the method will execute and read the data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] buffer = new byte[buf.readableBytes()]; buf.readBytes(buffer); String message = new String(buffer, "utf-8"); System.out.println(message);//Read the data sent by the server and code the output } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
4. Main steps of Netty development: refer to Netty's thread model to help understand
- Server:
- First, create two processing thread groups, that is, the master-slave multithread model in the Reactor model.
- Secondly, create the auxiliary startup class ServerBootstrap. The main function of this class is to assist in creating and starting the class of Netty server. Through this class, we can set a series of configurations and start the server to reduce the development complexity.
- Configure the binding thread group. The main thread group is used to receive and create new connections. The working thread group mainly performs IO operations.
- Set the channel type initially created as NioServerSocketChannel, which corresponds to the ServerSocketChannel in NIO and is used to receive connection requests from clients and create a connection (SocketChannel).
- Set the TCP connection parameters of NioServerSocketChannel, such as connect? Timeout? Millis connection timeout judgment.
- Set the NIOSocketChannel object created after the connection with the client is established (so it is the childHandler method, because it is created by NioServerSocketChannel). Bind the user-defined Handler processing chain. For data IO operation on SocketChannel in Netty, the Filter mechanism similar to that in Servlet is used, and the transformation of responsibility chain design mode is used.
- Bind the listening port and start the server.
- If the connection is closed, clean up the resource and exit
2. Client:
- First, create a worker thread group.
- Second, create a Bootstrap class.
- Specifies that the Channel type of the initial connection creation is NioSocketChannel
- Call the handler method to bind the processing chain of NioSocketChannel. Because it is bound for NioSocketChannel, it is the handler() method, not the childHandler() method.
- Call the connect() method to establish a connection with the server.
- If the connection is closed, clean up the resource and exit
2. Using Netty to solve the problem of half package reading (sticking and unpacking)
1. Sticking and unpacking in TCP
In the network model, TCP belongs to the transport layer protocol, that is, it is only responsible for data transmission, and will transmit the data to be transmitted between networks in the form of flow, just like a river, which is continuous. For TCP, it does not understand the meaning of data from the application layer, plus the window sliding mechanism of TCP, TCP may read a complete data flow when reading Some data may be read to multiple data streams at one time during reading (if it is difficult to understand, you can simply understand the principle of zero copy implementation designed in NIO, design to kernel buffer and application buffer, and some knowledge of TCP).
Figuratively speaking, the faucet is equivalent to the established network connection. There will be a buffer zone at the bottom of the system kernel, that is, there will be a "cylinder". This cylinder is directly placed under the faucet. After the faucet is turned on (connection is established), the water will flow out (data is received) directly to the cylinder for temporary storage (kernel buffer is received), and then the user space, In other words, the application program will use a "basin" (buffer in user space) to fetch water from this cylinder (kernel buffer data is copied to user buffer), and then process this part of data in the code (encoding and decoding, etc.). However, the problem is that the data sent by the data sender is usually sent as a whole, which means that the data receiver also needs to ensure that the data is received as a whole, so as to ensure the correct data encoding and decoding. However, for TCP protocol, because it is in the transmission layer, it is unable to understand the meaning of these data in the upper application layer, so it is impossible to split them Processing, only responsible for data transmission, in the data receiver, can you guarantee that the data taken out by each "basin" is a single complete message data sent by the sender?
When the server receives and reads the data sent by the client, the following situations may occur:
(1) the server receives and reads a complete message data sent by the client. For example, if the client sends a string data of "hello", the server also receives a message data of "hello".
(2) the client sends the message data twice, both of which are "hello", but the server only receives one message, and the message content is "hello". (sticky package)
(3) the client sends two message data, both of which are "hello". The server also receives two messages, but the first one is "helloh", and the second one is "hello". (unpacking)
If the sent string does not contain Chinese characters, the half packet reading can be normal. However, once the string contains Chinese characters or the byte code data of a certain type of file, once the half packet reading occurs, the encoding and decoding of the data will be abnormal.
Demo code of packet gluing and packet gluing: under normal circumstances (or when we want to avoid packet gluing and packet gluing), the client sends 100 pieces of information and requests the server time; the server receives 100 pieces of information and responds to the time information every time it receives one piece, but the result is obviously not the case because of packet gluing and packet gluing.
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; /** * @ClassName TimeServer * @Description: TCP Demonstration of problems caused by sticking and unpacking * @Author * @Date 2019/11/20 13:59 * @Modified By: * @Version V1.0 */ public class TimeServer { public static void main(String[] args) { try { new netty_demo.demo1.TimeServer().bind(8080); } catch (InterruptedException e) { e.printStackTrace(); } } //Binding port public void bind(int port) throws InterruptedException { //Think about Netty's thread model. First, we need to create a thread group in Reactor mode. There are two thread groups EventLoopGroup bossGroup = new NioEventLoopGroup();//Main thread group, used to receive connection requests and establish connections EventLoopGroup workGroup = new NioEventLoopGroup();//Worker thread group, which performs IO operations through SocketChannel try { //Startup class configuration of Netty server ServerBootstrap serverBootstrap = new ServerBootstrap();//Create a server startup class, which is a class to assist the server startup. The main purpose is to reduce the development complexity. We only need to configure the relevant parameters serverBootstrap.group(bossGroup, workGroup);//Configure thread pool //To configure the NioServerSocketChannel of the server, since it is a server, it must be a ServerSocketChannel to receive client connection requests and establish connections serverBootstrap.channel(NioServerSocketChannel.class); //Configure the parameters of NioServerSocketChannel, or the corresponding TCP connection parameters of NioServerSocketChannel. For example, the parameter is the connection timeout parameter, which is 10000 milliseconds serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); //As the above line of code, configure the backlog parameter of the TCP connection to 1024 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); //Finally, bind the processing class ChildChannelHandler of IO events. This class is the Handler in the Reactor model, which is used to handle IO events. However, the Handler here is specially used to handle ACCEPT events, that is, to establish a connection and create a SocketChannel serverBootstrap.childHandler(new ChildChannelHandler()); //After the auxiliary startup class of the server is configured, you can bind the listening port. The binding result will be obtained through ChannelFuture ChannelFuture future1 = serverBootstrap.bind(port); //This line of code indicates blocking the current thread until future1 can get the result of binding operation future1.sync(); //closeFuture means to close the connection. This method will return a ChannelFuture, and the result of closing the connection will be stored in this object ChannelFuture future2 = future1.channel().closeFuture(); //Block the current thread until future2 can always get the result of closing the connection future2.sync(); } finally { //Release thread pool resources bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } //This class is mainly used to listen for the ServerSocketChannel to receive connection requests and establish connection events, which is equivalent to the ACCEPT event in NIO private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { //The SocketChannel created after the connection is established will be processed in this method, and the handler handler of the response will be added ch.pipeline().addLast(new TimeServerHandler()); } } //Handle IO operations in SocketChannel for SocketChannel processors private class TimeServerHandler extends ChannelHandlerAdapter { private int count = 0; //Read and process data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //Take out all the read data ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); //Coding analysis String body = new String(req, "utf-8"); System.out.println("server received:" + body + ";count="+ ++count);//output //Response data if ("query server time".equals(body)) { String res = "server current time:" + System.currentTimeMillis(); ByteBuf data = Unpooled.copiedBuffer(res.getBytes()); ctx.writeAndFlush(data);//Note that the write operation here is not sent immediately, but will be saved in a buffer first } else { String res = "bad req:" + System.currentTimeMillis(); ByteBuf data = Unpooled.copiedBuffer(res.getBytes()); ctx.writeAndFlush(data);//Note that the write operation here is not sent immediately, but will be saved in a buffer first } } //Exception occurs during data reading. Subsequent processing to be performed is in this method @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
public class TimeClient { public void connect(int port, String host) { //Create NIO thread groups for clients EventLoopGroup workGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap();//Create customer secondary startup class try { bootstrap.group(workGroup);//Bind NIO thread group bootstrap.channel(NioSocketChannel.class);//Channel type initially created bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeChannelHandler()); } });//Set Handler //bootstrap.bind("8081"); ChannelFuture future = bootstrap.connect(host, port);//Establish a connection with the server future.sync();//Synchronous blocking waiting for connection to be established successfully ChannelFuture f = future.channel().closeFuture();//Wait for connection to close and disconnect f.sync();//Synchronization blocking waiting for connection closure to complete } catch (InterruptedException e) { e.printStackTrace(); }finally { workGroup.shutdownGracefully();//Cleaning up resources } } public static void main(String[] args) { new TimeClient().connect(8080, "localhost"); } private class TimeChannelHandler extends ChannelHandlerAdapter { private int count=0; //This method is executed after the connection is established, indicating that the connection establishment event is triggered @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for(int i=1;i<=100;i++){ String message = "query server time"; ByteBuf req = Unpooled.copiedBuffer(message.getBytes()); ctx.writeAndFlush(req);//Send data to server } } //Read the data sent by the server. If the server sends the data and the Channel triggers a readable event, the method will execute and read the data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] buffer = new byte[buf.readableBytes()]; buf.readBytes(buffer); String message = new String(buffer, "utf-8"); System.out.println(message+";count="+ ++count);//Read the data sent by the server and code the output } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
2. Solutions to sticking and unpacking in TCP
There are several solutions to the problems of sticking and unpacking as follows:
(1) fixed message length, that is, fixed message data length.
(2) use specific characters as separators or end marks, such as line breaks.
(3) the message is divided into message header and message body. The message header contains the description of message body, such as the data length of message body. (such as HTTP protocol and TCP protocol)
(4) define the application layer protocol.
1. Specify the message end flag character to solve the half package reading of text message data
For text message data, that is, string data, it is relatively easy to solve. You can use the second method above to specify a message end flag character, such as the common line break character \ n, Netty just provides a Handler class with line break as the message end flag, which solves the problem of unpacking and pasting the board through io.netty.handler.codec.LineBasedFrameDecoder class . The code is as follows
public class TimeClient { public void connect(int port, String host) { //Create NIO thread groups for clients EventLoopGroup workGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap();//Create customer secondary startup class try { bootstrap.group(workGroup);//Bind NIO thread group bootstrap.channel(NioSocketChannel.class);//Channel type initially created bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new TimeChannelHandler()); } });//Set Handler //bootstrap.bind("8081"); ChannelFuture future = bootstrap.connect(host, port);//Establish a connection with the server future.sync();//Synchronous blocking waiting for connection to be established successfully ChannelFuture f = future.channel().closeFuture();//Wait for connection to close and disconnect f.sync();//Synchronization blocking waiting for connection closure to complete } catch (InterruptedException e) { e.printStackTrace(); }finally { workGroup.shutdownGracefully();//Cleaning up resources } } public static void main(String[] args) { new TimeClient().connect(8080, "localhost"); } private class TimeChannelHandler extends ChannelHandlerAdapter{ private int count=0; //This method is executed after the connection is established, indicating that the connection establishment event is triggered @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for(int i=1;i<=100;i++){ String message = "query server time"+i+System.getProperty("line.separator"); ByteBuf req = Unpooled.copiedBuffer(message.getBytes()); ctx.writeAndFlush(req);//Send data to server } } //Read the data sent by the server. If the server sends the data and the Channel triggers a readable event, the method will execute and read the data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String message = (String) msg; System.out.println(message+";count="+ ++count);//Read the data sent by the server, } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
/** * Netty A modified version of the problem of packet gluing and unpacking in time server */ public class TimeServer { public static void main(String[] args) { try { new TimeServer().bind(8080); } catch (InterruptedException e) { e.printStackTrace(); } } //Binding port public void bind(int port) throws InterruptedException { //Think about Netty's thread model. First, we need to create a thread group in Reactor mode. There are two thread groups EventLoopGroup bossGroup = new NioEventLoopGroup();//Main thread group, used to receive connection requests and establish connections EventLoopGroup workGroup = new NioEventLoopGroup();//Worker thread group, which performs IO operations through SocketChannel try { //Startup class configuration of Netty server ServerBootstrap serverBootstrap = new ServerBootstrap();//Create a server startup class, which is a class to assist the server startup. The main purpose is to reduce the development complexity. We only need to configure the relevant parameters serverBootstrap.group(bossGroup, workGroup);//Configure thread pool //To configure the NioServerSocketChannel of the server, since it is a server, it must be a ServerSocketChannel to receive client connection requests and establish connections serverBootstrap.channel(NioServerSocketChannel.class); //Configure the parameters of NioServerSocketChannel, or the corresponding TCP connection parameters of NioServerSocketChannel. For example, the parameter is the connection timeout parameter, which is 10000 milliseconds serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); //As the above line of code, configure the backlog parameter of the TCP connection to 1024 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); //Finally, bind the processing class ChildChannelHandler of IO events. This class is the Handler in the Reactor model, which is used to handle IO events. However, the Handler here is specially used to handle ACCEPT events, that is, to establish a connection and create a SocketChannel serverBootstrap.childHandler(new ChildChannelHandler()); //After the auxiliary startup class of the server is configured, you can bind the listening port. The binding result will be obtained through ChannelFuture ChannelFuture future1 = serverBootstrap.bind(port); //This line of code indicates blocking the current thread until future1 can get the result of binding operation future1.sync(); //closeFuture means to close the connection. This method will return a ChannelFuture, and the result of closing the connection will be stored in this object ChannelFuture future2 = future1.channel().closeFuture(); //Block the current thread until future2 can always get the result of closing the connection future2.sync(); } finally { //Release thread pool resources bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } //This class is mainly used to listen for the ServerSocketChannel to receive connection requests and establish connection events, which is equivalent to the ACCEPT event in NIO private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); ch.pipeline().addLast(new StringDecoder()); //The SocketChannel created after the connection is established will be processed in this method, and the handler handler of the response will be added ch.pipeline().addLast(new TimeServerHandler()); } } //Handle IO operations in SocketChannel for SocketChannel processors private class TimeServerHandler extends ChannelHandlerAdapter { //Read and process data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("server received:" + body);//output //Response data String res = "server current time:" + System.currentTimeMillis()+System.getProperty("line.separator"); ByteBuf data = Unpooled.copiedBuffer(res.getBytes()); ctx.writeAndFlush(data); } //Exception occurs during data reading. Subsequent processing to be performed is in this method @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
This class of LineBasedFrameDecoder is a message end flag that has been defaulted as a line feed character and cannot be modified. Netty also provides another class that allows you to freely specify the end flag character. This class is the DelimiterBasedFrameDecoder, through which you can specify the character $as the message end flag character. The code is as follows
public class TimeClient { public void connect(int port, String host) { //Create NIO thread groups for clients EventLoopGroup workGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap();//Create customer secondary startup class try { bootstrap.group(workGroup);//Bind NIO thread group bootstrap.channel(NioSocketChannel.class);//Channel type initially created bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ByteBuf buf = Unpooled.copiedBuffer("$".getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new TimeChannelHandler()); } });//Set Handler //bootstrap.bind("8081"); ChannelFuture future = bootstrap.connect(host, port);//Establish a connection with the server future.sync();//Synchronous blocking waiting for connection to be established successfully ChannelFuture f = future.channel().closeFuture();//Wait for connection to close and disconnect f.sync();//Synchronization blocking waiting for connection closure to complete } catch (InterruptedException e) { e.printStackTrace(); }finally { workGroup.shutdownGracefully();//Cleaning up resources } } public static void main(String[] args) { new TimeClient().connect(8080, "localhost"); } private class TimeChannelHandler extends ChannelHandlerAdapter{ private int count=0; //This method is executed after the connection is established, indicating that the connection establishment event is triggered @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for(int i=1;i<=100;i++){ String message = "query server time"+i+"$"; ByteBuf req = Unpooled.copiedBuffer(message.getBytes()); ctx.writeAndFlush(req);//Send data to server } } //Read the data sent by the server. If the server sends the data and the Channel triggers a readable event, the method will execute and read the data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String message = (String) msg; System.out.println(message+";count="+ ++count);//Read the data sent by the server, } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
public class TimeServer { public static void main(String[] args) { try { new TimeServer().bind(8080); } catch (InterruptedException e) { e.printStackTrace(); } } //Binding port public void bind(int port) throws InterruptedException { //Think about Netty's thread model. First, we need to create a thread group in Reactor mode. There are two thread groups EventLoopGroup bossGroup = new NioEventLoopGroup();//Main thread group, used to receive connection requests and establish connections EventLoopGroup workGroup = new NioEventLoopGroup();//Worker thread group, which performs IO operations through SocketChannel try { //Startup class configuration of Netty server ServerBootstrap serverBootstrap = new ServerBootstrap();//Create a server startup class, which is a class to assist the server startup. The main purpose is to reduce the development complexity. We only need to configure the relevant parameters serverBootstrap.group(bossGroup, workGroup);//Configure thread pool //To configure the NioServerSocketChannel of the server, since it is a server, it must be a ServerSocketChannel to receive client connection requests and establish connections serverBootstrap.channel(NioServerSocketChannel.class); //Configure the parameters of NioServerSocketChannel, or the corresponding TCP connection parameters of NioServerSocketChannel. For example, the parameter is the connection timeout parameter, which is 10000 milliseconds serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); //As the above line of code, configure the backlog parameter of the TCP connection to 1024 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); //Finally, bind the processing class ChildChannelHandler of IO events. This class is the Handler in the Reactor model, which is used to handle IO events. However, the Handler here is specially used to handle ACCEPT events, that is, to establish a connection and create a SocketChannel serverBootstrap.childHandler(new ChildChannelHandler()); //After the auxiliary startup class of the server is configured, you can bind the listening port. The binding result will be obtained through ChannelFuture ChannelFuture future1 = serverBootstrap.bind(port); //This line of code indicates blocking the current thread until future1 can get the result of binding operation future1.sync(); //closeFuture means to close the connection. This method will return a ChannelFuture, and the result of closing the connection will be stored in this object ChannelFuture future2 = future1.channel().closeFuture(); //Block the current thread until future2 can always get the result of closing the connection future2.sync(); } finally { //Release thread pool resources bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } //This class is mainly used to listen for the ServerSocketChannel to receive connection requests and establish connection events, which is equivalent to the ACCEPT event in NIO private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ByteBuf buf = Unpooled.copiedBuffer("$".getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf)); ch.pipeline().addLast(new StringDecoder()); //The SocketChannel created after the connection is established will be processed in this method, and the handler handler of the response will be added ch.pipeline().addLast(new TimeServerHandler()); } } //Handle IO operations in SocketChannel for SocketChannel processors private class TimeServerHandler extends ChannelHandlerAdapter { //Read and process data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("server received:" + body);//output //Response data String res = "server current time:" + System.currentTimeMillis()+"$"; ByteBuf data = Unpooled.copiedBuffer(res.getBytes()); ctx.writeAndFlush(data); } //Exception occurs during data reading. Subsequent processing to be performed is in this method @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
2. Fixed message length solves the problem of half packet reading of message data
In fact, if you can determine the maximum message length each time you send a message, you can use the message fixed length method to solve the problem. Of course, this method must ensure that the message fixed length is not very long. You can use the FixedLengthFrameDecoder class provided by Netty to realize the message fixed length. The code is as follows
public class TimeClient { public void connect(int port, String host) { //Create NIO thread groups for clients EventLoopGroup workGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap();//Create customer secondary startup class try { bootstrap.group(workGroup);//Bind NIO thread group bootstrap.channel(NioSocketChannel.class);//Channel type initially created bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new FixedLengthFrameDecoder(1024)); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new TimeChannelHandler()); } });//Set Handler //bootstrap.bind("8081"); ChannelFuture future = bootstrap.connect(host, port);//Establish a connection with the server future.sync();//Synchronous blocking waiting for connection to be established successfully ChannelFuture f = future.channel().closeFuture();//Wait for connection to close and disconnect f.sync();//Synchronization blocking waiting for connection closure to complete } catch (InterruptedException e) { e.printStackTrace(); }finally { workGroup.shutdownGracefully();//Cleaning up resources } } public static void main(String[] args) { new TimeClient().connect(8080, "localhost"); } private class TimeChannelHandler extends ChannelHandlerAdapter{ private int count=0; //This method is executed after the connection is established, indicating that the connection establishment event is triggered @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { for(int i=1;i<=100;i++){ String message = "query server time"+i+"$"; ByteBuf req = Unpooled.copiedBuffer(message.getBytes()); ctx.writeAndFlush(req);//Send data to server } } //Read the data sent by the server. If the server sends the data and the Channel triggers a readable event, the method will execute and read the data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String message = (String) msg; System.out.println(message+";count="+ ++count);//Read the data sent by the server, } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
public class TimeServer { public static void main(String[] args) { try { new TimeServer().bind(8080); } catch (InterruptedException e) { e.printStackTrace(); } } //Binding port public void bind(int port) throws InterruptedException { //Think about Netty's thread model. First, we need to create a thread group in Reactor mode. There are two thread groups EventLoopGroup bossGroup = new NioEventLoopGroup();//Main thread group, used to receive connection requests and establish connections EventLoopGroup workGroup = new NioEventLoopGroup();//Worker thread group, which performs IO operations through SocketChannel try { //Startup class configuration of Netty server ServerBootstrap serverBootstrap = new ServerBootstrap();//Create a server startup class, which is a class to assist the server startup. The main purpose is to reduce the development complexity. We only need to configure the relevant parameters serverBootstrap.group(bossGroup, workGroup);//Configure thread pool //To configure the NioServerSocketChannel of the server, since it is a server, it must be a ServerSocketChannel to receive client connection requests and establish connections serverBootstrap.channel(NioServerSocketChannel.class); //Configure the parameters of NioServerSocketChannel, or the corresponding TCP connection parameters of NioServerSocketChannel. For example, the parameter is the connection timeout parameter, which is 10000 milliseconds serverBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); //As the above line of code, configure the backlog parameter of the TCP connection to 1024 serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024); //Finally, bind the processing class ChildChannelHandler of IO events. This class is the Handler in the Reactor model, which is used to handle IO events. However, the Handler here is specially used to handle ACCEPT events, that is, to establish a connection and create a SocketChannel serverBootstrap.childHandler(new ChildChannelHandler()); //After the auxiliary startup class of the server is configured, you can bind the listening port. The binding result will be obtained through ChannelFuture ChannelFuture future1 = serverBootstrap.bind(port); //This line of code indicates blocking the current thread until future1 can get the result of binding operation future1.sync(); //closeFuture means to close the connection. This method will return a ChannelFuture, and the result of closing the connection will be stored in this object ChannelFuture future2 = future1.channel().closeFuture(); //Block the current thread until future2 can always get the result of closing the connection future2.sync(); } finally { //Release thread pool resources bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } //This class is mainly used to listen for the ServerSocketChannel to receive connection requests and establish connection events, which is equivalent to the ACCEPT event in NIO private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new FixedLengthFrameDecoder(1024)); ch.pipeline().addLast(new StringDecoder()); //The SocketChannel created after the connection is established will be processed in this method, and the handler handler of the response will be added ch.pipeline().addLast(new TimeServerHandler()); } } //Handle IO operations in SocketChannel for SocketChannel processors private class TimeServerHandler extends ChannelHandlerAdapter { //Read and process data @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("server received:" + body);//output //Response data String res = "server current time:" + System.currentTimeMillis()+"$"; ByteBuf data = Unpooled.copiedBuffer(res.getBytes()); ctx.writeAndFlush(data); } //Exception occurs during data reading. Subsequent processing to be performed is in this method @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
3. Define application layer protocol and message structure
The above two methods are basically applicable to the transmission of small text message data. However, for complex situations, such as the transmission of both text data and various files, it is more complex. Because TCP protocol belongs to the transmission layer protocol, it does not understand what the binary data represents, so it is necessary to define an upper application layer's Protocol to analyze the data received by TCP.
The protocol doesn't go into details. It is simply the agreement between the client and the server. The protocol specifies the format of data sent by the client and the way of data analysis by the server. The common HTTP protocol is a good example. It uses a variety of ways to solve the problem of half package reading. First, it specifies the structure of the message (such as request header, request line, request body). For the request header, request header and request line, an end character must also be added. A line break character will be added to the interval between message substructures to indicate the end . You can learn more about the format of HTTP protocol.
The corresponding Handler of Http protocol certainly doesn't need to mention that Netty is ready to implement a simple file server of Http protocol by directly calling. The code is as follows
public class HttpFileServer { private static final String DEFAULT_URL = "/src/netty_demo/"; public static void main(String[] args) { int port = 8080; try { new HttpFileServer().run(port, DEFAULT_URL); } catch (InterruptedException e) { e.printStackTrace(); } } //Server startup method public void run(final int port,final String url) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.channel(NioServerSocketChannel.class); bootstrap.group(bossGroup, workGroup); bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());//Add HTTP request message decoder ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));//The decoder is used to combine multiple HTTP messages into a complete HTTP message, ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());//HTTP response data encoder ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());//The handler is used for asynchronous transmission of large code streams (such as file transmission), but does not occupy too much memory to prevent memory overflow ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url)); } }); ChannelFuture cf = bootstrap.bind(port).sync(); System.out.println("HTTP The file server starts at: http://localhost:8080"+url); cf.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } //Returns an HTTP response indicating that the request is wrong private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){ FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(status.toString()+"\r\n", CharsetUtil.UTF_8)); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8"); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } //Set response header for HTTP response private static void setContentTypeHeader(HttpResponse response, File file) { MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap(); response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath())); } //Respond to a redirect message private static void sendRedirect(ChannelHandlerContext ctx, String uri) { FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND); response.headers().set(HttpHeaderNames.LOCATION, uri); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } //Return to the html page of the file list private static void sendFileList(ChannelHandlerContext ctx, File dir) { FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); StringBuilder html = new StringBuilder(); String dirPath = dir.getPath(); html.append("<!DOCTYPE html>\n" + "<html>\n" + "<head>"); html.append("<title>"+dirPath+"Catalog:"+"</title>"); html.append("</head>"); html.append("<body>\n"); html.append("<h3>").append(dirPath).append("Catalog:").append("</h3>\n"); html.append("<ul>"); html.append("<li>Links:<a href=\"../\">..</a></li>\n"); for (File f:dir.listFiles()) { if (f.isHidden() || !f.canRead()) { continue; } String name = f.getName(); html.append("<li>Links:<a href=\""+name+"\">"+name+"</a></li>\n"); } html.append("</ul>"); html.append("</body>\n"); html.append("</html>"); ByteBuf buf = Unpooled.copiedBuffer(html, CharsetUtil.UTF_8); response.content().writeBytes(buf); buf.release(); ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); } //A series of operations after an HTTP request is pushed, including responding to a message private class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private String url; public HttpFileServerHandler(String url) { this.url = url; } //Decode the uri and convert to the local file directory private String decodeUri(String uri) { try { uri = URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { try { uri = URLDecoder.decode(uri, "ISO-8859-1"); } catch (UnsupportedEncodingException ex) { throw new RuntimeException("Request path exception"); } } if (!uri.startsWith(DEFAULT_URL)) { return null; } if (!uri.startsWith("/")) { return null; } uri = uri.replace('/', File.separatorChar); String path = System.getProperty("user.dir")+uri; return path; } //Processing after receiving the request message @Override protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { //First, judge the correctness of the request path if(request.decoderResult().isFailure()){ sendError(ctx, HttpResponseStatus.BAD_REQUEST); return; } if(request.method()!=HttpMethod.GET) { sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED); return; } String uri = request.uri(); String path = decodeUri(uri); if (path == null) { sendError(ctx, HttpResponseStatus.FORBIDDEN); return; } File file = new File(path); if (file.isHidden() || !file.exists()) { sendError(ctx, HttpResponseStatus.NOT_FOUND); return; } //If the local file corresponding to the request path is a folder (directory), the files in the directory will be displayed in a list in the HTML page, //And return the page to the client as a response message if (file.isDirectory()) { if(uri.endsWith("/")) { sendFileList(ctx, file); } else { sendRedirect(ctx, uri+"/"); } return; } if (!file.isFile()) { sendError(ctx, HttpResponseStatus.FORBIDDEN); return; } //If the target of the request path is a file, file transfer is enabled RandomAccessFile accessFile = null; try { accessFile = new RandomAccessFile(file, "r");//Open file read-only }catch (FileNotFoundException e) { sendError(ctx, HttpResponseStatus.NOT_FOUND); return; } long fileLength = accessFile.length(); //Create and set response headers and response lines HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); setContentTypeHeader(response, file); response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength+""); String connection = request.headers().get(HttpHeaderNames.CONNECTION).toString(); if(connection.contentEquals(HttpHeaderValues.KEEP_ALIVE)) { response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } ctx.write(response); //Create and set the response body, which is the file data ChannelFuture sendFileFuture; sendFileFuture = ctx.write(new ChunkedFile(accessFile, 0, fileLength, 8192), ctx.newProgressivePromise()); sendFileFuture.addListener(new ChannelProgressiveFutureListener() { @Override public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception { if (total < 0) { System.out.println("File transfer in progress,Has been sent:"+progress+"byte"); }else { System.out.println("File transfer in progress,Has been sent:"+progress+"/"+total+"byte"); } } @Override public void operationComplete(ChannelProgressiveFuture future) throws Exception { System.out.println("File transfer complete"); } }); ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); if(connection.contentEquals(HttpHeaderValues.KEEP_ALIVE)) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); if (ctx.channel().isActive()) { sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR); } } } }
If you have read the Http protocol server code, you will feel that the implementation code for reverse proxy in nginx seems to be like this. Of course, the path and other parameters are not dead, but are defined through the configuration file of nginx, and a series of load balancing rules are defined. The actual code must be much more complex, but the principle is the same.
3. summary
After using Netty, I believe that 10 of 10 people will not want to use NIO's native class library any more. The biggest advantage of Netty compared with NIO's native class library is that it has low development complexity, complete class library and powerful functions.