Netty组件Channel

Netty组件Channel

  • 1 简介
  • 2 Channel 生命周期
  • 3 Channel类型
  • 4 Channel详解
  • 5 异步IO和ChannelFuture
  • 6 channel的主要作用
  • 7 Channel的层级结构
  • 8 释放资源
  • 9 事件处理
  • 10 Channel 源码分析
  • 11 总结

1 简介

什么是Channel?
channel是一个管道,用于连接字节缓冲区Buf和另一端的实体,这个实例可以是Socket,也可以是File, 在Nio网络编程模型中, 服务端和客户端进行IO数据交互(得到彼此推送的信息)的媒介就是Channel。
Channel是连接ByteBuf和Event的桥梁,netty中的Channel提供了统一的API,通过这种统一的API,netty可以轻松的对接多种传输类型,如OIO,NIO等。今天本文将会介绍Channel的使用和Channel相关的一些概念。

Channel通过ChannelPipeline中的多个Handler处理器,Channel使用它处理IO数据。

Netty对Jdk原生的ServerSocketChannel进行了封装和增强封装成了NioXXXChannel, 相对于原生的JdkChannel, Netty的Channel增加了如下的组件:

id 标识唯一身份信息
可能存在的parent Channel
管道 pepiline
用于数据读写的unsafe内部类
关联上相伴终生的NioEventLoop

Netty组件Channel_第1张图片
Channel,表示一个连接,可以理解每一个请求,就是一个Channel
ChannelHandler,核心处理业务就在这里,用于处理业务请求
ChannelHandlerContext,用于传输业务数据
ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext

ByteBuf
ByteBuf是一个操作字节的容器,最大的特点就是使用方便
他有三种使用模式: Heap Buffer 堆缓冲区, Direct Buffer 直接缓冲区, Composite Buffer 复合缓冲区

Codec
Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,从而达到自定义协议的目的。
在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。

在Netty框架中,Channel是其中之一的核心概念,是Netty网络通信的主体,由它负责同对端进行网络通信、注册和数据操作等功能。本文我们来详细地分析 Netty 中的 Channel以及跟Channel相关的其他概念,包括ChannelPipeline、ChannelHandlerContext、ChannelHandler等
Netty组件Channel_第2张图片

2 Channel 生命周期

Channel 有个简单但强大的状态模型,与 ChannelInboundHandler API 密切相关。下面表格是 Channel 的四个状态
状态 描述
channelUnregistered channel已创建但未注册到一个 EventLoop.
channelRegistered channel 注册到一个 EventLoop.
channelActive channel 变为活跃状态(连接到了远程主机),现在可以接收和发送数据了
channelInactive channel 处于非活跃状态,没有连接到远程主机

Channel 的正常的生命周期如下图,当状态出现变化,就会触发对应的事件,这样就能与 ChannelPipeline 中的 ChannelHandler进行及时的交互。

Netty组件Channel_第3张图片

3 Channel类型

(1)FileChannel
(2)DatagramChannel
(3)SocketChannel
(4)ServerSocketChannel

FileChannel用于文件的数据读写。
DatagramChannel用于UDP的数据读写。
SocketChannel用于TCP的数据读写。
ServerSocketChannel允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel。

1). FileChannel
FileChannel 是操作文件的Channel,我们可以通过 FileChannel 从一个文件中读取数据,也可以将数据写入到文件中。

注意,FileChannel 不能设置为非阻塞模式。

//操作一:打开 FileChannel通道
RandomAccessFile aFile     = new RandomAccessFile("test.txt","rw");
FileChannel      inChannel = aFile.getChannel();

//操作二:读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

//操作三:写入数据
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining())
{
    channel.write(buf);
}

//操作四:关闭
channel.close();

//当我们对 FileChannel 的操作完成后,必须将其关闭。

//操作五:强制刷新磁盘
channel.force(true);

FileChannel的force()方法将所有未写入的数据从通道刷新到磁盘中。在你调用该force()方法之前,出于性能原因,操作系统可能会将数据缓存在内存中,因此您不能保证写入通道的数据实际上写入磁盘。

2). SocketChannel
有两种Socket通道,一个是客户端的SocketChannel,一个是负责服务器端的Socket通道ServerSocketChannel。SocketChannel与OIO中的Socket类对应,ServerSocketChannel对应于OIO中的ServerSocket类相NIO。

