Netty源码分析--客户端连接接入流程解析

前言

本章将解析新连接接入时,Netty底层的具体实现机制。阅读前,建议先熟悉前文介绍的 Reactor线程模型服务端启动流程

一、Reactor线程模型简述

Netty的核心在于两类Reactor线程,它们共同驱动整个框架的运行:

  • Boss线程:专门负责接收新连接,并将其封装为Channel后交给Worker线程。
  • Worker线程:负责已建立连接的数据读写。

无论Boss还是Worker线程,都遵循以下三步循环:

  1. 轮询Selector上的IO事件;
  2. 处理相应IO事件;
  3. 执行异步任务(Task)。

Boss线程主要轮询ACCEPT事件(新连接),Worker线程则主要处理READ/WRITE事件(读写操作)。

二、服务端启动流程简述

服务端在用户线程中通过bind方法启动,首次提交异步任务时会触发Boss线程的运行,从而正式开始监听客户端连接。

新连接接入的总体流程

1.检测到有新连接

2.将新连接注册到worker线程

3.注册新连接的读事件

监测到新连接

我们在前面服务端启动流程分析中知道,调用bind方法启动服务端之后,服务端的Channel,即NioServerSocketChannel,已经注册到boss Reactor线程,Reactor线程不断检测是否有新的事件,直到检测出有ACCEPT事件发生。

csharp 复制代码
NioEventLoop.java  

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {  
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();  
int readyOps = k.readyOps();  
    if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps   
== 0) {  
        unsafe.read();  
    }  
}

这段代码表示boss Reactor线程已经轮询到SelectionKey.OP_ACCEPT事件,即表明有新连接进入,此时将调用Channel的Unsafe来进行实际的操作。

提问:为什么新连接建立会先走到boss线程?

  • 我们知道网络事件的源头是来自操作系统内核的事件通知机制,当新连接建立的网络包SYN到达时,此刻协议栈在内存空间中为这个待建立的连接分配一块内存结构来表示这个半连接,并将这块内存结构保存在半连接队列中。在三次握手完成后,操作系统将这个连接从半连接队列移动至全连接队列并生成fd指向这个新连接。
  • 而我们的serverSocketChannel对象在操作系统层面指向监听socket的fd,对应一个端口的监听点。如果读者读过我之前关于网络是如何连接的系列文章,则应该清楚协议栈的内部结构,我们服务的监听端口在协议栈内部是有一块内存空间用来记录一些连接信息的,我们把这块内存空间叫做socket,而serverSocketChannel则指向这个socket的fd。
  • 因此,当新连接的fd生成时,serverSocketChannel可以通过内核数据结构(全连接队列)知晓有新的连接生成了。此时,操作系统的epoll函数通知应用层有新的ACCEPT事件。由于应用层中只有boss线程的Selector注册了ACCEPT事件,接下来会就会进入到boss线程的unsafe.read()方法,boss线程调用accept()方法取出新连接的fd并生成netty层面的连接对象并交由worker线程处理,这个我们马上就会讲到。

注册Reactor线程

在服务端启动流程解析章节中,我们已经知道,服务端对应的Channel的Unsafe是NioMessageUnsafe,我们进入它的read方法,进入新连接处理的第二步。

scss 复制代码
NioMessageUnsafe.java  
  
private final List<Object> readBuf = new ArrayList<Object>();  
public void read() {  
    assert eventLoop().inEventLoop();  
    final ChannelPipeline pipeline = pipeline();  
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();  
    do {  
        // 1.创建 NioSocketChannel  
        int localRead = doReadMessages(readBuf);  
        if (localRead == 0) {  
            break;  
        }  
    } while (allocHandle.continueReading());  
    // 2.设置并绑定 NioSocketChannel  
    int size = readBuf.size();  
    for (int i = 0; i < size; i ++) {  
        pipeline.fireChannelRead(readBuf.get(i));  
 }  
    readBuf.clear();  
    pipeline.fireChannelReadComplete();  
}

read()方法使用一个List来保存从doReadMessages()读取到的连接。然后使用for循环调用pipeline的firChannelRead()方法,将每个新连接都过一遍服务端channel的pipeline处理逻辑。之后清理容器,触发pipeline.fireChannelReadComplete()。

我们来看这两个方法

一、创建NioSocketChannel:doReadMessages(List)

java 复制代码
NioServerSocketChannel.java  
  
