Netty series: implementation in channel,ServerChannel and netty

brief introduction

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

So what are the types of channel s in netty? How do they work? Let's have a look.

channel and ServerChannel

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

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

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

You can see that ServerChannel inherits from Channel, which means that the Channel on the server is also a kind of Channel.

But strangely enough, you can see that there are no new methods in ServerChannel. In other words, ServerChannel and Channel are essentially the same in definition. You can think of ServerChannel as a tag interface.

So what is the relationship between channel and ServerChannel?

We know that a parent method is defined in the 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.

Firstly, the value of parent is realized through the public parent class AbstractChannel of LocalChannel and LocalServerChannel:

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

For LocalChannel, you can set the parent channel 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 the LocalChannel, its connect method actually calls the connect method of pipeline:

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

Finally, localunsafe in LocalChannel will be called Connect method.

And in localunsafe Serverchannel will be called again in the connect method Serve method.

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

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

Here, the LocalChannel created using the newLocalChannel method is the child channel of serverChannel.

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

Implementation of channel in netty

In netty, channel and Serverchannel have many implementation classes to complete different business functions.

In order to understand the secret of channel in netty step by step, let's first discuss the basic implementation of channel in netty and the working principle of LocalChannel and LocalServerChannel.

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

As can be seen from the figure, LocalChannel inherits from AbstractChannel, while LocalServerChannel inherits from AbstractServerChannel.

Because ServerChannel inherits from Channel, AbstractServerChannel naturally inherits from AbstractChannel.

Next, we explore the underlying principle of channel implementation in netty by comparing and analyzing AbstractChannel and AbstractServerChannel, LocalChannel and LocalServerChannel.

AbstractChannel and AbstractServerChannel

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

Firstly, AbstractChannel defines some basic channel related attributes to be returned in the channel interface, including parent channel, channel id,pipline,localAddress,remoteAddress,eventLoop, etc., as follows:

    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;

Note that there is also a very Unsafe attribute in AbstractChannel.

Unsafe itself is an internal interface defined in the Channel interface. Its function is to provide specific implementations for different types of transport.

It can be seen from the name that Unsafe is an Unsafe implementation. It is only used in the source code of netty. It cannot appear in user code. Or you can regard Unsafe as the underlying implementation, and the AbstractChannel or other channels wrapped around it are the encapsulation of the underlying implementation. For ordinary users, they only need to use the Channel without going deep into the underlying content.

In addition, for Unsafe, the remaining methods must be invoked from I/O thread except for the following methods:

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

Data related to some basic states:

private volatile boolean registered;
private boolean closeInitiated;

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

  1. bind method for establishing server-side service:
public ChannelFuture bind(SocketAddress localAddress) {
        return pipeline.bind(localAddress);
    }
  1. connect method for establishing connection between client and server:
public ChannelFuture connect(SocketAddress remoteAddress) {
        return pipeline.connect(remoteAddress);
    }
  1. disconnect method for disconnecting:
public ChannelFuture disconnect() {
        return pipeline.disconnect();
    }
  1. close method of closing channel:
public ChannelFuture close() {
        return pipeline.close();
    }
  1. deregister method to unregister:
public ChannelFuture deregister() {
        return pipeline.deregister();
    }
  1. flush method for refreshing data:
    public Channel flush() {
        pipeline.flush();
        return this;
    }
  1. 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);
    }

You can see that the reading, writing and binding work in these channels are performed by the pipeline related to the channel.

In fact, it is well understood that channel is just a channel, and data related operations 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);
        }

Unsafe here is actually unsafe in AbstractChannel. 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, you only need to implement these do * methods.

OK, 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 only makes some adjustments to the implementation of AbstractChannel.

In AbstractServerChannel, let's observe 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, which means 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 a remote Server, so there is no remoteAddress.

In addition, because the disconnection is actively called by the client, 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 the association 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 throw UnsupportedOperationException

LocalChannel and LocalServerChannel

LocalChannel and LocalServerChannel are the most basic implementations of AbstractChannel and AbstractServerChannel. From the name, we can see that 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, which has several extensions to AbstractChannel.

The first extension point is the addition of several channel statuses in LocalChannel:

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

Through different states, the channel can be controlled more finely.

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

private volatile LocalChannel peer;

Because LocalChannel represents the client channel, this peer represents the server channel equivalent to the client channel. Next, let's look at the specific implementation.

The 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();
    }

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

So how was 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 status of the current channel. If it is unbound, bind it. First, create the corresponding LocalAddress according to the incoming LocalChannel.

This LocalAddress is only a representation of LocalChannel and has no special functions.

Let's take a look at the 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 local Channel registry, which stores the registered channels

Registration here is to get the corresponding channel conveniently later.

After registering localChannel, the next step is to get the corresponding LocalServerChannel according to the registered remoteAddress, and finally call LocalServerChannel's serve method 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 will first create 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 channelB. 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 channel s.

Next, let's see how read and write of localChannel work.

First, take a 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 ChannelOutboundBuffer, add it to the inboundBuffer of peer, and finally call the finishPeerRead method.

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

In fact, this method will call 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();
    }

Therefore, for localChannel, its write is actually written to the inboundBuffer of peer. Then call peer's read method to read data from inboundBuffer.

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

summary

This chapter introduces the differences between channel and serverChannel in detail, and their simplest local implementation. I hope you have a basic understanding of the working principle of channel and serverChannel.

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 tutorial, and many tips you don't know are waiting for you to find!

Added by amity on Tue, 22 Feb 2022 05:44:15 +0200