The evolution of network programming: a new chapter in Netty Channel

In the previous article (Introduction to Netty – ByteBuf, the carrier of Netty data transmission), we learned that Netty data is transmitted in ByteBuf units, but with data, you have no channel and the data cannot be transmitted, so today we will Familiar with Netty’s third core component: Channel. ByteBuf is data, and Channel is the channel responsible for transmitting data. It is the key to Netty communication. Without it, Netty cannot communicate.

Channel Introduction

In Java NIO, we know that Channel is a “pipeline” used to transmit data. It complements Buffer. In Java NIO, we can only read data from Channel to Buffer, or read from Buffer. Data is sent to Channel as follows:

There is also a Channel in Netty. TheChannel is one of the core concepts of Netty. It is an abstraction of Netty network IO operations, that is, the main body of Netty network communication. It is responsible for the network communication, registration, and data of the other end. Operations and all IO related operations, its main functions include:

  1. Network IO reading and writing
  2. Client initiates connection
  3. close connection
  4. Network connection related parameters
  5. Bind port
  6. Netty framework related operations, such as obtaining the EventLoop, pipeline, etc. associated with the Channel.

Why start a new business?

JDK provides a Channel, why does Netty have to start a new project to implement one by itself? I think the main reasons are as follows:

  1. The native Channel has too few functions and does not meet Netty’s requirements.
  2. The native ServerSocketChannel and SocketChannel are an SPI interface. The specific implementation is implemented by the virtual vendor. The native ServerSocketChannel and SocketChannel are directly implemented and meet the requirements of Netty. The workload is no less than re-developing one.
  3. Netty’s Channel needs to conform to Netty’s overall architecture design. It needs to be coupled with Netty’s overall architecture, such as IO models, configured TCP parameters based on metadata descriptions, etc., which are not supported by native Channels.
  4. Customized Channel has higher flexibility and more comprehensive functions.

Channel Principle

The core principle of Channel is as follows:

  1. After the client and server successfully establish a connection, the server will create a Channel for the connection.
  2. Channel obtains an EventLoop from the EventLoopGroup, and the Channel is registered in the EventLoop. From then on, the Channel is bound to the EventLoop, and will only be bound to the EventLoop during the entire life cycle of the Channel.
  3. IO operations initiated by the client will generate corresponding Events in the Channel, triggering the EventLoop bound to the Channel for processing.
  4. If it is a read or write event, the execution thread schedules ChannelPipeline to process the business logic. ChannelPipeline is only responsible for the orchestration of Handlers, and each specific ChannelHandler actually performs the task.

Channel state transition

From creation to death, Channel has four states, namely:

  1. Open:
    1. When a Channel is open, it means that it has been created but has not yet been bound to any address or connected to a remote server.
  2. Active status (Active):
    1. When the Channel is active, it means that it has successfully bound to the local address or connected to the remote server.
    2. At this time, you can call writeAndFlush() to send data to the other party.
  3. Inactive:
    1. When a Channel is inactive, it means that it has been active, but the connection has been dropped or is otherwise unavailable.
    2. When the connection is closed or an error occurs, the Channel becomes inactive.
    3. No read or write operations are possible, but the Channel can be reactivated.
  4. Closed:
    1. When a Channel is in the closed state, it means that it has been completely closed and no further operations can be performed.

The status flow is as follows:

Netty provides four methods to determine the status of Channel:

  • isOpen(): Check whether the Channel is open.
  • isRegistered(): Check whether the Channel is registered.
  • isActive(): Check whether the Channel is active.
  • isWritable(): This method is misleading. It does not determine whether the current Channel is writable. In fact, it is used to detect whether the write operation of the current Channel can be processed immediately by the IO thread. When this method When false is returned, any write requests will be blocked until the I/O thread is capable of processing them.

Each status and their corresponding operations are as follows:

Status isOpen() isActive() close() writeAndFlush() Read operation Write operation
Open (Open) true false true false true true
Active true true true true true true
Inactive true false true false false false
Closed false false false false false false

Channel API

There are many commonly used APIs for Channel, as shown below (part):

Although there are many methods, they can generally be divided into the following categories:

Class getter API

This method is mainly used to obtain Channel related properties, such as binding address, related configuration, etc.

  • SocketAddress localAddress(): Returns the local address bound to Channel
  • SocketAddress remoteAddress(): Returns the remote address bound to Channel
  • ChannelConfig config(): Returns a ChannelConfig object through which Channel-related parameters can be configured.
  • ChannelMetadata metadata(): Returns a ChannelMetadata object. ChannelMetadata can query whether the Channel implementation supports certain operations. Currently, it only has one method hasDisconnect(), which is used to determine the Channel implementation. Whether to support the disconnect() operation.
  • Channel parent(): Returns the parent Channel of Channel. SocketChannel returns a relative ServerSocketChannel, while ServerSocketChannel returns null. Why does SocketChannel return ServerSocketChannel? Because all SocketChannels (client-initiated connections) are created by ServerSocketChannel to accept connections, SocketChannel’s parent() returns the corresponding ServerSocketChannel.
  • EventLoop eventLoop(): Returns the EventLoop registered by the Channel.
  • ChannelPipeline pipeline(): Returns the ChannelPipeline associated with Channel.
  • ByteBufAllocator alloc(): Returns the ByteBufAllocator object associated with the Channel.
  • Unsafe unsafe(): Returns the Unsafe object of Channel. Unsafe is an internal class of Channel and is only used internally by Channel.
