Netty源码阅读(2)之——服务端源码梗概

上文我们把客户端源码梗概大致了解了一下,这样再了解服务端源码就轻松一点,我们将从服务端和客户端的区别着手去解析。

目录

区别

 ④

 ①


区别

Netty源码阅读(2)之——服务端源码梗概_第1张图片

 ④

客户端:.option(ChannelOption.TCP_NODELAY, true)

在TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。这里就涉及到一个名为Nagle的算法,该算法的目的就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
TCP_NODELAY就是用于启用或关于Nagle算法。如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭Nagle算法;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false。

服务端:.option(ChannelOption.SO_BACKLOG, 100)

服务器端TCP内核模块维护有2个队列,我们称之为A,B,客户端向服务端connect的时候,第二次握手时TCP内核模块把客户端连接加入到A队列中,然后第三次握手时TCP把客户端连接从A队列移到B队列。连接完成,应用程序的accept会返回,accept从B队列中取出完成三次握手的连接。

A队列和B队列的长度之和是backlog,当A,B队列的长之和大于backlog时,新连接将会被TCP内核拒绝,所以,如果backlog过小,可能会出现accept速度跟不上,A.B 队列满了,导致新客户端无法连接,要注意的是,backlog对程序支持的连接数并无影响,backlog影响的只是还没有被accept 取出的连接

 常用的还有:.option(ChannelOption.SO_REUSEADDR, true)

参数表示允许重复使用本地地址和端口, 比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,
使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,

客户端:.channel(NioSocketChannel.class)

服务端:.channel(NioServerSocketChannel.class)

他们的如何加载以及类型确定的过程是相同的,所以我们着重来看下实例化的不同。

和NioSocketChannel相似,调用了newSocket方法,但是接下来调用有所不同。服务端调用的是openServerSocketChannel方法,而客户端调用的是openSocketChannel方法,顾名思义,一个是客户端的 Java SocketChannel, 一个是服务器端的 Java ServerSocketChannel。

Netty源码阅读(2)之——服务端源码梗概_第2张图片

 接下来调用重载方法,这里传入的是SelectionKey.OP_ACCEPT,有 Java NIO Socket 相关知识的朋友明白, Java NIO 是一种 Reactor 模式, 我们通过 selector 来实现 I/O 的多路复用复用. 在一开始时, 服务器端需要监听客户端的连接请求, 因此在这里我们设置了 SelectionKey.OP_ACCEPT。我们还有客户端的印象吗?下图做对比

 接着逐步调用构造器,和客户端代码一样的,会实例化unsafe和pipeline

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

但是newUnsafe又有所不同,服务端 unsafe 字段其实是一个 AbstractNioMessageChannel#AbstractNioUnsafe 的实例。而客户端是AbstractNioByteChannel#NioByteUnsafe的实例

 ①

在客户端的时候, 我们只提供了一个 EventLoopGroup 对象, 而在服务器端的初始化时, 我们设置了两个 EventLoopGroup, 一个是 bossGroup, 另一个是 workerGroup. 那么这两个 EventLoopGroup 都是干什么用的呢? 其实呢, bossGroup 是用于服务端 的 accept 的, 即用于处理客户端的连接请求. 而 workerGroup, 其实就是实际上干活的啦, 它们负责客户端连接通道的 IO 操作。如下图所示

Netty源码阅读(2)之——服务端源码梗概_第3张图片

服务器端 bossGroup 不断地监听是否有客户端的连接, 当发现有一个新的客户端连接到来时, bossGroup 就会为此连接初始化各项资源, 然后从 workerGroup 中选出一个 EventLoop 绑定到此客户端连接中. 那么接下来的服务器与客户端的交互过程就全部在此分配的 EventLoop 中了 。来跟进源码分析一下。

    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup); // 1
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        // 2
        this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
        return this;
    }

上边代码做了两件事。1是进入AbstractBootstrap的group方法指定group属性,在客户端就是直接来到这里。2是指定ServerBootstrap的childGroup属性。初始化完属性后,那么在哪里用到他们呢?

bossGroup:AbstractBootstrap.bind -> AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister把boosGroup和NioServerSocketChannsl 关联起来了

ChannelFuture regFuture = config().group().register(channel);

 workGroup:仍然是initAndRegister方法里,出现的init(channel)。

   void init(Channel channel) {
        // 只贴出和childGroup有关的关键代码
        final EventLoopGroup currentChildGroup = childGroup;
        p.addLast(new ChannelInitializer() {
            @Override
            public void initChannel(final Channel ch) {
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

init 方法在 ServerBootstrap 中重写了,而childGroup主要是封装成为ServerBootstrapAcceptor类,所以我们着重来看这个类。

我们点进去一看,原来是个静态内部类。有关childGroup的操作主要是放在了类重写的channelRead方法里。

 childGroup.register(child).addListener(...);

这里的child是一个NioSocketChannel,意思是workerGroup 中的一个 EventLoop 和 NioSocketChannel 关联了。

那么,这个channelRead方法什么时候被调用呢?当一个 client 连接到 server 时, Java 底层的 NIO ServerSocketChannel 会有一个 SelectionKey.OP_ACCEPT 就绪事件, 接着就会调用到 NioServerSocketChannel.doReadMessages。

            SocketChannel ch = SocketUtils.accept(javaChannel());
            if (ch != null) {
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }

 通过accept方法获取到客户端的连接,然后封装为NioSocketChannel,传入的this就是指NioServerSocketChannel也就是封装NioSocketChannel的父channel。接下来就经由 Netty 的 ChannelPipeline 机制, 将读取事件逐级发送到各个 handler 中, 于是就会触发前面我们提到的 ServerBootstrapAcceptor.channelRead。

和boosGroup、workGroup一样,这里出现了handler、childHandler,那么他会不会和前者两个一样?一个处理连接,一个处理IO事件?我们来看下代码。

在④中我们提到了init 方法在 ServerBootstrap 中的重写。里边也牵扯到了对handler的操作

        final ChannelHandler currentChildHandler = childHandler; // 1

        p.addLast(new ChannelInitializer() {
            @Override
            public void initChannel(final Channel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler(); //2
                if (handler != null) {
                    pipeline.addLast(handler);
                }

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

首先看2(上边代码注释2),这里的config.handler()就是服务端代码.handler(new LoggingHandler(LogLevel.INFO))指定的handler,如果不为空则添加到pipeline。

Netty源码阅读(2)之——服务端源码梗概_第4张图片

 接着来看1(上边代码注释1),这里的childHandler仍然是构建ServerBootstrapAcceptor的属性,点进这个类,发现这个childHandler在ServerBootstrapAcceptor重写的channelRead方法里(还记得在什么时候调用吗?在连接建立起来之后)

child.pipeline().addLast(childHandler);

还记得这个child吗,他就是一个NioSocketChannel,在他的pipeline中添加了childHandler。

小结以下就是

  • handler 是在 accept 阶段起作用, 它处理客户端的连接请求.
  • childHandler 是在客户端连接建立以后起作用, 它负责客户端连接的 IO 交互.

你可能感兴趣的:(源码,tcp/ip,java,netty)