Netty学习笔记七-Channel分析

Channel介绍

Channel是JDK 的NIO类库中的重要组成部分,我们在之前的代码中也经常用到io.netty.channel.socket.nio.NioSocketChannel和io.netty.channel.socket.nio.NioServerSocketChannel用于阻塞性IO操作
NioServerSocketChannel类的继承图


image.png

从上可以看出,Channel是最顶层的抽象,一个Channel抽象为网络socket的连接或者读写,连接,绑定IO的一个组件。
一个Channel需要提供给用户:
1、channel当前的状态(打开还是已连接?)
2、channel的配置参数(channelConfig)
3、channel支持哪些IO操作
4、处理channel IO操作的事件
在Netty中所有的IO操作都是异步的,也就意味着IO的请求会立刻返回,并不能保证请求是否已完成。在Netty中IO请求会返回ChannelFuture实例。
Channel是有层级的,例如,对于服务端而言,父channel为空,对于客户端NioSocketChannel,它的父Channel就是创建它的ServerSocketChannel.

Channel主要API方法

下面对我们对Channel的主要API做简单介绍
Channel read() 从channel中读取数据到第一个buffer,如果数据被成功读取触发channelRead事件,如果数据被读取完成会触发channelReadComplete事件,如果有读操作被挂起,那么后续读操作会取消。
ChannelFuture write(Object msg) 将当前msg通过ChannelPipeLine写入到Channel,这个方法不会真正执行flush操作,所以当写入完成后要执行flush()方法才能将数据真正发送出去。
write(Object msg, ChannelPromise promise) 跟write功能一样,只是写入完成后会回调promise。
Channel flush() 将channel中所有缓存消息全部写入到目标channel,发送给通信对方。
ChannelFuture writeAndFlush(Object msg) 作用等于write+flush
ChannelFuture disconnect() 请求与远程通信对端断开连接,这个操作会级联触发ChannelHandler.disconnect(ChannelHandlerContext, ChannelPromise)事件
ChannelFuture connect(SocketAddress remoteAddress) 使用指定的remoteAddress发起连接请求,并且返回ChannelFuture对象,如果连接超时会ChannelFuture返回操作结果是ConnectTimeoutException,如果连接被拒绝操作结果是ConnectException,该操作会触发ChannelHandler#connect(ChannelHandlerContext, SocketAddress, SocketAddress, ChannelPromise)事件。
EventLoop eventLoop() 返回channel注册的eventLoop
ChannelConfig config() 返回channel的配置
ChannelMetadata metadata() 返回当前channel的元数据信息,比如TCP参数配置。
SocketAddress localAddress() 返回channel绑定的本地地址
SocketAddress remoteAddress() 返回channel通信的远程地址
Channel parent() 返回父级channel

AbstractChannel分析

AbstractChannel是Channel的基本实现类,它采用聚合的方式封装了各种功能,从成员变量聚合了以下内容:
private final Channel parent :父级channel
private final ChannelId id = DefaultChannelId.newInstance() 全局唯一id
private final Unsafe unsafe : unsafe示例
private final DefaultChannelPipeline pipeline; 当前channle对应的DefaultChannelPipeline
private final EventLoop eventLoop; 当前channel注册的EventLoop
前面提到channel的IO操作会触发会产生对应的IO事件,然后事件在ChannelPipeLine中传播,并由对应的ChannleHandler处理。
AbstractChannel的IO操作直接调用DefaultChannelPipeline的相关方法由DefaultChannelPipeline处理相关逻辑

@Override
    public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
        return pipeline.connect(remoteAddress, localAddress);
    }
 @Override
    public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
        return pipeline.bind(localAddress, promise);
    }

    @Override
    public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return pipeline.connect(remoteAddress, promise);
    }
……

AbstractNioChannel分析

AbstractChannel是Channel的基本实现类,那么AbstractNioChannel就是Channel基于选择器的实现类,它实现了核心的将Channel注册到Selector的功能。
首先看下成员变量

private static final InternalLogger logger =
            InternalLoggerFactory.getInstance(AbstractNioChannel.class);

    private final SelectableChannel ch;//JDK NIO SelectableChannel
    protected final int readInterestOp;//JDK selectionKey 的OP_READ
    private volatile SelectionKey selectionKey;
    private volatile boolean inputShutdown;

    /**
     * The future of the current connection attempt.  If not null, subsequent
     * connection attempts will fail.
     */
    private ChannelPromise connectPromise;
    private ScheduledFuture<?> connectTimeoutFuture;
    private SocketAddress requestedRemoteAddress;

核心注册功能源码:

protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(eventLoop().selector, 0, this);
                return;
            } catch (CancelledKeyException e) {
                if (!selected) {
                    // Force the Selector to select now as the "canceled" SelectionKey may still be
                    // cached and not removed because no Select.select(..) operation was called yet.
                    eventLoop().selectNow();
                    selected = true;
                } else {
                    // We forced a select operation on the selector before but the SelectionKey is still cached
                    // for whatever reason. JDK bug ?
                    throw e;
                }
            }
        }
    }