两种Socket通道新增的通道都支持阻塞和非阻塞两种模式。在阻塞模式下的通道的创建、关闭、读写操作如下:

//操作一:创建
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));

//这个是客户端的创建。当一个服务器端的ServerSocketChannel 接受到连接请求时,也会返回一个 SocketChannel 对象。

//操作二:读取
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
//如果 read()返回 -1,那么表示连接中断了.

//操作三:写入数据
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    channel.write(buf);
}

//操作四:关闭
socketChannel.close();
//在非阻塞模式,我们可以设置 SocketChannel 为异步模式,这样我们的 connect,read,write 都是异步的了.
//操作一:连接
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
while(! socketChannel.finishConnect() ){
    //wait,or do something else...
}
//在异步模式中,或许连接还没有建立,socketChannel.connect 方法就返回了,因此我们不断的自旋,检查当前是否是连接到了主机。

//操作二:非阻塞读写
//在异步模式下,读写的方式是一样的.
//在读取时,因为是异步的,因此我们必须检查 read 的返回值,来判断当前是否读取到了数据.
ServerSocketChannel
ServerSocketChannel 顾名思义,是用在服务器为端的,可以监听客户端的 TCP 连接,例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();
    //do something with socketChannel...
}

//操作四:关闭
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.close();

2.1). 监听连接
我们可以使用ServerSocketChannel.accept()方法来监听客户端的 TCP 连接请求,accept()方法会阻塞,直到有连接到来,当有连接时,这个方法会返回一个 SocketChannel 对象:

while(true){

SocketChannel socketChannel =

serverSocketChannel.accept();

//do something with socketChannel...

}

2.2). 非阻塞模式
在非阻塞模式下,accept()是非阻塞的,因此如果此时没有连接到来,那么 accept()方法会返回null:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));

serverSocketChannel.configureBlocking(false);

while(true){

    SocketChannel socketChannel =

            serverSocketChannel.accept();

    if(socketChannel != null){

        //do something with socketChannel...

        }

}

3). DatagramChannel
DatagramChannel 是用来处理 UDP 连接的.

操作一:打开

DatagramChannel channel = DatagramChannel.open();

channel.socket().bind(new InetSocketAddress(9999));

操作二:读取数据

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

channel.receive(buf);
操作三:发送数据

String newData = "New String to write to file..."

                    + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());

buf.flip();

int bytesSent = channel.send(buf,new InetSocketAddress("example.com",80));  因为 UDP 是非连接的,因此这个的 connect 并不是向 TCP 一样真正意义上的连接,因此我们仅仅可以从指定的地址中读取或写入数据.  channel.connect(new InetSocketAddress("example.com",80));

4 Channel详解

Channel是什么? Channel是一个连接网络输入和IO处理的桥梁。你可以通过Channel来判断当前的状态,是open还是connected,还可以判断当前Channel支持的IO操作,还可以使用ChannelPipeline对Channel中的消息进行处理。

public interface Channel extends AttributeMap, ChannelOutboundInvoker, Comparable {

可以看到Channel是一个接口,它继承了AttributeMap, ChannelOutboundInvoker, Comparable三个类。Comparable表示这个类可以用来做比较。AttributeMap用来存储Channel的各种属性。ChannelOutboundInvoker主要负责Channel和外部 SocketAddress 进行连接和对写。

Netty组件Channel_第4张图片

5 异步IO和ChannelFuture

netty中所有的IO都是异步IO,也就是说所有的IO都是立即返回的,返回的时候,IO可能还没有结束,所以需要返回一个ChannelFuture,当IO有结果之后,会去通知ChannelFuture,这样就可以取出结果了。

ChannelFuture是java.util.concurrent.Future的子类,它除了可以拿到线程的执行结果之外,还对其进行了扩展,加入了当前任务状态判断、等待任务执行和添加listener的功能。

其他的功能都很好理解,它的突破在于可以对ChannelFuture添加listener,我们列出一个添加listener的方法:

Future addListeners(GenericFutureListener>... listeners);

添加的Listener会在future执行结束之后,被通知。不需要自己再去调用get等待future结束。这里实际上就是异步IO概念的实现,不需要主动去调用,当你完成之后来通知我就行。非常的美好!

ChannelFuture 有两个状态:uncompleted或者completed,分别代表任务的执行状态。

当一个IO刚开始的时候,返回一个ChannelFuture对象,这个对象的初始状态是uncompleted。注意,这个状态下的IO是还未开始工作的状态。当IO完成之后,不管是succeeded, failed 或者 cancelled状态,ChannelFuture的状态都会转换成为completed。

如果要监控IO的状态,可以使用上面我们提到的 addListener 方法,为ChannelFuture添加一个ChannelFutureListener。

如果要等待IO执行完毕,还有一个await()方法,但是这个方法会去等待IO执行完毕,是一个同步的方法,所以并不推荐。

相比而言,addListener(GenericFutureListener)是一个非阻塞的异步方法,将会把一个ChannelFutureListener添加到ChannelFuture中,当IO结束之后会自动通知ChannelFutureListener,非常好用。

对于处理IO操作的ChannelHandler来说,为了避免IO的阻塞,一定不要在ChannelHandler的IO方法中调用await(),这样有可能会导致ChannelHandler因为IO阻塞导致性能下降。