protected int doReadMessages(List<Object> buf) throws Exception {  
    // 1. 创建 JDK 领域的 Channel  
    SocketChannel ch = javaChannel().accept();// NIO中的serverSocket.accept  
    // 2. 封装为 Netty 领域的 Channel  
    if (ch != null) {  
        buf.add(new NioSocketChannel(this, ch));  
        return 1;  
    }  
    return 0;  
}
  • 首先调用accept方法返回JDK NIO底层创建的channel,由于是已经轮询到accept事件,所以此处accept方法立刻返回。
  • 接下来将NIO的channel包装成Netty的NioSocketChannel。我们在前面分析NioServerSocketChannel的创建过程时,会创建Netty的一系列核心组件,包括Pipeline、Unsafe等。这里的new NioSocketChannel()也会创建相同的组件。区别在于NioServerSocketChannel在创建时注册的是ACCEPT事件,NioSocketChannel注册READ事件。

二、Netty中SocketChannel的结构

  • 这张简图展示了Netty中最常用的Channel的结构,我们简要说明

1.Channel继承AttributeMap表示Channel是可以绑定属性的对象,在用户代码中,我们经常使用channel.attr(...)来给Channel绑定属性,其实就是把属性设置到AttributeMap中。

2.DefaultAttributeMap为AttributeMap的默认实现,后面的Channel继承了它,可以直接使用。

3.AbstractChannel用于实现Channel的大部分方法,其中我们最熟悉的就是在其构造方法中,创建一条Channel的基本组件,这里的Channel通常包括SocketChannel和ServerSocketChannel。

4.AbstractNioChannel基于AbstractChannel做了NIO相关的一些操作,保存JDK底层的SelectableChannel的引用,并且在构造方法中设置Channel为非阻塞。设置非阻塞这一点对于NIO编程是必不可少的。

5.最后,就是两大Channel---NioServerSocketChannel和NioSocketChannel,分别对应着服务端接收新连接过程和新连接读写过程。

三、设置并绑定NioSocketChannel:fireChannelRead()

笔者默认读者具有一定netty基础,因此不在赘述pipeline等概念 不难知道,此处的pipeline是ServerSocketChannel的pipeline对象,在前面服务端启动流程的章节中我们分析过,在init()方法中有过这么一段代码

java 复制代码
p.addLast(new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }
        ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                pipeline.addLast(new ServerBootstrapAcceptor(
                        currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
    }
});

config.handler()方法会获取到服务端启动流程中启动示例代码里定义的ChannelInitializer对象,NioServerSocketChannel 初始化完成后,Netty 会触发 ChannelInitializer 的 initChannel()将LoggingHandler实际添加到pipeline中,之后ChannelInitializer 自己就会自动从 pipeline 中移除。

typescript 复制代码
.handler(new ChannelInitializer<NioServerSocketChannel>() {
    @Override 
    protected void initChannel(NioServerSocketChannel ch) { 
        ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO)); 
    }
})

之后继续执行这段代码

typescript 复制代码
ch.eventLoop().execute(new Runnable() { 
    @Override 
    public void run() {
        pipeline.addLast(new ServerBootstrapAcceptor( currentChildGroup, 
            currentChildHandler, currentChildOptions, currentChildAttrs)); 
} });

这段代码实际上是向netty提交一个任务,这个任务的作用是将ServerBootstrapAcceptor这个内部handler添加到服务端channel的pipeline链上。

此时,ServerSocketChannel的pipeline链是这样的:head-->LoggingHandler-->ServerBootstrapAcceptor-->head

pipeline.fireChannelRead()方法会执行到ServerBootstrapAcceptor的channelRead()方法,我们来看一下这个方法。

scss 复制代码
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;

    child.pipeline().addLast(childHandler);

    for (Entry<ChannelOption<?>, Object> e: childOptions) {
        try {
            if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
                logger.warn("Unknown channel option: " + e);
            }
        } catch (Throwable t) {
            logger.warn("Failed to set a channel option: " + child, t);
        }
    }

    for (Entry<AttributeKey<?>, Object> e: childAttrs) {
        child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
    }

    try {
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}

这个方法干了如下几件事

  • 1.将用户代码中定义的handler添加到socketChannel的pipeline中,这里的childHandler其实就是用户代码中在服务端启动时添加的handler。

具体来说,就是将childHandler()方法中的ChannelInitializer这个特殊的handler添加到socketChannel的pipeline中。这个ChannelInitializer的initChannel()方法会在后续流程中执行,最终将SimpleServerHandler()添加到pipeline中并移除自身。

  • 2.将childOptions和childAttrs分别设置代config和attr中。这些属性也是在用户代码中定义的,主要来用控制TCP底层的一些配置和行为。

  • 3.绑定Reactor线程。对于childGroup.register(child),这里的childGroup就是我们在用户代码里创建的workerNioEventLoopGroup。register()方法首先会会通过next()方法拿到一个工作线程。

