Introduction

In the previous section, we explained the Channel in netty and learned that the channel is the bridge between the event processor and the external connection. Today this article will explain in detail the remaining few very important parts of netty Event, Handler and PipeLine.

ChannelPipeline

PipeLine is a bridge connecting Channel and handler. It is actually an implementation of filter, which is used to control the processing method of handler.

When a channel is created, its corresponding ChannelPipeline will also be created.

First look at the definition of ChannelPipeline:

public interface ChannelPipeline
        extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable 

First, ChannelPipeline inherits from Iterable, indicating that it is traversable, and the result of the traversal is one of the Handlers.

As a qualified Iterable, ChannelPipeline provides a series of add and remote methods through which Handlers can be added or removed from ChannelPipeline. Because ChannelPipeline is a filter, and the filter needs to specify the order of the corresponding filter, there are addFirst and addLast methods in ChannelPipeline to add different orders.

Then you can see that ChannelPipeline inherits the two interfaces of ChannelInboundInvoker and ChannelOutboundInvoker.

First look at a work flow chart of channelPipeline:

It can be seen that ChannelPipeline has two main operations, one is to read Inbound, and the other is to write OutBound.

For read operations such as Socket.read(), the method in ChannelInboundInvoker is actually called. For external IO write requests, the method in ChannelOutboundInvoker is called.

Note that the processing order of inbound and outbound is reversed, such as the following example:

    ChannelPipeline p = ...;
   p.addLast("1", new InboundHandlerA());
   p.addLast("2", new InboundHandlerB());
   p.addLast("3", new OutboundHandlerA());
   p.addLast("4", new OutboundHandlerB());
   p.addLast("5", new InboundOutboundHandlerX());

In the above code, we added 5 handlers to ChannelPipeline, including 2 InboundHandlers, 2 OutboundHandlers, and a Handler that handles both In and Out.

Then when the channel encounters an inbound event, it will be processed in the order of 1, 2, 3, 4, 5, but only the InboundHandler can handle the inbound event, so the actual execution order is 1, 2, 5.

Similarly, when the channel encounters an outbound event, it will be executed in the order of 5, 4, 3, 2, 1, but only the outboundHandler can handle the outbound event, so the actual execution order is 5, 4, 3.

Simply put, ChannelPipeline specifies the execution order of Handler.

ChannelHandler

netty is an event-driven framework, all events are processed by Handler. ChannelHandler can process IO, intercept IO, or pass the event to the next Handler in ChannelPipeline for processing.

The structure of ChannelHandler is very simple, with only three methods, namely:

void handlerAdded(ChannelHandlerContext ctx) throws Exception;
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;

According to the difference between inbound and outbound events, ChannelHandler can be divided into two categories, namely ChannelInboundHandler and ChannelOutboundHandler.

Because these two are interfaces, it is more troublesome to implement, so netty provides you with three default implementations: ChannelInboundHandlerAdapter, ChannelOutboundHandlerAdapter and ChannelDuplexHandler. The first two are well understood, namely inbound and outbound, and the last one can handle inbound and outbound at the same time.

ChannelHandler is provided by ChannelHandlerContext, and the interaction with ChannelPipeline is also carried out through ChannelHandlerContext.

ChannelHandlerContext

ChannelHandlerContext allows ChannelHandler to interact with ChannelPipeline or other Handlers. It is a context environment that enables Handler and Channel to interact.

For example, in ChannelHandlerContext, call channel() to get the bound channel. The bound Handler can be obtained by calling handler(). Trigger Channel events by calling the fire* method.

Look at the definition of ChannelHandlerContext:

public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker 

You can see whether it is an AttributeMap used to store attributes, or a ChannelInboundInvoker and ChannelOutboundInvoker used to trigger and propagate corresponding events.

For Inbound, the methods of propagating events are:

ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()

For Outbound, the methods of propagating events are:

ChannelHandlerContext.bind(SocketAddress, ChannelPromise)
ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise)
ChannelHandlerContext.write(Object, ChannelPromise)
ChannelHandlerContext.flush()
ChannelHandlerContext.read()
ChannelHandlerContext.disconnect(ChannelPromise)
ChannelHandlerContext.close(ChannelPromise)
ChannelHandlerContext.deregister(ChannelPromise)

These methods are called in a Handler, and then the event is passed to the next Handler, as shown below:

   public class MyInboundHandler extends ChannelInboundHandlerAdapter {
        @Override
       public void channelActive(ChannelHandlerContext ctx) {
           System.out.println("Connected!");
           ctx.fireChannelActive();
       }
   }
  
   public class MyOutboundHandler extends ChannelOutboundHandlerAdapter {
        @Override
       public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
           System.out.println("Closing ..");
           ctx.close(promise);
       }
   }

State variables in ChannelHandler

ChannelHandler is a Handler class. In general, instances of this class can be used by multiple channels, provided that this ChannelHandler has no shared state variables.