   // 错误操作
    @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) {
       ChannelFuture future = ctx.channel().close();
       future.awaitUninterruptibly();
       // 调用其他逻辑
   }
  
   // 正确操作
    @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) {
       ChannelFuture future = ctx.channel().close();
       future.addListener(new ChannelFutureListener() {
           public void operationComplete(ChannelFuture future) {
               // 调用其他逻辑
           }
       });
   }

另外要注意的是ChannelFuture中的这些await方法比如:await(long), await(long, TimeUnit), awaitUninterruptibly(long), 或者 awaitUninterruptibly(long, TimeUnit)可以带一个过期时间,大家要注意的是这个过期时间是等待IO执行的时间,并不是IO的timeout时间,也就是说当await超时之后,IO还有可能没有执行完成,这就导致了下面的代码有可能报错:

Bootstrap b = ...;
   ChannelFuture f = b.connect(...);
   f.awaitUninterruptibly(10, TimeUnit.SECONDS);
   if (f.isCancelled()) {
       // 用户取消了Channel
   } else if (!f.isSuccess()) {
       // 这里可能会报异常,因为底层的IO可能还没有执行完成
       f.cause().printStackTrace();
   } else {
       // 成功建立连接
   }
Bootstrap b = ...;
   // 配置连接timeout的时间
   b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
   ChannelFuture f = b.connect(...);
   f.awaitUninterruptibly();
  
   // 等待直到底层IO执行完毕
   assert f.isDone();
  
   if (f.isCancelled()) {
       // 用户手动取消Channel
   } else if (!f.isSuccess()) {
       f.cause().printStackTrace();
   } else {
       // 成功建立连接
   }
   

6 channel的主要作用

close():关闭channel
closeFuture():处理channel的关闭
sync 同步等待channel关闭
addListener 异步等待channel关闭
pipeline():添加处理器
write():将数据写入
writeAndFlush():将数据写入并刷出

channel异步连接处理–channelFuture
采用sync阻塞连接操作
//方法1:使用sync方法同步处理结果
channelFuture.sync(); // 阻塞方法 连接建立后 才向下运行
Channel channel = channelFuture.channel();
channel.writeAndFlush(“123”);

采用addListener异步执行后续操作
// 方法2: 使用addListener(callback)方法异步处理结果
channelFuture.addListener(new ChannelFutureListener() {
// 连接建立完成后,调用该方法
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Channel channel = future.channel();
log.debug(“123”);
channel.writeAndFlush(“123123123”);
}
});

channel异步关闭处理–CloseFuture
与连接操作类似的:

采用sync阻塞
// 1. 直接阻塞 同步操作
// ChannelFuture channelFuture1 = channel.closeFuture();
// channelFuture1.sync();
// log.debug(“close operation”);

调用closeFuture异步执行后续操作
// 2. 使用addListener
ChannelFuture channelFuture1 = channel.closeFuture();
channelFuture1.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug(“close operation”);
}
});

优雅的关闭程序
当然是采用优雅的关闭:拒绝新的任务,发送完毕所有的数据之后再关闭线程。

    ChannelFuture channelFuture1 = channel.closeFuture();
    channelFuture1.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            log.debug("close operation");
            group.shutdownGracefully();
        }
    });

7 Channel的层级结构

netty中的Channel是有层级结构的,通过parent属性可获取这种层级结构。parent获取的对象和Channel的创建方式有关。比如如果是一个被ServerSocketChannel accepted的SocketChannel,那么它的parent就是ServerSocketChannel。