ruby 复制代码
MultithreadEventExecutorGroup.java

@Override
public EventExecutor next() {
    return chooser.next();
}

这里的choser对象就是前面章节中分析到的EventExecutorChooser,它的作用是从NioEventLoopGroup中,选择一个NioEventLoop,所以,最终childGroup.register(child)会调用NioEventLoop的register方法,由其父类SingleThreadEventLoop来实现。

arduino 复制代码
SingleThreadEventLoop.java

@Override

public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}

@Override
public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    promise.channel().unsafe().register(this, promise);
    return promise;
}

看到这里,接下来的流程就和服务端启动流程中的注册Channel的模板一样,都由AbstractUnsafe来执行。我们再来看一遍这个过程。

AbstractUnsafe最终会执行register0()这个方法

scss 复制代码
private void register0(ChannelPromise promise) {
    try {
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        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.
        pipeline.invokeHandlerAddedIfNeeded();

        safeSetSuccess(promise);
        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.
        if (isActive()) {
            if (firstRegistration) {
                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
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}

这个方法的逻辑还是比较清晰的。

  • 首先调用doRegister()方法进行真正的注册过程。
ini 复制代码
@Override
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                eventLoop().selectNow();
                selected = true;
            } else {
                throw e;
            }
        }
    }
}

javaChannel.register将socketChannel绑定到selector上并将netty层名的channel(this)作为attachment挂到 SelectionKey上,方便后续取用。

  • 接下来配置用户自定义handler。 pipeline.invokeHandlerAddedIfNeeded(); 到目前为止,NioSocketChannel的Pipeline中有三个Handler:head->ChannelInitializer->tail。接下来,invokeHandlerAddedIfNeeded最终会调用ChannelInitializer的handlerAdded方法。
scss 复制代码
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    if (ctx.channel().isRegistered()) {
        initChannel(ctx);
    }
}

initChannel()方法会将我们在用户代码中定义的handler添加到socketChannel的pipeline中。

  • 传播ChannelRegistered事件。
    pipeline.fireChannelRegistered()其实没有干特别的事情,最终只是把连接注册事件往下传播,调用了每一个Handler的channelRegistered方法。

注册新连接的读事件

在连接建立后,isActive()方法返回true,程序最终会执行doBeginRead()方法

java 复制代码
AbstractNioChannel.java

@Override
protected void doBeginRead() throws Exception {
    final SelectionKey selectionKey = this.selectionKey;
    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

这里其实就是将SelectionKey.OP_READ事件注册到Selector,表示这条管道已经可以开始处理读事件。至此,新连接接入的流程就算结束了。

小结

当boss Reactor线程在检测到有ACCEPT事件之后,创建JDK底层的Channel,然后使用一个NioSocketChannel包装JDK底层的Channel,把用户设置的ChannelOption、ChannelAttr、ChannelHandler都设置到NioSocketChannel中。

接着,从worker Reactor线程组,也就是worker NioEventLoopGroup选择一个NioEventLoop,把NioSocketChannel包装的JDK的Channel当作key,自身当作attachment,注册到NioEventLoop对应的Selector。这样,后续有读写事件发生时,就可以直接获得attachment,也就是NioSocketChannel,来处理读写数据逻辑。

Netty源码的核心流程分析至此已基本完成。若读者从本系列首篇伊始持续阅读至此,理应对Netty框架形成较为完整的认知体系。在后续章节中,我们将聚焦于Netty中若干局部但至关重要的技术要点,通过深入底层机制剖析,引领读者共同探究Netty与网络编程的本质内涵

相关推荐
L47544 小时前
SSL/TLS证书:保障网站安全的关键
网络协议·安全·ssl·tls
我只有一岁半7 小时前
java17中,使用原生url connection的方式去创建的http链接,使用的是http1.1还是2.0?
网络·网络协议·http
00后程序员张7 小时前
HTTPS 包 抓取与分析实战,从抓包到解密、故障定位与真机取证
网络协议·http·ios·小程序·https·uni-app·iphone
局i7 小时前
HTTP与HTTPS的区别
网络协议·http·https
小糖学代码9 小时前
网络:2.Socket编程UDP
网络·网络协议·udp
m0_6117799610 小时前
MQTT和WebSocket的差别
网络·websocket·网络协议
wang090712 小时前
网络协议之DNS
网络·网络协议
Allen Roson1 天前
Burp Suite抓包软件使用说明1-Http history
网络·网络协议·http
爱吃芒果的蘑菇1 天前
C++之WebSocket初体验
网络·c++·websocket·网络协议