A preliminary netty

As a NIO client server framework, Netty can quickly and easily build network applications, such as protocol servers and clients. Netty absorbs the experience of implementing protocols such as FTP, SMTP, and HTTP to ensure the robustness and maintainability of programs on the basis of ease of use and agility.

When we first learned Java network programming, we all opened a socket port, and then called the accept() method to block the connection, and then kept reading data. As we learn more later, we’ll try reading data in a non-blocking way and non-blocking I/O using the Selector Selector.

As the business grows, tens of thousands of concurrent transactions are no longer impossible. For higher throughput and scalable performance, robust and easy client development framework has become the pursuit of developers, and Netty perfectly meets the needs of people. It encapsulates Java’s complex underlying apis and exposes them in an easy-to-use way, enabling netty to focus more on the development of business logic rather than the trivial underlying architecture.

The following are the core components of Netty, which will be detailed in a follow-up note:

  • Channel.

    A channel is a core concept of Java NIO and represents an operational connection to an entity (such as a network connection, file I/O operation).

  • The callback.

    A callback is actually a method, and when a method is called, its specified (or bound) callback method is also called.

  • The Future.

    A Future is a placeholder for an asynchronous operation whose corresponding Future object is invoked when the asynchronous operation completes. Netty’s Future implementation, ChannelFuture, allows an asynchronous operation to register multiple ChannelFutureListener instances. Each netty outbound I/O returns a ChannelFuture instance.

  • Events and handlers.

    Netty notifies us of a change in the state of an operation through events such as:

    • Connection activation or deactivation (inbound event).
    • Data reads (inbound events).
    • User events (inbound events).
    • Error events (inbound events).
    • Open or close a connection to a remote node (outbound event).
    • Write data to the socket (outbound event).

    Netty has many implementations for ChannelHandler, and you can also customize the implementation. Each event is distributed to a method in the corresponding ChannelHandler class.

Note: Inbound and outbound are relative to ChannelHandler. Inbound is inbound and outbound is outbound.

Code sample

Here is a simple netty use:

Inbound processor implementation

@ChannelHandler.Sharable // Identifies a channelHandler that can be safely called by multiple channels.
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    // This method is called when there is an inbound message
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer = (ByteBuf)msg;
        System.out.println("Server receives message :" + buffer.toString(CharsetUtil.UTF_8));
        // Write the received message to the sender without flushing the outbound message.
        ctx.write(buffer);
    }

    @Override
    // channelRead is triggered when it finishes consuming read data
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // Flush pending messages to the remote node and close the channel
        ChannelFuture channelFuture = ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
        channelFuture.addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    // Handle exceptions during read operations
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // Prints the exception stack and closes the channelcause.printStackTrace(); ctx.close(); }}Copy the code

Note: Netty server handler processing is in the form of a chain of responsibility. By default, channelHandler forwards method calls to it to the next channelHandler in the chain. If the exceptionCaught() method is not implemented somewhere in the chain, the received exception is passed to the end of the channelPipeline and logged.

Server implementation

public class NettyServer {
    public static void main(String[] args) {
        NettyServer nettyServer = new NettyServer();
        nettyServer.start(8888);
    }

    public void start(int port){
        // Process TCP connection requests
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // Handle I/O events
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            // Used to boot and bind the server
            ServerBootstrap bootstrap = new ServerBootstrap();
            // Add the above thread group to the bootstrap
            bootstrap.group(bossGroup,workGroup)
                    // Set the channel to be asynchronous
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // Since NettyServerHandler is marked @sharable, the same instance can be used
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG,200)
                    .childOption(ChannelOption.SO_KEEPALIVE,true);
            // Asynchronously bind the server, calling sync() to block and wait until the bind is complete.
            ChannelFuture future = bootstrap.bind(port).sync();
            // Get closeFuture for the channel and block until it completes.
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally{ bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); }}}Copy the code

Outbound processor implementation

@ChannelHandler.Sharable
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
       ctx.writeAndFlush(Unpooled.copiedBuffer(Netty "active", CharsetUtil.UTF_8));
    }

    // Records received message stores
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("Client receives message:" + msg.toString(CharsetUtil.UTF_8));
    }

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

SimpleChannelInboundHandler and ChannelInboundHandler:

Clients use SimpleChannelInboundHandler asynchronous operations because it didn’t need to be considered when channelRead0 method after the execution, SimpleChannelInboundHandler will release the memory ByteBuffer to save this message.

