Netty 源码 Channel(二)主要类

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

一、Channel 类图

二、AbstractChannel

2.1 几个重要属性

// SocketChannel 的 parent 是 ServerSocketChannel
private final Channel parent;
// 唯一标识
private final ChannelId id;
// Netty 内部使用
private final Unsafe unsafe;
// pipeline
private final DefaultChannelPipeline pipeline;
// 绑定的线程
private volatile EventLoop eventLoop; protected AbstractChannel(Channel parent, ChannelId id) {
this.parent = parent;
this.id = id;
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}

2.2 核心 API

read、write、connect、bind 都委托给了 pipeline 处理。

三、AbstractNioChannel

3.1 几个重要属性

// NIO 底层 Channel
private final SelectableChannel ch;
// 感兴趣的事件
protected final int readInterestOp;
// 绑定的 SelectionKey,当 selectionKey 修改后其它线程可以感知
volatile SelectionKey selectionKey;

3.2 核心 API

(1) doRegister

将 channel 注册到 eventLoop 线程上,此时统一注册的感兴趣的事件类型为 0。

@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
// 1. 将 channel 注册到 eventLoop 线程上
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
// 2. 对注册失败的 channel,JDK 将在下次 select 将其删除
// 然而此时还没有调用 select,当然也可以调用 selectNow 强删
eventLoop().selectNow();
selected = true;
} else {
// 3. JDK API 描述不会有异常,实际上...
throw e;
}
}
}
}

(2) doBeginRead

doBeginRead 只做了一件事就是注册 channel 感兴趣的事件。此至就可以监听网络事件了。

@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
} readPending = true;
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}

四、AbstractNioByteChannel

AbstractNioByteChannel 中最重要的方法是 doWrite,我们一起来看一下:

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
// 1. spin 是自旋的意思,也就是最多循环的次数
int writeSpinCount = config().getWriteSpinCount();
do {
// 2. 从 ChannelOutboundBuffer 弹出一条消息
Object msg = in.current();
if (msg == null) {
// 3. 写完了就要清除半包标记
clearOpWrite();
// 4. 直接返回,不调用 incompleteWrite 方法
return;
}
// 5. 正确处理了一条 msg 消息,循环次数就减 1
writeSpinCount -= doWriteInternal(in, msg);
} while (writeSpinCount > 0);
// 6. writeSpinCount < 0 认为有半包需要继续处理
incompleteWrite(writeSpinCount < 0);
}

为什么要设置最大自旋次数,一次把 ChannelOutboundBuffer 中的所有 msg 处理完了不是更好吗?如果不设置的话,线程会一直尝试进行网络 IO 写操作,此时线程无法处理其它网络 IO 事件,可能导致线程假死。

下面我们看一下 msg 消息是如何处理的,这里以 ByteBuf 消息为例:

private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
// 1. 不可读则丢弃这条消息,继续处理下一条消息
if (!buf.isReadable()) {
in.remove();
return 0;
} // 2. 由具体的子类重写 doWriteBytes 方法,返回处理了多少字节
final int localFlushedAmount = doWriteBytes(buf);
if (localFlushedAmount > 0) {
// 3. 更新进度
in.progress(localFlushedAmount);
if (!buf.isReadable()) {
in.remove();
}
return 1;
}
// 文件处理,这里略过,类似 ByteBuf
} else if (msg instanceof FileRegion) {
// 省略 ...
} else {
throw new Error();
}
return WRITE_STATUS_SNDBUF_FULL; // WRITE_STATUS_SNDBUF_FULL=Integer.MAX_VALUE
}

doWriteBytes 进行消息发送,它是一个抽象方法,由具体的子类实现。如果本次发送的字节数为 0,说明发送的 TCP 缓冲区已满,发生了 ZERO_WINDOW。此时再次发送可能仍是 0,空循环会占用 CPU 资源。因此返回 Integer.MAX_VALUE。直接退出循环,设置半包标识,下次继续处理。

// 没有写完,有两种情况:
// 一是 TCP 缓冲区已满,doWriteBytes 定入 0 个字节,导致 doWriteInternal 返回 Integer.MAX_VALUE,
// 这时设置了半包标识,会自动轮询写事件
// 二是自旋的次数已到,将线程交给其它任务执行,未写完的数据通过 flushTask 继续写
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);
}
}

最后我们来看一下半包是如何处理的,可以看到所谓的半包标记其实就是是否取 OP_WRITE 事件。

protected final void clearOpWrite() {
final SelectionKey key = selectionKey();
final int interestOps = key.interestOps();
if ((interestOps & SelectionKey.OP_WRITE) != 0) {
key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
}
} 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);
}
}

