Introduction

We know that channel is the bridge used to communicate ByteBuf and Event in netty. During the creation of netty service, whether it is Bootstrap on the client side or ServerBootstrap on the server side, you need to call the channel method to specify the corresponding channel type.

So what are the types of channels in netty? How exactly do they work? Take a look.

channel and ServerChannel

Channel is an interface in netty, and many very useful methods are defined in Channel. Generally speaking, if it is a client, the corresponding channel is an ordinary channel. If it is on the server side, the corresponding channel should be ServerChannel.

So what is the difference between a client-side channel and a server-side channel? Let's first look at the definition of ServerChannel:

public interface ServerChannel extends Channel {
    // This is a tag interface.
}

It can be seen that ServerChannel inherits from Channel, which means that the Channel of the server is also a kind of Channel.

But strangely, you can see that there are no new methods added to ServerChannel. That is to say, ServerChannel and Channel are essentially the same in definition. You can think of ServerChannel as just a tag interface.

So what is the relationship between channel and ServerChannel?

We know that a parent method is defined in Channel:

Channel parent();

The parent method returns the parent channel of the channel. Let's take the simplest LocalChannel and LocalServerChannel as examples to see how their parent-child relationship is created.

First, the value of parent is implemented through the common parent class AbstractChannel of LocalChannel and LocalServerChannel:

    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }

For LocalChannel, the parent channel can be set through its constructor:

    protected LocalChannel(LocalServerChannel parent, LocalChannel peer) {
        super(parent);
        config().setAllocator(new PreferHeapByteBufAllocator(config.getAllocator()));
        this.peer = peer;
        localAddress = parent.localAddress();
        remoteAddress = peer.localAddress();
    }

We know that when the client side wants to connect to the server side, it needs to call the connect method of the client channel. For LocalChannel, its connect method actually calls the connect method of the pipeline:

public ChannelFuture connect(SocketAddress remoteAddress) {
        return pipeline.connect(remoteAddress);
    }

Eventually, the LocalUnsafe.connect method in LocalChannel will be called.

In the LocalUnsafe.connect method, the serverChannel.serve method is called again.

The newLocalChannel method of serverChannel will create a new LocalChannel and return:

    protected LocalChannel newLocalChannel(LocalChannel peer) {
        return new LocalChannel(this, peer);
    }

The LocalChannel created by the newLocalChannel method here is the sub-channel of the serverChannel.

The finally returned LocalChannel will exist as the peer channel of the client-side LocalChannel.

Implementation of channel in netty

There are many implementation classes for channel and Serverchannel in netty, which are used to complete different business functions.

In order to understand the secrets of channels in netty step by step, let's first discuss the working principles of LocalChannel and LocalServerChannel, the basic implementations of channels in netty.

The following figure shows the main inheritance and dependencies of LocalChannel and LocalServerChannel:

<img src="https://img-blog.csdnimg.cn/1d9c19d567084c199dfade76c8a0d52a.png" style="zoom:67%;" />

As you can see from the figure, LocalChannel inherits from AbstractChannel and LocalServerChannel inherits from AbstractServerChannel.

Because ServerChannel inherits from Channel, it is natural that AbstractServerChannel inherits from AbstractChannel.

Next, we compare and analyze AbstractChannel and AbstractServerChannel, LocalChannel and LocalServerChannel to explore the underlying principles of channel implementation in netty.

AbstractChannel and AbstractServerChannel

AbstractChannel is the most basic implementation of Channel. Let's first look at those functions in AbstractChannel.

First, AbstractChannel defines some basic channel-related properties to be returned in the Channel interface, including parent channel, channel id, pipline, localAddress, remoteAddress, eventLoop, etc., as shown below:

    private final Channel parent;
    private final ChannelId id;
    private final DefaultChannelPipeline pipeline;
    private volatile SocketAddress localAddress;
    private volatile SocketAddress remoteAddress;
    private volatile EventLoop eventLoop;

    private final Unsafe unsafe;

It should be noted that there is also a very important Unsafe property in AbstractChannel.

Unsafe itself is an internal interface defined in the Channel interface, and its role is to provide specific implementations for different types of transport.