The server uses ChannelInboundHandler because it needs to send messages to the client. The ctx.write() method is asynchronous and may not return after the channelRead() method has executed. So ChannelInboundHandler is used to avoid this. The channelReadComplete method is fired when channelRead() has finished consuming the read data, flushing output to the channel.

Client-side implementation

public class NettyClient {
    public static void main(String[] args) {
        NettyClient nettyClient = new NettyClient();
        nettyClient.connect("localhost".8888);
    }

    public void connect(String hostname,int port) {
        // Process TCP connection requests
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // Used to boot and bind the server
            Bootstrap bootstrap = new Bootstrap();
            // Add the above thread group to the bootstrap
            bootstrap.group(group)
                	// Set the channel to be asynchronous
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline()
                                    .addLast(new NettyClientHandler());
                        }
                    })
                    .option(ChannelOption.TCP_NODELAY,true);

            // Connect to the remote node, block and wait until the connection is complete.
            ChannelFuture future = bootstrap.connect(hostname, port).sync();
            // block until the channel is closed.
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // Close the thread pool and release all resources.group.shutdownGracefully(); }}}Copy the code

conclusion

As you can see from the code examples above, the Netty server and client are built much the same, implementing the processor and binding it.

Server-side development:

  1. Create and implement processor logic.
    1. The application expands as requiredChannelHandler.
    2. Called for different types of eventsChannelHandler.
  2. createServerBootstrapInstance to boot and bind the server.
  3. Create and assign oneNioEventLoopGroupInstance for request processing.
  4. Create and assign oneNioEventLoopGroupInstance for event handling.
  5. Specifies the local to which the server is boundInetSocketAddress.
  6. Initialize each new one with a processor instancechannel.
  7. callServerBootstrap.bind()Method to bind the server.

Client development:

  1. Create and implement processor logic.
    1. The application expands as requiredChannelHandler.
    2. Called for different types of eventsChannelHandler.
  2. Create a Bootstrap instance to initialize the client.
  3. Create and assign oneNioEventLoopGroupInstance for event handling.
  4. Create for the serverInetSocketAddressInstance.
  5. When a connection is established, oneHandlerWill be installed to thechanneltheChannelPiplineOn.
  6. callBootstrap.connect()Method to connect to a remote node.

Earlier, we took a look at Netty, including its core content, simple use. We will continue to look at the main components and design concepts of Netty.

Netty component design

The Channel interface

We know that the core components of NetTY include channels, an important concept in Java that represents a connection to an entity operation. Netty abstracts several operations as channels, including:

  • A Socket operation.
  • Multithreading.
  • Asynchronous notification.

We know that the basic I/O operations in the network (connection establishment, data reading, data writing) are dependent on the interface provided by the underlying network transport, which is represented by the Socket class in Java. Netty further encapsulates this class and greatly reduces the complexity of the Socket class. It provides a series of implementations based on the Channel interface:

  • EmbeddedChannel
  • EpollDatagramChannel
  • LocalServerChannel
  • KQueueDatagramChannel
  • NioDatagramChannel
  • NioSctpChannel
  • NioSocketChannel

As shown in the figure, each Channel is assigned a ChannelPipline and a ChannelConfig, which contains all the configuration for the Channel and supports hot updates. The default ChannelConfig is normally created when a Channel instance is created:

// NioServerSocketChannel constructor
public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
Copy the code

Netty provides a ChannelOption class that defines all the parameter types supported by ChannelConfig, which can be used as follows:

NioServerSocketChannel channel = new NioServerSocketChannel();
ServerSocketChannelConfig config = channel.config();
config.setOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
// For each ChannelOption option, Netty also provides a corresponding method, such as the above setting can be used instead
// config.setAllocator(PooledByteBufAllocator.DEFAULT);
// Set the channel
bootstrap.channel(channel.getClass());
Copy the code

The Comparable interface is implemented to ensure that channels are sequential. So if two different channels have the same hash value, an error will be thrown.

Channel also provides other methods, the main ones are:

The method name describe
eventLoop Return the corresponding EventLoop
pipeline Returns the corresponding ChannelPipline
localAddress Return the local SocketAddress
remoteAddress Return the remote SocketAddress
write Write data to the remote node, and this data will be passed to ChannelPipline and queued until it is flushed
flush Flush the previously written data to the remote node
writeFlush Write data and flush it to the remote node
isActive Checks whether the Channel is active

Netty supported transport

NIO

NIO implements all asynchronous I/O operations with a Selector, which runs on a thread that checks for state changes and reacts to them. The Selector patterns by Java. Nio. Channels. SelectionKey definition:

The name of the describe
OP_ACCEPT Get notified when you accept a connection and create a Channel
OP_CONNECT Get notified when a connection is established
OP_READ Get notified when data is available for reading
OP_WRITE Gets notification when a Channel’s send buffer is writable

Epoll

Epoll is a high-performance, extensible I/O event notification feature from Linux. Netty provides an API for Epoll on Linux.

OIO

OIO (Old IO) is the old blocking I/O that can be used to transition during project migration through the normal transport API.

Local

It is a local transport provided by Netty for asynchronous communication between clients and servers running in the same JVM.

Embedded

Embedded transports make it possible to embed a set of ChannelHandlers inside other ChannelHandlers as helper classes, extending the functionality of a Single ChannelHandler without modifying the internal code. The key to Embedded transport is the Channel implementation of the EmbeddChannel.

The lifecycle of a Channel

A Channel defines a set of state modes associated with ChannelInboundHandler:

state describe
ChannelUnregistered A Channel has been created but is not registered with EventLoop
ChannelRegistered Channels are registered with EventLoop
ChannelActive A Channel is active and can receive and send data
ChannelInactive The Channel is inactive

Unit testing based on EmbeddedChannel

EmbeddedChannel is provided by netty specifically to improve unit testing for ChannelHandler. You can use it to simulate send and request messages to test the functional implementation of the corresponding ChannelHandler.

EmbeddedChannel provides the following commonly used apis:

API describe
writeInbound Write an inbound message to the EmbeddedChannel. Returns true if the data can be read from the EmbeddedChannel via readInbound();
readInbound Read the inbound message from the EmbeddedChannel. Any return traverses the entire ChannelPipeline. If the read is not ready, this method returns NULL;
writeOutbound Write an outbound message to an EmbeddedChannel. Returns true if the data can be read from the EmbeddedChannel through readOutbound();
readOutbound Read the outbound message from the EmbeddedChannel. Any return traverses the entire ChannelPipeline. If the read is not ready, this method returns NULL;
Finish If data can be read from the inbound or outbound, the flag EmbeddedChannel completes and returns. This also calls the closed method of the EmbeddedChannel;

@Test
public void testFramesDecoded(a){
    ByteBuf buf= Unpooled.buffer();
    for (int i=0; i<9; i++){ buf.writeByte(i); } ByteBuf input=buf.duplicate(); EmbeddedChannel channel=new EmbeddedChannel(
        new FixedLengthFrameDecoder(3)); assertTrue(channel.writeInbound(input.retain())); assertTrue(channel.finish());// Read the message
    ByteBuf read=channel.readInbound();
    assertEquals(buf.readSlice(3),read);
    read.release();

    read = (ByteBuf) channel.readInbound();
    assertEquals(buf.readSlice(3), read);
    read.release();
    read = (ByteBuf) channel.readInbound();
    assertEquals(buf.readSlice(3), read);
    read.release();
    assertNull(channel.readInbound());
    buf.release();
}
Copy the code

EventLoop interface

EventLoop defines netTY’s core abstraction for handling what happens during the lifetime of a connection.

From the picture above, we can see that:

  • An EventLoopGroup contains one or more Eventloops.
  • An EventLoopGroup can be bound to only one Thread during its lifetime.
  • All I/O events processed by EventLoop will be processed on its proprietary Thread.
  • A Channel can register with only one EventLoop during its lifetime.
  • An EventLoop may be assigned to one or more channels.

Netty uses EventLoop to handle request tasks in connection. EventLoop uses two basic apis: networking and concurrent programming. The io.netty.util.concurrent package is built on the JUC package to provide an executor for a thread. The IO. Util. channel package classes extend these classes and interfaces to interact with channel events.

The task scheduling of netty extended JUC SecheduledExecutorService, because netty timing task can fit into a EventLoop execution queue, don’t need to switch from the thread like JUC, so reduces the performance overhead:

ctx.channel().eventLoop().schedule(new Runnable() { // Create a task processing thread
            @Override
            public void run(a) {
                System.out.println(EventLoop Task Scheduling); }},60, TimeUnit.MICROSECONDS); // Specify the scheduling period
Copy the code

ChannelFuture interface

Netty provides ChannelFuture interface is used to as a placeholder of asynchronous invocation, ChannelFuture. The addListener () method to register a ChannelFutureListener, in order to be notified when an operation to complete.

ChannelHandler interface

The ChannelHandler interface becomes the container for application logic that handles inbound and outbound data.