Future related API

All IO operations in Netty are asynchronous, which means that any IO call will return immediately, but there is no guarantee that all operations will be completed after the call ends, and we do not know the results of the IO operation execution. . Netty will return a Future object after completing the IO call, which is the result of Channel asynchronous IO. Channel provides us with methods to operate these Futures:

  • ChannelFuture closeFuture(): Returns a ChannelFuture when the Channel is closed. We can use this method to do some processing after the Channel is closed.
  • ChannelPromise voidPromise(): Returns a ChannelPromise instance object.
  • ChannelPromise newPromise(): Returns a ChannelPromise.
  • ChannelProgressivePromise newProgressivePromise(): Returns a new ChannelProgressivePromise instance object.
  • ChannelFuture newSucceededFuture(): Create a new ChannelFuture and mark it as succeed.
  • ChannelFuture newFailedFuture(Throwable cause): Create a new ChannelFuture and mark it as failed.

ChannelFuture and ChannelPromise are two special Futures provided by Netty. Using them, we can complete the processing of some asynchronous operations in Netty.

Judge status API

Channel provides four isXxx methods to determine the status of Channel:

  • boolean isOpen(): Determine whether the Channel is opened
  • boolean isRegistered(): Determine whether the Channel is registered
  • boolean isActive(): Determine whether the Channel is active
  • boolean isWritable(): Determine whether the Channel can handle the IO event immediately
Event triggering class method

These methods will trigger IO events, which will be propagated through ChannelPipeline and then processed by ChannelHandler.

  • ChannelFuture bind(SocketAddress localAddress): The server binds the local port and starts monitoring the client’s connection request.
  • ChannelFuture connect(SocketAddress remoteAddress): The client initiates a connection request to the server.
  • ChannelFuture disconnect(): Disconnect, but it should be noted that this method does not release resources. It can also establish a connection with the server again through connect().
  • ChannelFuture close(): Close the channel and release resources.
  • Channel read():Read channel
  • ChannelFuture write(Object msg): Write data to the Channel. This method does not actually write the data to the channel. It only writes the data to the on-off buffer. We need to call flush() Flushes the data in the buffer area into the Channel.
  • Channel flush(): Flush data into Channel.
  • ChannelFuture writeAndFlush(Object msg): Equivalent to calling write() and flush().

Channel configuration

ChannelConfig

In Netty, each Channel has a corresponding ChannelConfig, which can be obtained by calling config(). ChannelConfig is an interface, and each specific Channel has a specific ChannelConfig implementation class, for example:

  • The corresponding configuration class of NioSocketChannel is NioSocketChannelConfig.
  • The corresponding configuration class of NioServerSocketChannel is NioServerSocketChannelConfig.

The overall UML diagram is as follows:

The specific implementation of our article does not need to be related. What we need to focus on is what Config it provides.

  • ChannelConfig provides universal configuration

    • ChannelOption.CONNECT_TIMEOUT_MILLIS: Connection timeout, the default value is 30000 milliseconds, which is 30 seconds.
    • ChannelOption.WRITE_SPIN_COUNT: The maximum number of cycles for write operations, that is, the maximum number of times write() can be called during one write event processing. It’s a bit like a spin lock in Java. The main purpose of introducing this parameter is to avoid writing a large amount of data to one Channel and causing delays in the read and write processing of other network channels.
    • ChannelOption.ALLOCATOR: Set the memory allocator.
    • ChannelOption.RCVBUF_ALLOCATOR: Sets the memory allocator for read events.
    • ChannelOption.AUTO_READ: Configure whether to automatically trigger read(). The default is True. The program does not need to explicitly call read().
    • ChannelOption.AUTO_CLOSE: Configure whether to automatically close the Channel when writing events fails. The default is True.
    • ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK: Set the high water mark of the write buffer area. If the data in the write buffer exceeds this value, the Channel#isWritable() method will return false.
    • ChannelOption.WRITE_BUFFER_LOW_WATER_MARK: Set the low water mark of the write buffer area. If the data in the write buffer area exceeds the high water mark, the channel will become unwritable. After the write buffer data drops to the low water mark, the channel will return to the writable state (Channel#isWritable() will return again true).
    • ChannelOption.MESSAGE_SIZE_ESTIMATOR: Sets the detector used to detect channel message size: MessageSizeEstimator.

    Two concepts are introduced here: high water mark and low water mark. We will discuss these two concepts in detail when we talk about buffer zones.

  • NioSocketChannelConfig

    NioSocketChannelConfig adds the following configurations based on ChannelConfig:

    • ChannelOption.SO_KEEPALIVE: Connection maintenance, the default is False, we can regard this parameter as the heartbeat mechanism of TCP.
    • ChannelOption.SO_REUSEADDR: Address reuse, default value False.
    • ChannelOption.SO_LINGER: Delay time for closing Socket. The default value is -1, which means disabling this function.
    • ChannelOption.TCP_NODELAY: Send data immediately, the default value is True. This value actually sets the enablement of the Nagle algorithm. We will discuss the Nagle algorithm in detail later.
    • ChannelOption.SO_RCVBUF: TCP data receive buffer size. This buffer is the TCP receive sliding window.
    • ChannelOption.SO_SNDBUF: TCP data sending buffer size. This buffer is the TCP send sliding window.
    • ChannelOption.IP_TOS: IP parameter, sets the Type-of-Service field of the IP header, used to describe the priority and QoS options of the IP packet.
    • ChannelOption.ALLOW_HALF_CLOSURE: Whether the local end of a connection is closed when the remote end of the connection is closed. The default value is False.
  • NioServerSocketChannelConfig

    • ChannelOption.SO_REUSEADDR: Address reuse, default value False.
    • ChannelOption.SO_RCVBUF: TCP data receive buffer size. This buffer is the TCP receive sliding window.
    • ChannelOption.SO_BACKLOG: The queue length for the server to accept connections. If the queue is full, the client connection will be rejected.

From the above we can see that ChannelConfig provides some general configurations, while NioSocketChannelConfig and NioServerSocketChannelConfig provide basically Socket-related configuration parameters, each of which is defined with java.net.StandardSocketOptions The standard TCP parameters correspond one to one.

Since this is an introductory article, Brother Daming will not elaborate further here. A more detailed explanation of these configuration parameters will be provided by Brother Daming later. Here we only need to understand that ChannelConfig is the parameter related to the Channel channel that we configure. The service class is enough.

How to use Channel

After reading the above part, Brother Daming believes that you have a basic understanding of Channel. In fact, the API of Channel is nothing to demonstrate, because these APIs are not used alone and require some other components to cooperate. But we still need to have a sense of ritual, right? Let’s write a simple demo to see the status changes of Channel. And simply feel the asynchronous style.

  • Server
public static void main(String[] args) throws InterruptedException {
  Channel channel = new ServerBootstrap()
                  .group(new NioEventLoopGroup())
                  .channel(NioServerSocketChannel.class)
                  .childHandler(new ChannelInitializer<NioSocketChannel>() {
                      @Override
                      protected void initChannel(NioSocketChannel ch) throws Exception {
                          ch.pipeline().addLast(new LoggingHandler());
                      }
                  })
                  .bind(8081)
                  .channel();
  System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive() );

  System.out.println("eventLoop():" + channel.eventLoop());
  System.out.println("pipeline():" + channel.pipeline());

  TimeUnit.SECONDS.sleep(5);
  System.out.println("=============================");
  System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive() );
}
  • Client
public static void main(String[] args) throws InterruptedException {
    Channel channel = new Bootstrap()
            .group(new NioEventLoopGroup())
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler());
                }
            })
            .connect("127.0.0.1",8081)
            .channel();

    System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive() );

    System.out.println("eventLoop():" + channel.eventLoop());
    System.out.println("pipeline():" + channel.pipeline());

    TimeUnit.SECONDS.sleep(5);
    System.out.println("=============================");
    System.out.println("isOpen:" + channel.isOpen() + ";;;isRegistered:" + channel.isRegistered() + ";;;isActive:" + channel.isActive() );
}
  • Run results
// server
isOpen:true;;;isRegistered:false;;;isActive:false
eventLoop():io.netty.channel.nio.NioEventLoop@2f333739
pipeline():DefaultChannelPipeline{(ServerBootstrap$1#0 = io.netty.bootstrap.ServerBootstrap$1)}
=============================
isOpen:true;;;isRegistered:true;;;isActive:true

//client
isOpen:true;;;isRegistered:false;;;isActive:false
eventLoop():io.netty.channel.nio.NioEventLoop@6ed3ef1
pipeline():DefaultChannelPipeline{(ChannelTestClient$1#0 = com.sike.netty.rumen.ChannelTestClient$1)}
=============================
isOpen:true;;;isRegistered:true;;;isActive:true

It can be seen from the results that whether it is the server or the client, the Channel is asynchronous. When the server calls the bind() method, the Channel returned after it only completes the creation, registration and The binding work has not been completed. After waiting for 5 seconds, we look at its status and it is all true.

Code address: http://m6z.cn/5O6hON