As can be seen from the name, Unsafe is an unsafe implementation. It is only used in the source code of netty, and it cannot appear in user code. Or you can think of Unsafe as the underlying implementation, and the AbstractChannel or other Channel that wraps it is the encapsulation of the underlying implementation. For ordinary users, they only need to use the Channel, and they do not need to go deeper into the lower layer. Content.

In addition, for Unsafe, in addition to the following methods, the remaining methods must be called from the I/O thread:

localAddress()
remoteAddress()
closeForcibly()
register(EventLoop, ChannelPromise)
deregister(ChannelPromise)
voidPromise()

And some basic state related data:

private volatile boolean registered;
private boolean closeInitiated;

In addition to basic property setting and reading, the final methods in our channel mainly include the following:

  1. The bind method used to build the server-side service:
public ChannelFuture bind(SocketAddress localAddress) {
        return pipeline.bind(localAddress);
    }
  1. The connect method used by the client to establish a connection with the server:
public ChannelFuture connect(SocketAddress remoteAddress) {
        return pipeline.connect(remoteAddress);
    }
  1. Disconnect method to disconnect:
public ChannelFuture disconnect() {
        return pipeline.disconnect();
    }
  1. The close method of closing the channel:
public ChannelFuture close() {
        return pipeline.close();
    }
  1. The deregister method to cancel the registration:
public ChannelFuture deregister() {
        return pipeline.deregister();
    }
  1. Flush method to refresh data:
    public Channel flush() {
        pipeline.flush();
        return this;
    }
  1. The read method for reading data:
    public Channel read() {
        pipeline.read();
        return this;
    }
  1. How to write data:
    public ChannelFuture write(Object msg) {
        return pipeline.write(msg);
    }

It can be seen that the read, write and binding work in these channels is performed by the pipeline related to the channel.

In fact, it is also well understood that a channel is just a channel, and operations related to data still need to be performed in the pipeline.

Let's take the bind method as an example to see how the pipline in AbstractChannel is implemented.

In AbstractChannel, the default pipeline is DefaultChannelPipeline, and its bind method is as follows:

        public void bind(
                ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
            unsafe.bind(localAddress, promise);
        }

The unsafe here is actually unsafe in AbstractChannel, and the bind method in unsafe will eventually call the dobind method in AbstractChannel:

protected abstract void doBind(SocketAddress localAddress) throws Exception;

So in the final analysis, if it is based on various implementations of AbstractChannel, then you only need to implement these do* methods of it.

Well, the introduction of AbstractChannel is over. Let's take another look at AbstractServerChannel. AbstractServerChannel inherits from AbstractChannel and implements the ServerChannel interface.

public abstract class AbstractServerChannel extends AbstractChannel implements ServerChannel 

We know that ServerChannel and Channel are actually the same, so AbstractServerChannel just made some adjustments on the implementation of AbstractChannel.

In AbstractServerChannel, let's take a look at the difference between AbstractServerChannel and AbstractChannel.

The first is the constructor of AbstractServerChannel:

protected AbstractServerChannel() {
        super(null);
    }

In the constructor, the parent channel of super is null, indicating that there is no parent channel in ServerChannel itself, which is ServerChannel and client channel
the first difference. Because the server channel can accept the client channel through the worker event loop, the server channel is the parent channel of the client channel.

In addition, we also observe the implementation of several methods:

public SocketAddress remoteAddress() {
        return null;
    }

For ServerChannel, there is no need to actively connect to the remote Server, so there is no remoteAddress.

In addition, because the disconnection is actively called by the client side, the doDisconnect of the server channel will throw an exception that does not support this operation:

    protected void doDisconnect() throws Exception {
        throw new UnsupportedOperationException();
    }

At the same time, ServerChannel is only used to establish an association relationship between accept and client channel, so the server channel itself does not support the write operation to the channel, so this doWrite method is also not supported:

    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        throw new UnsupportedOperationException();
    }

Finally, ServerChannel only supports bind operation, so the connect method in DefaultServerUnsafe will also throw UnsupportedOperationException.

LocalChannel and LocalServerChannel

LocalChannel and LocalServerChannel are the most basic implementations of AbstractChannel and AbstractServerChannel. As can be seen from the name, these two Channels are local channels. Let's take a look at the specific implementation of these two Channels.

First, let's take a look at LocalChannel. LocalChannel has several extensions to AbstractChannel.

The first extension point is that several states of the channel are added to the LocalChannel:

private enum State { OPEN, BOUND, CONNECTED, CLOSED }