Netty provides a number of default ChannelHandler implementations in adapter mode, designed to simplify the development of application processing logic. Here are the adapter classes that are common when writing a custom ChannelHandler:

  • ChannelHandlerAdapter
  • ChannelInbouAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandler

ChannelHandler has a life cycle that calls operations when a ChannelHandler is added or removed from a ChannelPipline.

type describe
handlerAdded ChannelHandler is added to ChannelPipline
handlerRemoved ChannelHandler was removed by ChannelPipline
exceptionCaught An error occurred in ChannelPipline

We can customize our own processing logic by implementing the ChannelInboundHandler or ChannelOutboundHandler interface, Also can be done by extending ChannelInboundHandlerAdapter or ChannelOutboundHandlerAdapter class.

Resource management: As the processor processes data, we need to ensure that there is no resource leakage at the end of the process, and the ByteBuf reference counting technique described later addresses this problem. Netty provides resource Detector to detect memory leaks. Leakage level can be through the Java – Dio.net ty. To specify leakDetectionLevel = leakage level. The leak levels defined by Netty are:

level describe
DISABLED Disable leak detection
SIMPLE Use a default sampling rate of 1% and report found leaks (default level)
ADVANCED Use a 1% sampling rate and report the leaks found and the corresponding locations where the messages were accessed
PARANOID Sample each message access and report the leaks found and the corresponding access location

Encoders and decoders are typical ChannelHandler implementations.

In server development, pay attention to resolving sticky packages. The general solution is to define a protocol format and parse the data according to the protocol upon receiving the information. Netty provides different types of abstractions for different format designs, most of which are named XxxDecoder and XxxEncoder, such as ProtobufEncoder and ProtobufDecoder, which support Google Protocol Buffers.

Netty decoding encoders can be broadly divided into two categories:

  • From ByteToMessage (ByteToMessage)
  • From MessageToMessage

We can implement our own processors by extending netty’s preset decoders and encoders. For each message read from an inbound Channel, after the channelRead() method completes, it calls the decode() method provided by the decoder and forwards the decoded bytes to the next ChannelInboundHandler in the ChannelPipline. The exit is similar.

decoder

Netty bytes to the news of the encoder provides a base class: ByteToMessageDecoder (inherited ChannelInboundHandlerAdapter), the class will be on inbound data buffer, until ready to process. It has two most important methods:

API describe
decode(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) Abstract methods that must be implemented. The decode() method is called with a ByteBuf containing the incoming data and a List to add decoded messages to. The call to this method is repeated until it is determined that no new elements have been added to the List or that there are no more bytes in the ByteBuf to read. Then, if the List is not empty, its contents will be passed to the next ChannelInboundHandler in the ChannelPipeline.
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List< Objec t> out) This method is called once when a Channel’s state becomes inactive. The default is to call the decode() method.

ReplayingDecoder extends ByteToMessageDecoder (inherited ChannelInboundHandlerAdapter), ReplayingDecoder when processing the data don’t have to determine the length of the receiving data. ReplayingDecoder implements its own ReplayingDecoderByteBuf, throws an exception when there is not enough data, and then ReplayingDecoder resets the readerIndex and calls the decode method again.

Although ReplayingDecoder is more convenient than ByteToMessageDecoder, in fact, ReplayingDecoder runs slightly slower than ByteToMessageDecoder.

Netty provides a base class for message-to-message encoder implementations: MessageToMessageDecoder. It has the most important method:

API describe
decode(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) Abstract methods that must be implemented. The decode() method is called with a ByteBuf containing the incoming data and a List to add decoded messages to. The call to this method is repeated until it is determined that no new elements have been added to the List or that there are no more bytes in the ByteBuf to read. Then, if the List is not empty, its contents will be passed to the next ChannelInboundHandler in the ChannelPipeline.

Netty provides a TooLongFrameException exception, which is thrown when the decoder exceeds a specified frame size limit, preventing the decoder from running out of memory by buffering large amounts of data.

Decoding delimite-based protocols: The Delimited messaging protocol uses defined characters to mark the beginning or end of a message or message segment (frame). Decoders for handling delimiter based and length based protocols are:

The name of the describe
DelimiterBasedFrameDecoder A generic decoder to extract frames using any user-supplied delimiter
LineBasedFrameDecoder A decoder that extracts frames separated by line terminators (\n or \r\n). The decoder is quicker than the DelimiterBasedFrameDecoder

Decoding length-based protocols: A length-based protocol defines a frame by encoding its length to the frame’s header, rather than using a special delimiter to mark its end. Decoders for length based protocols are:

The name of the describe
FixedLengthFrameDecoder Extracts the fixed-length frame specified when the constructor is called
LengthFieldBasedFrameDecoder Extract the frame according to the length value in the encoding frame header; The offset and length of this field are specified in the constructor

The encoder

The encoder implements the ChannelOutboundHandler and converts outbound data from one format to another.

MessageToByteEncoder is the base class for converting messages to bytes. The most important method is:

API describe
encode(ChannelHandlerContext ctx,I msg,ByteBuf out) Abstract methods that must be implemented. Is called to pass in an outbound message of type I to be encoded by the class as ByteBuf. This ByteBuf will then be forwarded to the next ChannelOutboundHandler in the ChannelPipeline

ByteToMessageDecoder has more decodeLast methods than MessageToByteEncoder because the decoder usually needs to produce the last message after a Channel is closed.

MessageToMessageEncoder is the base class for converting messages to messages. The most important methods are:

API describe
encode(ChannelHandlerContext ctx,I msg,List< Object > out) Abstract methods that must be implemented. Each message written by the write() method is passed to the encode() method to encode one or more outbound messages. These outbound messages are then forwarded to the next ChannelOutboundHandler in the ChannelPipeline

codecs

From above, we know that the encoder is inherited ChannelInboundHandlerAdapter, and inherited ChannelOutboundHandlerAdapter decoder, so if we implement these two features at the same time, whether it can be integrated into the encoding and decoding in a class? Netty provides us with both byte and message codecs.

ByteToMessageCodec: Abstract class that combines ByteToMessageDecoder and MessageToByteEncode. This class consists of three important methods:

API describe
decode(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) This method will be called whenever there are bytes available to consume. It converts the inbound ByteBuf to the specified message format and forwards it to the next ChannelInboundHandler in the ChannelPipeline
decodeLast(ChannelHandlerContext ctx,ByteBuf in,List< Object > out) The default implementation of this method delegates to the decode() method. It is called only once when a Channel’s state becomes inactive. It can be overridden to implement special processing.
encode(ChannelHandlerContext ctx,msg,ByteBuf out) This method will be called for every message of type I that will be encoded and written to outbound ByteBuf

Message codec: The MessageToMessageCodec abstract class, which enables round-tripping of this transformation in a single class by using MessageToMessageCodec. Two important methods of this class:

API describe
protected abstract decode(ChannelHandlerContext ctx,INBOUND_IN msg,List< Object > out) This method is called and passed a message of type INBOUND_IN. It decodes them into messages of type OUTBOUND_IN, which are forwarded to the next ChannelInboundHandler in the ChannelPipeline
protected abstract encode(ChannelHandlerContext ctx,OUTBOUND_IN msg,List< Object > out) This method will be called for each message of type OUTBOUND_IN. These messages will be encoded as INBOUND_IN messages and forwarded to the next ChannelOutboundHandler in the ChannelPipeline

CombinedChannelDuplexHandler encoder and the decoder can be grouped to, as shown in the following cases:

// ByteToCharDecoder is a custom encoder, CharToByteEncoder is a custom decoder
public class CombinedByteCharCodec extends
    CombinedChannelDuplexHandler<ByteToCharDecoder.CharToByteEncoder> {
    public CombinedByteCharCodec(a) {
    super(new ByteToCharDecoder(), newCharToByteEncoder()); }}Copy the code

Serialization and deserialization

Netty provides three serialization methods:

  1. Serialization comes with the JDK.

    The name of the describe
    CompatibleObjectDecode Decoders that interoperate with non-Netty based remote nodes that use JDK serialization
    CompatibleObjectEncoder Encoders that interoperate with non-Netty based remote nodes serialized with the JDK
    ObjectDecoder Decoders built on JDK serialization that use custom serialization to decode; It provides an improvement in speed when there are no other external dependencies. Otherwise, other serialization implementations are preferable
    ObjectEncoder Encoders built on JDK serialization that are encoded using custom serialization; It provides an improvement in speed when there are no other external dependencies. Otherwise, other serialization implementations are preferable
  2. Serialization with JBoss: JBoss not only fixes some issues with the JDK’s built-in serializer, but also improves performance.

    The name of the describe
    CompatibleMarshallingDecoder,CompatibleMarshallingEncoder Compatible with remote nodes that only use JDK serialization
    MarshallingDecoder,MarshallingEncoder This applies to nodes using JBoss Marshalling. These classes must be used together
  3. Serialization using Protocol Buffers: Protocol Buffers is a data exchange format developed and open-source by Google.

    The name of the describe
    ProtobufDecoder The message is decoded using protobuf
    ProtobufEncoder The message is encoded using protobuf
    ProtobufVarint32FrameDecoder Dynamically split the received ByteBuf based on the value of the “Base 128 Varints” integer length field of Google Protocol Buffers in the message
    ProtobufVarint32LengthFieldPrepender Append a length field of Google Protocal Buffers integer “Base 128 Varints” to ByteBuf