8 释放资源

和所有的IO一样,Channel在用完之后也需要被释放,需要调用close()或者close(ChannelPromise) 方法。

9 事件处理

channel负责建立连接,建立好的连接就可以用来处理事件ChannelEvent了,实际上ChannelEvent是由定义的一个个Channelhandler来处理的。而ChannelPipeline就是连接channel和channelhandler的桥梁。

10 Channel 源码分析

Netty组件Channel_第5张图片

Channel重要的内部接口 unsafe
Netty中,真正帮助Channel完成IO读写操作的是它的内部类unsafe, 源码如下, 很多重要的功能在这个接口中定义, 下面列举的常用的方法:

interface Unsafe {
//  把channel注册进EventLoop
void register(EventLoop eventLoop, ChannelPromise promise);
 
 // todo 给channel绑定一个 adress,
void bind(SocketAddress localAddress, ChannelPromise promise);

// 把channel注册进Selector
void deregister(ChannelPromise promise);

// 从channel中读取IO数据
void beginRead();

// 往channe写入数据
void write(Object msg, ChannelPromise promise);

接着往下看,下面来到Channel接口的直接实现类,AbstractChannel 他是个抽象类, AbstractChannel重写部分Channel接口预定义的方法, 它的抽象内部类AbstractUnsafe实现了Channel的内部接口unsafe。当我们创建对象使用的时候其实是使用的特化的对象,创建特化的对象就难免会调层层往上调用父类的构造方法, 所以我们看看AbstractChannel的构造方法干了什么活? 源码如下:

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    // todo channelId 代表Chanel唯一的身份标志
    id = newId();
    // todo 创建一个unsafe对象
    unsafe = newUnsafe();
    // todo 在这里初始化了每一个channel都会有的pipeline组件
    pipeline = newChannelPipeline();
}

AbstractChannel构造函数, 接受的子类传递进来的参数只有一个parent CHannel,而且,还不有可能为空, 所以在AbstractChannel是没有维护jdk底层的Channel的, 相反他会维护着Channel关联的EventLoop,我是怎么知道的呢? 首先,它的属性中存在这个字段,而且,将channel注册进selector的Register()方法是AbastractChannel重写的,Selector在哪呢? 在EventLoop里面,它怎么得到的呢? 它的子类传递了给了它

终于看出来点眉目,构造方法做了四件事

设置parent
如果当前创建的channel是客户端的channel,把parent初始化为他对应的parent
如果为服务端的channel,这就是null
创建唯一的id
创建针对channel进行io读写的unsafe
创建channel的处理器handler链 channelPipeline
AbstractChannel中维护着EventLoop

AbstractChanel的重要抽象内部类AbstractUnsafe 继承了Channel的内部接口Unsafe
他的源码如下,我贴出来了两个重要的方法, 关于这两个方法的解析,我写在代码的下面:

protected abstract class AbstractUnsafe implements Unsafe {

@Override
// todo 入参 eventLoop == SingleThreadEventLoop   promise == NioServerSocketChannel + Executor
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    if (!isCompatible(eventLoop)) {
        promise.setFailure(
                new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }
    // todo 赋值给自己的 事件循环, 把当前的eventLoop赋值给当前的Channel上  作用是标记后续的所有注册的操作都得交给我这个eventLoop处理, 正好对应着下面的判断
    // todo 保证了 即便是在多线程的环境下一条channel 也只能注册关联上唯一的eventLoop,唯一的线程
    AbstractChannel.this.eventLoop = eventLoop;

    // todo 下面的分支判断里面执行的代码是一样的!!, 为什么? 这是netty的重点, 它大量的使用线程, 线程之间就会产生同步和并发的问题
    // todo 下面的分支,目的就是把线程可能带来的问题降到最低限度
    // todo 进入inEventLoop() --> 判断当前执行这行代码的线程是否就是 SingleThreadEventExecutor里面维护的那条唯一的线程
    // todo 解释下面分支的必要性, 一个eventLoop可以注册多个channel, 但是channel的整个生命周期中所有的IO事件,仅仅和它关联上的thread有关系
    // todo 而且,一个eventLoop在他的整个生命周期中,只和唯一的线程进行绑定,
    //
    // todo 当我们注册channel的时候就得确保给他专属它的thread,
    // todo 如果是新的连接到了,
    if (eventLoop.inEventLoop()) {
        // todo 进入regist0()
        register0(promise);
    } else {
        try {
            // todo 如果不是,它以一个任务的形式提交  事件循环 , 新的任务在新的线程开始,  规避了多线程的并发
            // todo 他是SimpleThreadEventExucutor中execute()实现的,把任务添加到执行队列执行
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // todo 进入这个方法doRegister()
        // todo 它把系统创建的ServerSocketChannel 注册进了选择器
        doRegister();
        neverRegistered = false;
        registered = true;

        // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
        // user may already fire events through the pipeline in the ChannelFutureListener.
        // todo 确保在 notify the promise前调用 handlerAdded(...)
        // todo 这是必需的,因为用户可能已经通过ChannelFutureListener中的管道触发了事件。
        // todo 如果需要的话,执行HandlerAdded()方法
        // todo 正是这个方法, 回调了前面我们添加 Initializer 中添加 Accpter的重要方法
        pipeline.invokeHandlerAddedIfNeeded();

        // todo  !!!!!!!  观察者模式!!!!!!  通知观察者,谁是观察者?  暂时理解ChannelHandler 是观察者
        safeSetSuccess(promise);

        // todo 传播行为, 传播什么行为呢?   在head---> ServerBootStraptAccptor ---> tail传播事件ChannelRegistered  , 也就是挨个调用它们的ChannelRegisted函数
        pipeline.fireChannelRegistered();
        // Only fire a channelActive if the channel has never been registered. This prevents firing
        // multiple channel actives if the channel is deregistered and re-registered.
        // todo 对于服务端:  javaChannel().socket().isBound(); 即  当Channel绑定上了端口   isActive()才会返回true
        // todo 对于客户端的连接 ch.isOpen() && ch.isConnected(); 返回true , 就是说, Channel是open的 打开状态的就是true
        if (isActive()) {
            if (firstRegistration) {
                // todo 在pipeline中传播ChannelActive的行为,跟进去
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                // This channel was registered before and autoRead() is set. This means we need to begin read
                // again so that we process inbound data.
                //
                // See https://github.com/netty/netty/issues/4805
                // todo 可以接受客户端的数据了
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();

    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    // See: https://github.com/netty/netty/issues/576
    if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
        localAddress instanceof InetSocketAddress &&
        !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
        !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
        // Warn a user about the fact that a non-root user can't receive a
        // broadcast packet on *nix if the socket is bound on non-wildcard address.
        logger.warn(
                "A non-root user can't receive a broadcast packet if the socket " +
                "is not bound to a wildcard address; binding to a non-wildcard " +
                "address (" + localAddress + ") anyway as requested.");
    }

    boolean wasActive = isActive();
    // todo 由于端口的绑定未完成,所以 wasActive是 false

    try {
        // todo 绑定端口, 进去就是NIO原生JDK绑定端口的代码
        doBind(localAddress);
        // todo 端口绑定完成  isActive()是true
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }
    // todo 根据上面的逻辑判断, 结果为 true
    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            // todo 来到这里很重要, 向下传递事件行为, 传播行为的时候, 从管道的第一个节点开始传播, 第一个节点被封装成 HeadContext的对象
           // todo 进入方法, 去 HeadContext里面查看做了哪些事情
            // todo 她会触发channel的read, 最终重新为 已经注册进selector 的 chanel, 二次注册添加上感性趣的accept事件
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    // todo 观察者模式, 设置改变状态, 通知观察者的方法回调
    safeSetSuccess(promise);

AbstractChannel抽象内部类的register(EventLoop,channelPromise)方法
这个方法,是将channel注册进EventLoop的Selector, 它的调用顺序如下:

本类方法 regist()–> 本类方法 register0() --> 本类抽象方法doRegister()

doRegister() 在这里设计成抽象方法,等着子类去具体的实现, 为啥这样做呢?

刚才说了,AbstractChannel本身就是个模板,而且它仅仅维护了EventLoop,没有拿到channel引用的它根本不可能进行注册的逻辑,那谁有jdk原生channel的引用呢? 它的直接子类AbstractNioChannel下面是AbstractNioChannel的构造方法, 它自己维护jdk原生的Channel,所以由他重写doRegister()

*/ // todo 无论是服务端的channel 还是客户端的channel都会使用这个方法进行初始化
// // TODO: 2019/6/23                null        ServerSocketChannel       accept
// todo  如果是在创建NioSocketChannel  parent==NioServerSocketChannel  ch == SocketChanel
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);// todo  继续向上跟,创建基本的组件
    // todo 如果是创建NioSocketChannel   这就是在保存原生的jdkchannel
    // todo 如果是创建NioServerSocketChannel   这就是在保存ServerSocketChannel
    this.ch = ch;
    // todo 设置上感兴趣的事件
    this.readInterestOp = readInterestOp;
    try {
        // todo 作为服务端, ServerSocketChannel 设置为非阻塞的
        // todo 作为客户端   SocketChannel 设置为非阻塞的
        ch.configureBlocking(false);
    } catch (IOException e) {

AbstractChannel抽象内部类的bind()方法
bind()方法的调用顺序, 本类方法 bind()–> 本类的抽象方法 dobind()

private ChannelFuture doBind(final SocketAddress localAddress) {
    // 初始化及注册
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        // At this point we know that the registration was complete and successful.
        ChannelPromise promise = channel.newPromise();
        // 调用 doBind0
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        ....
    }
}

doBind 方法又会调用 doBind0() 方法, 在 doBind0() 方法中会通过 EventLoop 去执行 channel 的 bind()任务.

private static void doBind0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress localAddress, final ChannelPromise promise) {

    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                // 调用channel.bind接口
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

doBind0() 方法往下会调用到 pipeline.bind(localAddress, promise); 方法, 通过 pipeline 的传播机制, 最终会调用到 AbstractChannel.AbstractUnsafe.bind() 方法, 这个方法主要做两件事情:

调用 doBind(): 调用底层JDK API进行 Channel 的端口绑定.
调用 pipeline.fireChannelActive().
方法的目的是给Channel绑定上属于它的端口,同样有一个抽象方法,等着子类去实现,因为我们已经知道了AbstractChannel不维护channel的引用,于是我就去找dobind()这个抽象函数的实现, 结果发现,AbstractChannel的直接子类AbstractNioChannel中根本不没有他的实现,这是被允许的,因为AbstractNioChannel本身也是抽象类, 到底是谁实现呢? 如下图:在NioServerSocketChannel中获取出 Jdk原生的channel, 客户端和服务端的channel又不同,所以绑定端口这中特化的任务,交给他们自己实现
Netty组件Channel_第6张图片
AbstractChannel的beginRead()()方法
上面完成注册之后,就去绑定端口,当端口绑定完成,就会channel处于active状态,下一步就是执行beginRead() ,执行的流程如下

本类抽象方法 beginRead() --> 本类抽象方法doBeginRead()

这个read() 就是从已经绑定好端口的channel中读取IO数据,和上面的方法一样,对于没有channel引用的AbstractChannel来说,netty把它设计成抽象方法,交给拥有jdk 原生channel引用的AbstractNioChannel实现

AbstractChannel作为Channel的直接实现类,本身又是抽象类,于是它实现了Channel的预留的一些抽象方法, 初始化了channel的四个组件 id pipeline unsafe parent, 更为重要的是它的抽象内部类 实现了 关于nettyChannel的注册,绑定,读取数据的逻辑,而且以抽象类的方法,挖好了填空题等待子类的特化实现

递进AbstractNioChannel
跟进构造方法
依然是来到AbstractNioChannel的构造方法,发现它做了如下的构造工作:

把parent传递给了AbstractChannel
把子类传递过来的Channel要告诉Selector的感兴趣的选项保存
设置channel为非阻塞
重写了它父类的doRegister()
AbstractNioChannel维护channel的引用,真正的实现把 jdk 原生的 channel注册进 Selector中:

@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
    // todo  javaChannel() -- 返回SelectableChanel 可选择的Channel,换句话说,可以和Selector搭配使用,他是channel体系的顶级抽象类, 实际的类型是 ServerSocketChannel
    // todo  eventLoop().unwrappedSelector(), -- >  获取选择器, 现在在AbstractNioChannel中 获取到的eventLoop是BossGroup里面的
    // todo  到目前看, 他是把ServerSocketChannel(系统创建的) 注册进了 EventLoop的选择器
    // todo 这里的 最后一个参数是  this是当前的channel , 意思是把当前的Channel当成是一个 attachment(附件) 绑定到selector上 作用???
    // todo  现在知道了attachment的作用了
     //    todo 1. 当channel在这里注册进 selector中返回一个selectionKey, 这个key告诉selector 这个channel是自己的
     //    todo 2. 当selector轮询到 有channel出现了自己的感兴趣的事件时, 需要从成百上千的channel精确的匹配出 出现Io事件的channel,
    //     todo     于是seleor就在这里提前把channel存放入 attachment中, 后来使用
    // todo 最后一个 this 参数, 如果是服务启动时, 他就是NioServerSocketChannel   如果是客户端他就是 NioSocketChannel
    // todo 到目前为止, 虽然注册上了,但是它不关心任何事件
    selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
    return;
} catch (CancelledKeyException e) {

新增内部接口
AbstractNioChannel新添加了一个内部接口,作为原Channel的扩展,源码如下, 我们着重关心的就是这个新接口的 read()方法, 它的作用是从channel去读取IO数据,作为接口的抽象方法,它规范服务端和客户端根据自己需求去不同的实现这个read()

怎么特化实现这个read方法呢? 若是服务端,它read的结果就是一个新的客户端的连接, 如果是客户端,它read的结果就是 客户端发送过来的数据,所以这个read()很有必要去特化

/**
 * Read from underlying {@link SelectableChannel}
 */
// todo 两个实现类, NioByteUnsafe , 处理关于客户端发来的信息
// todo NioMessageUnsafe   处理客户端新进来的连接
void read();


/**
* Special {@link Unsafe} sub-type which allows to access the underlying {@link SelectableChannel}
*/
public interface NioUnsafe extends Unsafe {
/**
 * Return underlying {@link SelectableChannel}
 */
SelectableChannel ch();

/**
 * Finish connect
 */
void finishConnect();


void forceFlush();
}

AbstractNioChannel的抽象内部内同类时继承了它父类的AbstractUnsafe实现了当前的NioUnsafe, 再往后看, 问题来了, 服务端和客户端在的针对read的特化实现在哪里呢? 想想看肯定在它子类的unsafe内部类中,如下图,紫框:
Netty组件Channel_第7张图片
再进一步 AbstractNioMessageChannel
它的构造函数如下, 只是调用父类的构造函数,传递参数

protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    // todo 在进去
    // todo  null  ServerSocketChannel   accept
    super(parent, ch, readInterestOp);
}

AbstractNioMessageChannel的MessageNioUnsafe对read()特化实现
在read方法中,我们可以看到,他调用是本类的抽象方法doReadMessages(List buf), 方法的实现类是继承体系的最底层的NioServerSocketChannel, 因为他就是那个特化的服务端channel

当然如果我们一开始跟进read()时,来到的客户端的AbstractNioByteChannel,现在我们找到的doReadMessage()就是由 客户端的channelNioSocketChannel完成的doReadBytes()

// todo 用于处理新链接进来的内部类
private final class NioMessageUnsafe extends AbstractNioUnsafe {

// todo 这个容器用于存放临时读到的连接
private final List readBuf = new ArrayList();

// todo 接受新链接的 read来到这里
@Override
public void read() {
    ...
    doBeginRead(buf);
    ...
}

// todo 处理新的连接 是在 NioServerSocketChannel中实现的, 进入查看
protected abstract int doReadMessages(List buf) throws Exception;
 
  

最终,特化的channel实现
现在我们就来到了最底层,整张继承图就全部展现在眼前了,下面就去看看,特化的服务端Channel NioServerSocketChannel和NioSocketChannel对 doReadMessages()和doReadBytes()的各自实现

服务端, 我们看到了,它的特化read()是在创建新的 Jdk远程channel, 因为它在创建新的连接chanel

@Override
protected int doReadMessages(List buf) throws Exception {
    // todo java Nio底层在这里 创建jdk底层的 原生channel
    SocketChannel ch = SocketUtils.accept(javaChannel());

    try {
        if (ch != null) {
            // todo  把java原生的channel, 封装成 Netty自定义的封装的channel , 这里的buf是list集合对象,由上一层传递过来的
            // todo  this  --  NioServerSocketChannel
            // todo  ch --     SocketChnnel
            buf.add(new NioSocketChannel(this, ch));
            return 1;
        }
 
  

客户端, 读取客户端发送过来的IO数据
@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.attemptedBytesRead(byteBuf.writableBytes());
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

AbstractChannel维护NioChannel的EventLoop
AbstractNioChannel维护jdk原生 channel
AbstractChannel中的AbstractUnsafe主要是定义了一套模板,给子类提供了填空题,下面的三个填空
注册 把chanel注册进Selector
绑定 把chanel绑定上端口
添加感兴趣的事件, 给创建出来的channel二次注册上netty可以处理的感兴趣的事件
channel的io操作是unsafe内部类完成的
服务端从channel,读取出新连接NioMessageUnsafe
客户端从channel,读取出数据NioByteUnsafe

11 总结

Channel在netty中是做为一个关键的通道而存在的,后面的Event和Handler是以channel为基础运行的,所以说Channel就是netty的基础。
在Netty框架中,Channel是其中之一的核心概念,是Netty网络通信的主体,由它负责同对端进行网络通信、注册和数据操作等功能。本文我们来详细地分析 Netty 中的 Channel以及跟Channel相关的其他概念,包括ChannelPipeline、ChannelHandlerContext、ChannelHandler等
Netty对JDK原生的ServerSocketChannel进行了封装和增强, 相对于原生的JdkChannel, Netty的Channel增加了如下的组件

id 标识唯一身份信息
可能存在的parent Channel
管道 pepiline,用于链接handler
用于数据读写的unsafe内部类,实际IO操作都是该类实现的
关联唯一的EventLoop,EventGroup->EventLoop->Channel(后续章节再讲,其实就是类似线程池的东西)

channel生命周期

ChannelUnregistered 	Channel 已经被创建,但还未注册到 EventLoop
ChannelRegistered 	已经被注册到了 EventLoop
ChannelActive 	处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
ChannelInactive 	没有连接到远程节点

ChannelPipeline
每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline。这项关联是唯一不变的。
核心功能:

ChannelPipeline 保存了与 Channel 相关联第一个 ChannelHandler
ChannelPipeline 可以根据需要,通过添加或者删除 ChannelHandler 来动态地修改(典型应用,动态修改协议等)
ChannelPipeline 有着丰富的 API 用以被调用,以响应入站和出站事件

ChannelHandlerContext
ChannelHandlerContext类似上下文的概念,代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有 ChannelHandler 添加到 ChannelPipeline 中时,都会创建 ChannelHandlerContext。
ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。

核心功能:

ChannelHandlerContext使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler 交互。
ChannelHandler 可以通知其所属的 ChannelPipeline 中的下一个ChannelHandler
ChannelHandlerContext 具有丰富的用于处理事件和执行 I/O 操作的 API

ChannelHandler
生命周期:

handlerAdded 	当把 ChannelHandler 添加到 ChannelPipeline 中时被调用
handlerRemoved 	当从 ChannelPipeline 中移除 ChannelHandler 时被调用
exceptionCaught 	当处理过程中在 ChannelPipeline 中有错误产生时被调用
ChannelInboundHandler 的方法

ChannelInboundHandler 的方法
Netty组件Channel_第8张图片
ChannelOutboundHandler 的方法
Netty组件Channel_第9张图片
内存泄漏监控
在使用handler处理ByteBuf时,应正确处理其被引用次数,尤其是调用read和write后,应正确释放其引用。
(注意:SimpleChannelInboundHandler会自动释放引用)
Netty组件Channel_第10张图片

NioServerSocketChannel负责监听客户端的Accpet事件,当发生此事件时.那么它什么时候?或者在哪里知道有Accept事件呢?在之前的文章中我们知道,每一个Channel都和一个NioEventLoop关联,NioServerSocketChannel也和一个NioEventLoop关联.而NioEventLoop封装了一个IO Thread,由这个线程去监听Accept事件.

NioEventLoop主要由三个过程构成. 创建-启动-执行. 执行的环节是个无限循环.当IO Thread监听到有Accept事件后, 会调用JDK底层accept()方法获取到这个连接, 然后再封装成Netty自己的NioSocketChannel.这个就是对应的客户端Channel.

每一个Channel都与一个Pipeline关联, 在创建Channel的时候也会创建出来一个与之关联的Pipeline. Pipeline通过链表的方式将用户自定义的处理器Handler放在Pipeline中, 当Channel中有数据时, 就会由关联的Pipeline进行处理, 更具体说, 是由里面的每个Handler进行处理.

NioServerSocketChannel关联的Pipeline有一个默认的Handler,即ServerBootstrapAcceptor.它也是一个Handler.

你可能感兴趣的:(java,java,服务器,网络)