But sometimes, we have to maintain a state in the ChannelHandler, then it involves the problem of the state variables in the ChannelHandler. Look at the following example:

  public interface Message {
       // your methods here
   }
  
   public class DataServerHandler extends SimpleChannelInboundHandler<Message> {
  
       private boolean loggedIn;
  
        @Override
       public void channelRead0(ChannelHandlerContext ctx, Message message) {
           if (message instanceof LoginMessage) {
               authenticate((LoginMessage) message);
               loggedIn = true;
           } else (message instanceof GetDataMessage) {
               if (loggedIn) {
                   ctx.writeAndFlush(fetchSecret((GetDataMessage) message));
               } else {
                   fail();
               }
           }
       }
       ...
   }

In this example, after receiving the LoginMessage, we need to authenticate the message and save the authentication status. Because the business logic is like this, there must be a state variable.

Then the Handler with state variables can only be bound to one channel. If multiple channels are bound, the state may be inconsistent. A channel is bound to a Handler instance, it is very simple, just use the new keyword in the initChannel method to create a new object.

   public class DataServerInitializer extends ChannelInitializer<Channel> {
        @Override
       public void initChannel(Channel channel) {
           channel.pipeline().addLast("handler", new DataServerHandler());
       }
   }

So besides creating a new handler instance, is there any other way? Of course there is, that is the AttributeKey attribute in ChannelHandlerContext. Still the above example, let's take a look at how to use AttributeKey should be implemented:

   public interface Message {
       // your methods here
   }
  
    @Sharable
   public class DataServerHandler extends SimpleChannelInboundHandler<Message> {
       private final AttributeKey<Boolean> auth =
             AttributeKey.valueOf("auth");
  
        @Override
       public void channelRead(ChannelHandlerContext ctx, Message message) {
           Attribute<Boolean> attr = ctx.attr(auth);
           if (message instanceof LoginMessage) {
               authenticate((LoginMessage) o);
               attr.set(true);
           } else (message instanceof GetDataMessage) {
               if (Boolean.TRUE.equals(attr.get())) {
                   ctx.writeAndFlush(fetchSecret((GetDataMessage) o));
               } else {
                   fail();
               }
           }
       }
       ...
   }

In the above example, an AttributeKey is defined first, and then the Attribute is set to the ChannelHandlerContext using the attr method of the ChannelHandlerContext, so that the Attribute is bound to the ChannelHandlerContext. This attribute will be different even if the same Handler is used in different Channels.

The following is an example of using a shared Handler:

   public class DataServerInitializer extends ChannelInitializer<Channel> {
  
       private static final DataServerHandler SHARED = new DataServerHandler();
  
        @Override
       public void initChannel(Channel channel) {
           channel.pipeline().addLast("handler", SHARED);
       }
   }

Note that when defining the DataServerHandler, we added the @Sharable annotation. If a ChannelHandler uses the @Sharable annotation, it means that you can create the Handler only once, but you can bind it to one or more ChannelPipeline .

Note that the @Sharable annotation is prepared for the java document and will not affect the actual code execution effect.

Asynchronous Handler

As mentioned earlier, the handler can be added to the pipeline by calling the pipeline.addLast method. Because the pipeline is a filter structure, the added handlers are processed sequentially.

However, what should I do if I want some handlers to be executed in a new thread? What if we want the Handler executed in these new threads to be out of order?

For example, we now have 3 handlers namely MyHandler1, MyHandler2 and MyHandler3.

The wording of sequential execution is like this:

ChannelPipeline pipeline = ch.pipeline();
  
   pipeline.addLast("MyHandler1", new MyHandler1());
   pipeline.addLast("MyHandler2", new MyHandler2());
   pipeline.addLast("MyHandler3", new MyHandler3());

If you want MyHandler3 to execute in a new thread, you can add the group option to let the handler run in the new group:

static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
ChannelPipeline pipeline = ch.pipeline();
  
   pipeline.addLast("MyHandler1", new MyHandler1());
   pipeline.addLast("MyHandler2", new MyHandler2());
   pipeline.addLast(group,"MyHandler3", new MyHandler3());

But the Handler added by DefaultEventExecutorGroup in the above example will also be executed sequentially. If you really don't want to execute sequentially, you can try to consider using UnorderedThreadPoolEventExecutor.

Summarize

This article explains Event, Handler and PipeLine, and exemplifies the relationship and interaction between them. The follow-up will start from the specific practice of netty to further deepen the understanding and application of netty. I hope everyone will like it.

This article has been included in http://www.flydean.com/05-netty-channelevent/

The most popular interpretation, the most profound dry goods, the most concise tutorial, and many tips you don't know are waiting for you to discover!

Welcome to pay attention to my official account: "Program those things", know technology, know you better!


flydean
890 声望433 粉丝

欢迎访问我的个人网站:www.flydean.com