Through different states, you can have more fine-grained control over the channel.

In addition, a very important property has been added to LocalChannel:

private volatile LocalChannel peer;

Because LocalChannel represents the client channel, this peer represents the server channel of the client channel peer. Next we look at the specific implementation.

First is the constructor of LocalChannel:

    protected LocalChannel(LocalServerChannel parent, LocalChannel peer) {
        super(parent);
        config().setAllocator(new PreferHeapByteBufAllocator(config.getAllocator()));
        this.peer = peer;
        localAddress = parent.localAddress();
        remoteAddress = peer.localAddress();
    }

A LocalChannel can accept a LocalServerChannel as its parent and a LocalChannel as its peer channel.

So how is this peer created?

Let's take a look at the logic of connect in LocalUnsafe.

            if (state != State.BOUND) {
                // Not bound yet and no localAddress specified - get one.
                if (localAddress == null) {
                    localAddress = new LocalAddress(LocalChannel.this);
                }
            }

            if (localAddress != null) {
                try {
                    doBind(localAddress);
                } catch (Throwable t) {
                    safeSetFailure(promise, t);
                    close(voidPromise());
                    return;
                }
            }

First, judge the state of the current channel. If it is an unbound state, a binding operation is required. First create the corresponding LocalAddress based on the incoming LocalChannel.

This LocalAddress is just a representation of LocalChannel and has no special function.

Let's take a look at this doBind method:

    protected void doBind(SocketAddress localAddress) throws Exception {
        this.localAddress =
                LocalChannelRegistry.register(this, this.localAddress,
                        localAddress);
        state = State.BOUND;
    }

A static map is maintained in the LocalChannelRegistry, which stores the registered Channel.

The registration here is to easily get the corresponding channel later.

After registering the localChannel, the next step is to obtain the corresponding LocalServerChannel according to the registered remoteAddress, and finally call the serve method of the LocalServerChannel to create a new peer channel:

Channel boundChannel = LocalChannelRegistry.get(remoteAddress);
            if (!(boundChannel instanceof LocalServerChannel)) {
                Exception cause = new ConnectException("connection refused: " + remoteAddress);
                safeSetFailure(promise, cause);
                close(voidPromise());
                return;
            }

            LocalServerChannel serverChannel = (LocalServerChannel) boundChannel;
            peer = serverChannel.serve(LocalChannel.this);

The serve method first creates a new LocalChannel:

    protected LocalChannel newLocalChannel(LocalChannel peer) {
        return new LocalChannel(this, peer);
    }

If we call the previous Localchannel channelA, the new LocalChannel created here is called channelB. Then the final result is that the peer of channelA is channelB, the parent of channelB is LocalServerChannel, and the peer of channelB is channelA.

This constitutes a relationship between peer-to-peer channels.

Next, let's take a look at how the read and write of localChannel work.

First look at the doWrite method of LocalChannel:

Object msg = in.current();
...
peer.inboundBuffer.add(ReferenceCountUtil.retain(msg));
in.remove();
...
finishPeerRead(peer);

First get the msg to be written from the ChannelOutboundBuffer, add it to the peer's inboundBuffer, and finally call the finishPeerRead method.

It can be seen from the method name that finishPeerRead is the read method that calls the peer.

In fact, this method will call the peer's readInbound method to read the message from the inboundBuffer just written:

    private void readInbound() {
        RecvByteBufAllocator.Handle handle = unsafe().recvBufAllocHandle();
        handle.reset(config());
        ChannelPipeline pipeline = pipeline();
        do {
            Object received = inboundBuffer.poll();
            if (received == null) {
                break;
            }
            pipeline.fireChannelRead(received);
        } while (handle.continueReading());

        pipeline.fireChannelReadComplete();
    }

So, for localChannel, its writes are actually written to the peer's inboundBuffer. Then call the peer's read method to read data from the inboundBuffer.

Compared with localChannel, localServerChannel has an additional serve method, which is used to create a peer channel and call readInbound to start reading data from inboundBuffer.

Summarize

This chapter details the difference between channel and serverChannel, and their simplest native implementations. I hope you have a basic understanding of how channel and serverChannel work.

This article has been included in http://www.flydean.com/04-2-netty-channel-vs-serverchannel-md/

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

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


flydean
890 声望433 粉丝

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