五、AbstractNioMessageChannel

AbstractNioMessageChannel#doWrite 方法和 AbstractNioByteChannel#doWrite 类似,前者可以写 POJO 对象,后者只能写 ByteBuf 和 FileRegion。

六、NioServerSocketChannel

NioServerSocketChannel 通过 doReadMessages 接收客户端的连接请求:

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = SocketUtils.accept(javaChannel());
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
return 0;
}

七、NioSocketChannel


每天用心记录一点点。内容也许不重要,但习惯很重要!

Netty 源码 Channel(二)主要类的更多相关文章

  1. Netty 源码 Channel(二)核心类

    Netty 源码 Channel(二)核心类 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一.Channel 类图 二. ...

  2. Netty 源码(二)NioEventLoop 之 Channel 注册

    Netty 源码(二)NioEventLoop 之 Channel 注册 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一 ...

  3. Netty 源码 Channel(一)概述

    Netty 源码 Channel(一)概述 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) Channel 为 Netty ...

  4. Netty源码解析 -- 内存对齐类SizeClasses

    在学习Netty内存池之前,我们先了解一下Netty的内存对齐类SizeClasses,它为Netty内存池中的内存块提供大小对齐,索引计算等服务方法. 源码分析基于Netty 4.1.52 Nett ...

  5. netty源码理解(二) serverstrap.bind()

    eventloop是一个线程,里面有一个executor封装了一个线程工厂,在启动的时候启动一个线程,传入的实现了runnable的内部类,里面调用了eventloop的run方法.

  6. Netty 源码解析(二):Netty 的 Channel

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty源码解析(一):开始 当前:Netty 源码解析(二): Netty 的 Channel ...

  7. Netty 源码解析(八): 回到 Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第八篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  8. Netty 源码解析(六): Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第六篇. 接下来的时间灯塔君持续更新Netty系列一共九篇   Netty 源码解析(一 ):开始 Netty ...

  9. 【Netty源码分析】ChannelPipeline(二)

    在上一篇博客[Netty源码学习]ChannelPipeline(一)中我们只是大体介绍了ChannelPipeline相关的知识,其实介绍的并不详细,接下来我们详细介绍一下ChannelPipeli ...

随机推荐

  1. 大数据入门到精通2--spark rdd 获得数据的三种方法

    通过hdfs或者spark用户登录操作系统,执行spark-shell spark-shell 也可以带参数,这样就覆盖了默认得参数 spark-shell --master yarn --num-e ...

  2. 【MongoDB】关于无法连接mongo的问题

    今天使用MongoDB的时候发现直接输入mongo提示连接失败 首先想到的可能是服务还没启动 当我随便打开一个cmd输入net start MongoDB 提示:net start mongodb 发 ...

  3. Redis备份及回收策略

    Redis备份(持久化) Redis备份存在两种方式: 1.一种是"RDB".是快照(snapshotting),它是备份当前瞬间Redis在内存中的数据记录; 2.另一种是&qu ...

  4. 一个先进的App框架:使用Ionic创建一个简单的APP

    原文  http://www.w3cplus.com/mobile/building-simple-app-using-ionic-advanced-html5-mobile-app-framewor ...

  5. ACM-ICPC 2018 沈阳赛区网络预赛 J. Ka Chang(树状数组+分块)

    Given a rooted tree ( the root is node 1 ) of N nodes. Initially, each node has zero point. Then, yo ...

  6. f5单台安装配置

    1.对设备的管理口进行配置 管理口的默认设置 •IP 地址: 192.168.1.245/24 2.用户名和密码 1)初始密码: 图形界面用户名/密码:admin/admin:命令行用户名/密码:ro ...

  7. HA状态下防火墙损坏处理

    问题描述: web登录防火墙管理地址,发现在 状态-系统信息 里集群成员只有一台原备机.到机房发现原主机只有power灯是亮着的,HA灯和status灯都不亮. 用笔记本直连防火墙的mgmt口不亮,c ...

  8. swift 关于didSet 和willSet赋值的注意点

    1. 初始化赋值的时候都不会走这个的方法, 需要在创建结构体后或对象后,在赋值,此时才会走这个方法

  9. tomcat-maven-plugin的使用

    maven有一个把web应用部署到tomcat下的插件 tomcat-maven-plugin , 我们可以使用这个插件把web应用一键式的部署到一个远程的tomcat中. 插件的url: http: ...

  10. Linux下 MYSQL 主从复制、同步

    mysql从3.23.15版本以后提供数据库复制功能.利用该功能可以实现两个数据库同步,主从模式(A->B),互相备份模式(A<=>B)的功能. 主从模式(A->B)的配置过程 ...