定义布尔类型变量selected标识是否注册成功,调用SelectableChannel的register将当前channel注册到EventLoop的多路复用器Selector上。
其中SelectableChannel的注册方法定义如下:

public abstract SelectionKey register(Selector sel, int ops, Object att)
        throws ClosedChannelException;

第二个参数是 指定监听的网络操作位,表示channel对哪几类网络事件感兴趣,具体定义如下:

    public static final int OP_READ = 1 << 0; //读事件
    public static final int OP_WRITE = 1 << 2;//写事件
    public static final int OP_CONNECT = 1 << 3;//客户端连接服务端事件
    public static final int OP_ACCEPT = 1 << 4;//服务端接收客户端连接事件

其中注册时传的ops等于0,表示不对任何事件感兴趣,只是完成注册操作,注册成功之后返回selectionKey,通过selectionKey可以获取注册的channel.
如果注册返回的selectionKey被取消,则抛出CancelledKeyException异常,如果是第一次抛异常则调用selectNow将取消的selectionKey删除并将selected置为true并再次注册,如果仍未成功则抛出CancelledKeyException异常。
处理读操作之前设置网络操作位为读

protected void doBeginRead() throws Exception {
        if (inputShutdown) {
            return;
        }

        final SelectionKey selectionKey = this.selectionKey;
        if (!selectionKey.isValid()) {
            return;
        }

        final int interestOps = selectionKey.interestOps();
        if ((interestOps & readInterestOp) == 0) {
            selectionKey.interestOps(interestOps | readInterestOp);
        }
    }

先判断Channel是否关闭,如果处于关闭则直接返回,然后获取当前的SelectKey的操作位与读操作位位于,如果结果为0标识没有设置过读操作位,最后通过或设置读操作位

AbstractNioByteChannel分析

AbstractNioByteChannel是Channel操作字节的实现,核心代码是protected void doWrite(ChannelOutboundBuffer in)

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        int writeSpinCount = -1;

        for (;;) {
            Object msg = in.current(true);
            if (msg == null) {
                // Wrote all messages.
                clearOpWrite();
                break;
            }

首先从ChannelOutboundBuffer环形数组中读取数据,如果数据为空,调用clearOpWrite清除读标志位

if (msg instanceof ByteBuf) {
                ByteBuf buf = (ByteBuf) msg;
                int readableBytes = buf.readableBytes();
                if (readableBytes == 0) {
                    in.remove();
                    continue;
                }

如果数据不为空,判断是否是ByteBuf类型,若是强制转换为ByteBuf,并判断ByteBuf中是否有可读字节,如果没有则将该消息从数组中删除,并继续处理其他消息。

boolean setOpWrite = false;
                boolean done = false;
                long flushedAmount = 0;
                if (writeSpinCount == -1) {
                    writeSpinCount = config().getWriteSpinCount();
                }
                for (int i = writeSpinCount - 1; i >= 0; i --) {
                    int localFlushedAmount = doWriteBytes(buf);
                    if (localFlushedAmount == 0) {
                        setOpWrite = true;
                        break;
                    }

                    flushedAmount += localFlushedAmount;
                    if (!buf.isReadable()) {
                        done = true;
                        break;
                    }
                }

设置半包标志setOpWrite,消息是否全部发送标识done,发送总消息数flushedAmount,对循环发送次数writeSpinCount判断,如果为-1从channel配置重获取循环发送次数,调用doWriteBytes进行循环发送,如果本次发送的字节数为0那么说明TCP缓冲区已满,所以讲写半包标识置为true并跳出循环。

如果发送字节数大于0那么对发送字节数进行累加,如果当前buf没有可读字节数了则标识buf写入完成,设置消息发送标识为true并跳出循环。

in.progress(flushedAmount);

                if (done) {
                    in.remove();
                } else {
                    incompleteWrite(setOpWrite);
                    break;
                }

调用progress更新进度信息,如果消息已完全发送则将该buf从环形数组中移除,否则调用incompleteWrite方法

protected final void incompleteWrite(boolean setOpWrite) {
        // Did not write completely.
        if (setOpWrite) {
            setOpWrite();
        } else {
            // Schedule flush again later so other tasks can be picked up in the meantime
            Runnable flushTask = this.flushTask;
            if (flushTask == null) {
                flushTask = this.flushTask = new Runnable() {
                    @Override
                    public void run() {
                        flush();
                    }
                };
            }
            eventLoop().execute(flushTask);
        }
    }

如果写半包标识setOpWrite为true,调用setOpWrite()重新设置写操作位setOpWrite()

 protected final void setOpWrite() {
        final SelectionKey key = selectionKey();
        final int interestOps = key.interestOps();
        if ((interestOps & SelectionKey.OP_WRITE) == 0) {
            key.interestOps(interestOps | SelectionKey.OP_WRITE);
        }
    }

SelectKey设置为OP_WRITE后,Selector会不断轮询对应的Channel处理没有发送完成的半包消息,直到清除OP_WRITE标志为止。
如果没有设置半包标识,则需要新起一个线程来处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容