Preset ChannelHandler

Using HTTPS: Netty uses the Javax.net.ssl package through SslHandler to implement SSL encryption. It also provides an SSLEngine implementation of the OpenSSL tool, OPenSSLEngine, which performs better than the JDK’s SSLEngine. By default, Netty tries to load OpenSSLEngine, and if it fails to load JdkSSLEngine. Its related methods are:

API describe
setHandshakeTimeout (long,TimeUnit),setHandshakeTimeoutMillis (long),getHandshakeTimeoutMillis() Set and get the timeout period after which handshake ChannelFuture will be notified of failure
setCloseNotifyTimeout (long,TimeUnit),setCloseNotifyTimeoutMillis (long),getCloseNotifyTimeoutMillis() Sets and gets the timeout period, after which a close notification is triggered and the connection is closed. This will also cause notification of the ChannelFuture to fail
handshakeFuture() Returns a ChannelFuure that will be notified when the handshake is complete. If the handshake has already been performed, a ChannelFuture containing the results of the previous handshake is returned
close(),close(ChannelPromise),close(ChannelHandlerContext,ChannelPromise) Send close_notify to request that the underlying SslEngine be turned off and destroyed

Http codecs: Netty provides multiple ChannelHandlers for formatting data into Http responses or parsing requests into data. The following figure shows the components of an HTTP request response:

The main codecs are:

The name of the describe
HttpRequestEncoder Encode HttpRequest, HttpContent, and LastHttpContent messages into bytes
HttpResponseEncoder Encode HttpResponse, HttpContent, and LastHttpContent messages as bytes
HttpRequestDecoder Decode the bytes into HttpRequest, HttpContent, and LastHttpContent messages
HttpResponseDecoder Decode the bytes into HttpResponse, HttpContent, and LastHttpContent messages

HTTP aggregation: For some request or response data, Netty’s codecs may not parse it in its entirety, but instead parse it into multiple pieces of data, such as HttpServerCodec, which can only get parameters in the URI. If a POST request is used, the information is stored in messageBody, so it cannot be fully parsed. You need to add HttpObjectAggregator. The HttpObjectAggregator architecture is as follows:

MessageAggregator decode() has a currentMessage parameter, which is a member variable of the handler. Each channel corresponds to a handler instance, and currentMessage stores the results of multiple decode iterations. This is the key to aggregation implementation.

HTTP compression: Although HTTP data compression costs the server clock, it saves network traffic and speeds up transmission rates. The client can use HttpContentDecompressor to process the content from the server, and the server uses HttpContentCompressor() to compress the data.

Websocket: Netty provides a variety of frameworks for implementing WebSocket long connections. Here are the websocketFrame types:

The name of the describe
BinaryWebSocketFrame Data frame: Binary data
TextWebSocketFrame Data frame: Text data
ContinuationWebSocketFrame Data frame: the text or binary data belonging to the previous BinaryWebSocketFrame or TextWebSocketFrame
CloseWebSocketFrame Control frame: a CLOSE request, the status code for closing, and the reason for closing
PingWebSocketFrame Control frame: request a PongWebSocketFrame
PongWebSocketFrame Control frame: response to a PingWebSocketFrame request

To add security to the WebSocket, simply add SslHandler as the first ChannelHandler to the ChannelPipeline.

Connection management: Netty provides a manager for detecting and processing idle and timeout connections. The main ones are:

The name of the describe
IdleStateHandler An IdleStateEvent event is emitted when the connection is idle for too long. You can then handle the IdleStateEvent event by overriding the userEventTriggered() method in ChannelInboundHandler
ReadTimeoutHandler If no inbound data is received within the specified time interval, a ReadTimeoutException is thrown and the corresponding Channel is closed. This ReadTimeoutException can be detected by overriding the exceptionCaught() method in ChannelHandler
WriteTimeoutHandler If no outbound data is written within the specified time interval, a WriteTimeoutException is thrown and the corresponding Channel is closed. You can detect this WriteTimeoutException by overriding the exceptionCaught() method of your ChannelHandler

ChannelPipline interface

The ChannelPipline interface implements chained calls to add ChannelHandler containers. When a Channel is created, it is automatically assigned to the ChannelPipline it belongs to.

As shown, when an inbound message comes in, it starts at the head of the ChannelPipline and is passed to the first ChannelboundHandler. After this ChannelHandler has been processed, It is passed to the next ChannelInboundHandler until it reaches the end of the ChannelPipline. The outbound of messages is similar to the inbound.

When a ChannelHandler is added to a ChannelPipline, it is assigned a ChannelHandlerContext that represents the binding between ChannelHandler and ChannelPipline.

In Netty, there are two ways to send messages:

  1. Write directly to a Channel, which causes messages to flow from the end of the ChannelPipline.
  2. Write directly to the ChannelHandlerContext, which will allow messages to flow from the next ChannelHandlerContext in the ChannelPipline.

As ChannelPipline propagates an event, it tests whether the type of the next ChannelHandler in ChannelPipline matches the direction of the event’s movement, and if it doesn’t, it skips the ChannelHandler.

ChannelHandlerContext

ChannelHandlerContext is used to enable ChannelHandler to interact with ChannelPipline. ChannelHandler can notify the next ChannelHandler of the ChannelPipline that it belongs to to modify its ChannelPipline.

ChannelHandlerContext API:

ChannelHandlerContext has some methods that Channel and ChannelPipline have as well. Note that if you call these methods on a Channel or Channel pipeline, They propagate along the entire ChannelPipeline. Calling the same method on ChannelHandlerContext will start with the currently associated ChannelHandler and will only propagate to the next ChannelHandler in the ChannelPipeline capable of handling the event.

ServerBootstrap/Boostrap

Netty’s Bootstrap class provides a container for the network layer configuration of the application. ServerBootstrap is used to boot the server and Bootstrap is used to boot the client.

We can see that both ServerBootstrap and Bootstrap implement AbstractBootstrap abstract classes.

In the previous article, when implementing the server-side bootstrap, we created and bound two EventLoopGroups. Why? Check the source code and we can see its explanation:

Server boot requires an EventLoopGroup to indicate that the server itself is bound to the socket that the local port is listening for, and a second group to handle client connections.

Bootstrap. Group (EventLoopGroup group) ¶ As in the first line of ServerBootstrap.group(EventLoopGroup parentGroup, EventLoopGroup childGroup).

ByetBuf

The basic unit of network transport is bytes, and Java’s ByetBuffer API can be used for byte manipulation. But ByteBuf has some limitations:

  1. Its length is fixed. Once the allocation is completed, the capacity cannot be dynamically expanded and contracted. When the POJO object to be encoded is larger than the capacity of ByteBuffer, index out-of-bounds exceptions will occur.

  2. ByteBuffer has only one pointer position that identifies the bit-control. Flip () and rewind() need to be manually called when reading and writing, which easily leads to program processing failure.

  3. API functions are limited, and some advanced and practical features need to be realized by user programming.

For this, Netty built on top of ByteBuffer to implement a new and powerful ByteBuf API with the following benefits:

  1. Can be extended by user-defined buffer types.
  2. Zero copy with built-in compound buffer.
  3. Capacity grows.
  4. Read and write use different Pointers.
  5. Method chain calls are supported.
  6. Support reference counting.
  7. Pooling is supported.

Usage mode of ByteBuf

Heap buffer

The heap-buffer mode is also known as the backing Array mode. To store the data in the JVM’s heap space by storing the data in arrays. The following is an example:

public static void heapBuffer(a) {
    // Create a Java heap buffer
    ByteBuf heapBuf = Unpooled.buffer(); 
    if (heapBuf.hasArray()) { // Check if there is a supporting array
        byte[] array = heapBuf.array(); // Get the support array reference
        int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); // Computes the first byte offset
        int length = heapBuf.readableBytes(); // Get the number of readable bytes
        handleArray(array, offset, length); // Call your own method}}Copy the code

Direct buffer

The direct buffer is direct memory allocated out of the heap and does not occupy the capacity of the heap. It is suitable for socket transmission, avoiding the process of copying data from internal buffer to direct buffer, and has good performance. The main disadvantages are that they are expensive to allocate and release compared to heap-based buffers, and because the data is not on the Java heap, you need to copy it again before processing.

public static void directBuffer(a) {
    ByteBuf directBuf = Unpooled.directBuffer();
    if(! directBuf.hasArray()) {// If it is not the heap buffer
        int length = directBuf.readableBytes(); // Get the number of readable bytes
        byte[] array = new byte[length]; // Allocate a new array to hold the data
        directBuf.getBytes(directBuf.readerIndex(), array); // Copy the data to the new array
        handleArray(array, 0, length); // Call your own method}}Copy the code

Compound buffer

Compound buffers are netty specific buffers. Essentially similar to providing a composite view of one or more BytebuFs, different types of BytebuFs can be added and removed as needed. The composite buffer does not support access to its supporting array. Therefore, if you want to access, you need to copy the content to the heap memory before accessing it.

public static void byteBufComposite(a) {
    // Composite buffer, just provide a view
    CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
    ByteBuf headerBuf = Unpooled.buffer();
    ByteBuf bodyBuf = Unpooled.directBuffer();
    messageBuf.addComponents(headerBuf, bodyBuf); // Append the ByteBuf instance to the CompositeByteBuf
    messageBuf.removeComponent(0); // Remove the buffer at index position 0
    for (ByteBuf buf : messageBuf) { // Iterate over the bufferSystem.out.println(buf.toString()); }}Copy the code

Byte-level operation

Random access indexes: ByteBuf’s indexes start at 0.

public static void byteBufRelativeAccess(a) {
    ByteBuf buffer = Unpooled.buffer();
    for (int i = 0; i < buffer.capacity(); i++) {
        byte b = buffer.getByte(i);// Leave readerIndex unchanged
        System.out.println((char) b); }}Copy the code

Methods that require only one index parameter do not change readIndex and writeIndex, although they can be changed manually by calling readerIndex(index) and writeIndex(index).

Discardable bytes: DiscardReadBytes are the area between [0, readerIndex]. The discardReadBytes() method can be called to discard bytes that have been read. The discardReadBytes() method moves the CONTENT of the readable byte region. Multiple data replication costs may affect performance

Readable bytes: The readable byte area is the area between [readerIndex, writerIndex]. Any operation of readxxx() and skipxxx() will change the readerIndex index.

Writable section: The writable section area is the area between [writerIndex, capacity]. Any operation with writexxx() will change the value of writerIndex.

Index management:

  1. MarkReaderIndex (), markWriterIndex() — marks the current position of the stream.
  2. ResetReaderIndex (), resetWriterIndex() — resets the stream to the marker position.
  3. ReadIndex (index), writeIndex(int) – moves the read/writeIndex to the specified position.
  4. Clear () — Sets readerIndex and writeerIndex to 0, but the contents of memory are not cleared.

Find operation: The simplest way to find a value specified by ByteBuf is to use indexOf(). More complex methods can be used to find ByteBufProcessor. For example, int index = buffer.foreachbyte (byteprocessor.find_cr); .

Derived buffer: Derived buffer provides an accessible view for ByteBuf. A view provides only one access and does not do any copying. If you modify the contents of the new ByteBuf instance, the corresponding source instance will also be modified. If you need to copy a true copy of an existing buffer, use copy() or copy(int, int).

Read/write operations: There are two types of read/write operations:

  1. Get () and set() operations — start with the given index and leave the index unchanged.

  2. Read () and write() operations — start with a given index and access the index based on the number of bytes already accessed.

ByteBufHolder interface

ByteBufHolder is an advanced feature of Netty that provides support for buffer pooling. The ByteBufHolder interface can be subclassed to add data fields as needed. Can be used to customize buffer type extension fields.

ByetBuf distribution

To each according to his need

Netty implements pooling (ByteBuf) through the ByteBufAllocator interface. A reference to ByteBufAllocator can be obtained through Channel or ChannelHandler’s ChannelHandlerContext.

Netty provides two implementations of Byte locator: PooledByte Locator (pooled) and unpolledByte Byte Locator (non-pooled). PooledByteBufAllocator is used by netty by default, but can also be changed through ChannelConfig.

Unpooled buffer

In the absence of a ByteBufAllocator reference, we can use netty’s Unpooled utility class to create an Unpooled ByteBufAllocator.

ByteBufUtil class

The ByteBufUtil class provides static helper methods for manipulating ByteBuf. The hexdump() method is greater than the ByteBuf content in a hexadecimal representation. The equals(ByteBuf, ByteBuf) method is used to determine whether two instances of ByteBuf are equal.

Reference counting

Netty introduced reference counting technology for ButeBuf and ButeBufHolder by implementing the ReferenceCounted interface. When the count is zero, the system reclaims the buffer, which reduces the overhead of memory allocation.