`
xiaoyao8903
  • 浏览: 22021 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
文章分类
社区版块
存档分类
最新评论

Netty源码阅读(一) ServerBootstrap启动

 
阅读更多

Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。本文讲会对Netty服务启动的过程进行分析,主要关注启动的调用过程,从这里面进一步理解Netty的线程模型,以及Reactor模式。

Netty源码阅读(一) ServerBootstrap启动

这是我画的一个Netty启动过程中使用到的主要的类的概要类图,当然是用到的类比这个多得多,而且我也忽略了各个类的继承关系,关于各个类的细节,可能以后会写单独的博客进行分析。在这里主要注意那么几个地方:

1. ChannelPromise关联了Channel和Executor,当然channel中也会有EventLoop的实例。 2. 每个channel有自己的pipeline实例。 3. 每个NioEventLoop中有自己的Executor实例和Selector实例。

网络请求在NioEventLoop中进行处理,当然accept事件也是如此,它会把接收到的channel注册到一个EventLoop的selector中,以后这个channel的所有请求都由所注册的EventLoop进行处理,这也是Netty用来处理竞态关系的机制,即一个channel的所有请求都在一个线程中进行处理,也就不会存在跨线程的冲突,因为这些调用都线程隔离了。

下面我们先看一段Netty源码里面带的example代码,直观感受一下Netty的使用:

1 // Configure the server. 2 EventLoopGroup bossGroup = new NioEventLoopGroup(1); 3 EventLoopGroup workerGroup = new NioEventLoopGroup; 4 try { 5 ServerBootstrap b = new ServerBootstrap; 6 b.group(bossGroup, workerGroup) 7 .channel(NioServerSocketChannel.class) 8 .option(ChannelOption.SO_BACKLOG, 100) // 设置tcp协议的请求等待队列 9 .handler(new LoggingHandler(LogLevel.INFO)) 10 .childHandler(new ChannelInitializer<SocketChannel> { 11 @Override 12 public void initChannel(SocketChannel ch) throws Exception { 13 ChannelPipeline p = ch.pipeline; 14 if (sslCtx != null) { 15 p.addLast(sslCtx.newHandler(ch.alloc)); 16 } 17 p.addLast(new EchoServerHandler); 18 } 19 }); 20 21 // Start the server. 22 ChannelFuture f = b.bind(PORT).sync; 23 24 // Wait until the server socket is closed. 25 f.channel.closeFuture.sync; 26 } finally { 27 // Shut down all event loops to terminate all threads. 28 bossGroup.shutdownGracefully; 29 workerGroup.shutdownGracefully; 30 }

首先我们先来了解Netty的主要类:

EventLoop这个相当于一个处理线程,是Netty接收请求和处理IO请求的线程。

EventLoopGroup可以理解为将多个EventLoop进行分组管理的一个类,是EventLoop的一个组。

ServerBootstrap从命名上看就可以知道,这是一个对服务端做配置和启动的类。

ChannelPipeline这是Netty处理请求的责任链,这是一个ChannelHandler的链表,而ChannelHandler就是用来处理网络请求的内容的。

ChannelHandler用来处理网络请求内容,有ChannelInboundHandler和ChannelOutboundHandler两种,ChannlPipeline会从头到尾顺序调用ChannelInboundHandler处理网络请求内容,从尾到头调用ChannelOutboundHandler处理网络请求内容。这也是Netty用来灵活处理网络请求的机制之一,因为使用的时候可以用多个decoder和encoder进行组合,从而适应不同的网络协议。而且这种类似分层的方式可以让每一个Handler专注于处理自己的任务而不用管上下游,这也是pipeline机制的特点。这跟TCP/IP协议中的五层和七层的分层机制有异曲同工之妙。

现在看上面的代码,首先创建了两个EventLoopGroup对象,作为group设置到ServerBootstrap中,然后设置Handler和ChildHandler,最后调用bind方法启动服务。下面按照Bootstrap启动顺序来看代码。

1 public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) { 2 super.group(parentGroup); 3 if (childGroup == null) { 4 throw new NullPointerException("childGroup"); 5 } 6 if (this.childGroup != null) { 7 throw new IllegalStateException("childGroup set already"); 8 } 9 this.childGroup = childGroup; 10 return this; 11 }

首先是设置EverLoopGroup,parentGroup一般用来接收accpt请求,childGroup用来处理各个连接的请求。不过根据开发的不同需求也可以用同一个group同时作为parentGroup和childGroup同时处理accpt请求和其他io请求。

1 public B channel(Class<? extends C> channelClass) { 2 if (channelClass == null) { 3 throw new NullPointerException("channelClass"); 4 } 5 return channelFactory(new ReflectiveChannelFactory<C>(channelClass)); 6 }

接下来的channel方法设置了ServerBootstrap的ChannelFactory,这里传入的参数是NioServerSocketChannel.class,也就是说这个ReflectiveChannelFactory创建的就是NioServerSocketChannel的实例。

后面的option,handler和childHandler分别是设置Socket连接的参数,设置parentGroup的Handler,设置childGroup的Handler。childHandler传入的ChannelInitializer实现了一个initChannel方法,用于初始化Channel的pipeline,以处理请求内容。

之前都是在对ServerBootstrap做设置,接下来的ServerBootstrap.bind才是启动的重头戏。我们继续按照调用顺序往下看。

1 public ChannelFuture bind(int inetPort) { 2 return bind(new InetSocketAddress(inetPort)); 3 } 4 5 /** 6 * Create a new {@link Channel} and bind it. 7 */ 8 public ChannelFuture bind(SocketAddress localAddress) { 9 validate; 10 if (localAddress == null) { 11 throw new NullPointerException("localAddress"); 12 } 13 return doBind(localAddress); 14 } 15 16 // AbstractBootstrap 17 private ChannelFuture doBind(final SocketAddress localAddress) { 18 final ChannelFuture regFuture = initAndRegister; 19 final Channel channel = regFuture.channel; 20 if (regFuture.cause != null) { 21 return regFuture; 22 } 23 24 if (regFuture.isDone) { 25 // At this point we know that the registration was complete and successful. 26 ChannelPromise promise = channel.newPromise; 27 doBind0(regFuture, channel, localAddress, promise); 28 return promise; 29 } else { 30 // Registration future is almost always fulfilled already, but just in case it's not. 31 final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); 32 regFuture.addListener(new ChannelFutureListener { 33 @Override 34 public void operationComplete(ChannelFuture future) throws Exception { 35 Throwable cause = future.cause; 36 if (cause != null) { 37 // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an 38 // IllegalStateException once we try to access the EventLoop of the Channel. 39 promise.setFailure(cause); 40 } else { 41 // Registration was successful, so set the correct executor to use. 42 // See https://github.com/netty/netty/issues/2586 43 promise.registered; 44 45 doBind0(regFuture, channel, localAddress, promise); 46 } 47 } 48 }); 49 return promise; 50 } 51 }

我们可以看到bind的调用最终调用到了doBind(final SocketAddress),在这里我们看到先调用了initAndRegister方法进行初始化和register操作。了解JavaNIO框架的同学应该能看出来是在这个方法中将channel注册到selector中的。最后程序再调用了doBind0方法进行绑定,先按照顺序看initAndRegister方法做了什么操作。

1 // AbstractBootstrap 2 final ChannelFuture initAndRegister { 3 Channel channel = null; 4 try { 5 channel = channelFactory.newChannel; 6 init(channel); 7 } catch (Throwable t) { 8 // ... 9 } 10 11 ChannelFuture regFuture = config.group.register(channel); 12 // ... 13 return regFuture; 14 }

为了简单其间,我忽略了处理异常分支的代码,同学们有兴趣可以自行下载Netty源码对照。在这里终于看到channel的创建了,调用的是ServerBootstrap的channelFactory,之前的代码我们也看到了这里的工厂是一个ReflectChannelFactory,在构造函数中传入的是NioServerSocketChannel.class,所以这里创建的是一个NioServerSocketChannel的对象。接下来init(channel)对channel进行初始化。

1 // ServerBootstrap 2 void init(Channel channel) throws Exception { 3 final Map<ChannelOption<?>, Object> options = options0; 4 synchronized (options) { 5 channel.config.setOptions(options); 6 } 7 8 // 设置channel.attr 9 final Map<AttributeKey<?>, Object> attrs = attrs0; 10 synchronized (attrs) { 11 for (Entry<AttributeKey<?>, Object> e: attrs.entrySet) { 12 @SuppressWarnings("unchecked") 13 AttributeKey<Object> key = (AttributeKey<Object>) e.getKey; 14 channel.attr(key).set(e.getValue); 15 } 16 } 17 18 ChannelPipeline p = channel.pipeline; 19 20 final EventLoopGroup currentChildGroup = childGroup; 21 // childGroup的handler 22 final ChannelHandler currentChildHandler = childHandler; 23 final Entry<ChannelOption<?>, Object> currentChildOptions; 24 final Entry<AttributeKey<?>, Object> currentChildAttrs; 25 synchronized (childOptions) { 26 currentChildOptions = childOptions.entrySet.toArray(newOptionArray(childOptions.size)); 27 } 28 synchronized (childAttrs) { 29 currentChildAttrs = childAttrs.entrySet.toArray(newAttrArray(childAttrs.size)); 30 } 31 // 给channelpipeline添加handler 32 p.addLast(new ChannelInitializer<Channel> { 33 @Override 34 public void initChannel(Channel ch) throws Exception { 35 final ChannelPipeline pipeline = ch.pipeline; 36 // group的handler 37 ChannelHandler handler = config.handler; 38 if (handler != null) { 39 pipeline.addLast(handler); 40 } 41 42 // We add this handler via the EventLoop as the user may have used a ChannelInitializer as handler. 43 // In this case the initChannel(...) method will only be called after this method returns. Because 44 // of this we need to ensure we add our handler in a delayed fashion so all the users handler are 45 // placed in front of the ServerBootstrapAcceptor. 46 ch.eventLoop.execute(new Runnable { 47 @Override 48 public void run { 49 pipeline.addLast(new ServerBootstrapAcceptor( 50 currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); 51 } 52 }); 53 } 54 }); 55 }

先是设置了channel的option和attr,然后将handler加入到channelpipleline的handler链中,这里大家请特别注意ServerBootstrapAcceptor这个Handler,因为接下来对于客户端请求的处理以及工作channl的注册可全是这个Handler处理的。不过由于现在channel还没有注册,所以还不会调用initChannel方法,而是将这个handler对应的context加入到一个任务队列中,等到channel注册成功了再执行。关于ChannelPipeline的内容我们以后再说。然后在initAndRegister方法中调用config.group.register(channel)对channel进行注册。config.group获取到的其实就是bossGroup,在这个例子中就是一个NioEventLoopGroup,由于它继承了MultithreadEventLoopGroup所以这里调用的其实是这个类的方法。

1 // MultithreadEventLoopGroup 2 public ChannelFuture register(Channel channel) { 3 return next.register(channel); 4 } 5 6 public EventLoop next { 7 return (EventLoop) super.next; 8 } 9 10 // SingleThreadEventLoop 11 public ChannelFuture register(Channel channel) { 12 return register(new DefaultChannelPromise(channel, this)); 13 } 14 15 @Override 16 public ChannelFuture register(final ChannelPromise promise) { 17 ObjectUtil.checkNotNull(promise, "promise"); 18 promise.channel.unsafe.register(this, promise); 19 return promise; 20 }

这里会获取EventLoopGroup中的一个EventLoop,其实我们用的是NioEventLoopGroup所以这里获取到的其实是NioEventLoop,而NioEventLoop继承了SingleThreadEventLoop,这里register方法调用的就是SingleThreadEventLoop中的方法。我们重遇来到了channel最终注册的地方,这里其实是调用了channel的unsafe对象中的register方法,也就是NioServerSocketChannel的方法,这个方法是在AbstractChannel祖先类中实现的,代码如下:

1 public final void register(EventLoop eventLoop, final ChannelPromise promise) { 2 if (eventLoop == null) { 3 throw new NullPointerException("eventLoop"); 4 } 5 if (isRegistered) { 6 promise.setFailure(new IllegalStateException("registered to an event loop already")); 7 return; 8 } 9 if (!isCompatible(eventLoop)) { 10 promise.setFailure( 11 new IllegalStateException("incompatible event loop type: " + eventLoop.getClass.getName)); 12 return; 13 } 14 // 设置eventLoop 15 AbstractChannel.this.eventLoop = eventLoop; 16 // 这里是跟Netty的线程模型有关的,注册的方法只能在channel的工作线程中执行 17 if (eventLoop.inEventLoop) { 18 register0(promise); 19 } else { 20 try { 21 eventLoop.execute(new Runnable { 22 @Override 23 public void run { 24 register0(promise); 25 } 26 }); 27 } catch (Throwable t) { 28 logger.warn( 29 "Force-closing a channel whose registration task was not accepted by an event loop: {}", 30 AbstractChannel.this, t); 31 closeForcibly; 32 closeFuture.setClosed; 33 safeSetFailure(promise, t); 34 } 35 } 36 } 37 38 // AbstractNioChannel 39 protected void doRegister throws Exception { 40 boolean selected = false; 41 for (;;) { 42 try { 43 selectionKey = javaChannel.register(eventLoop.selector, 0, this); 44 return; 45 } catch (CancelledKeyException e) { 46 // ... 47 } 48 } 49 } 50 51 // AbstractSelectableChannel 52 public final SelectionKey register(Selector sel, int ops,Object att) 53 throws ClosedChannelException 54 { 55 synchronized (regLock) { 56 if (!isOpen) 57 throw new ClosedChannelException; 58 if ((ops & ~validOps) != 0) 59 throw new IllegalArgumentException; 60 if (blocking) 61 throw new IllegalBlockingModeException; 62 SelectionKey k = findKey(sel); 63 if (k != null) { 64 k.interestOps(ops); 65 k.attach(att); 66 } 67 if (k == null) { 68 // New registration 69 synchronized (keyLock) { 70 if (!isOpen) 71 throw new ClosedChannelException; 72 k = ((AbstractSelector)sel).register(this, ops, att); 73 addKey(k); 74 } 75 } 76 return k; 77 } 78 }

这里先设置了channel的eventLoop属性,然后在接下来的一段代码中判断当前线程是否是channel的处理线程,也就是是不是eventLoop的线程,如果不是那么就将注册作为一个任务用EventLoop.execute执行。按照这里的执行顺序,当前线程肯定不是eventLoop的线程,所以会执行else分支,其实eventLoop的线程也是在这个调用中启动的。最后的注册是在AbstractSelectableChannel类的register方法中执行的。这里有个很奇怪的地方,这里注册的ops是0,也就是没有感兴趣的事件。这个地方我们后面在分析。

将channel注册到selector的代码就是这些了,我们回头分析EventLoop.execute(…),其实注册的代码是在这里面被调用的。

1 // SingleThreadEventExecutor 2 public void execute(Runnable task) { 3 if (task == null) { 4 throw new NullPointerException("task"); 5 } 6 7 boolean inEventLoop = inEventLoop; 8 if (inEventLoop) { 9 addTask(task); 10 } else { 11 startThread; 12 addTask(task); 13 if (isShutdown && removeTask(task)) { 14 reject; 15 } 16 } 17 18 if (!addTaskWakesUp && wakesUpForTask(task)) { 19 wakeup(inEventLoop); 20 } 21 }

如果当前线程是EventLoop的线程,就把task加到任务队列中去,如果不是,那么启动线程,然后再把task加入到任务队列。

1 // SingleThreadEventLoop 2 private void startThread { 3 if (STATE_UPDATER.get(this) == ST_NOT_STARTED) { 4 if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { 5 doStartThread; 6 } 7 } 8 } 9 10 private void doStartThread { 11 assert thread == null; 12 executor.execute(new Runnable { 13 @Override 14 public void run { 15 thread = Thread.currentThread; 16 if (interrupted) { 17 thread.interrupt; 18 } 19 20 boolean success = false; 21 updateLastExecutionTime; 22 try { 23 SingleThreadEventExecutor.this.run; 24 success = true; 25 } catch (Throwable t) { 26 logger.warn("Unexpected exception from an event executor: ", t); 27 } finally { 28 // Some clean work 29 } 30 } 31 }); 32 }

其实最后的线程还是要落到EventLoop中的executor里面,而NioEventLoop初始化的时候executor属性设置的是一个ThreadPerTaskExecutor,顾名思义也就是每个任务新建一个线程去执行,而在这个Task里面对EventLoop的thread属性进行了设置,并且最后执行SingleThreadEventExecutor.this.run,这个run方法在NioEventLoop中实现。

1 protected void run { 2 for (;;) { 3 try { 4 switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks)) { 5 case SelectStrategy.CONTINUE: 6 continue; 7 case SelectStrategy.SELECT: 8 select(wakenUp.getAndSet(false)); 9 if (wakenUp.get) { 10 selector.wakeup; 11 } 12 default: 13 // fallthrough 14 } 15 16 cancelledKeys = 0; 17 needsToSelectAgain = false; 18 final int ioRatio = this.ioRatio; 19 if (ioRatio == 100) { 20 processSelectedKeys; 21 runAllTasks; 22 } else { 23 final long ioStartTime = System.nanoTime; 24 25 processSelectedKeys; 26 27 final long ioTime = System.nanoTime - ioStartTime; 28 runAllTasks(ioTime * (100 - ioRatio) / ioRatio); 29 } 30 31 if (isShuttingDown) { 32 closeAll; 33 if (confirmShutdown) { 34 break; 35 } 36 } 37 } catch (Throwable t) { 38 logger.warn("Unexpected exception in the selector loop.", t); 39 40 // Prevent possible consecutive immediate failures that lead to 41 // excessive CPU consumption. 42 try { 43 Thread.sleep(1000); 44 } catch (InterruptedException e) { 45 // Ignore. 46 } 47 } 48 } 49 } 50 51 private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) { 52 if (selectedKeys.isEmpty) { 53 return; 54 } 55 56 Iterator<SelectionKey> i = selectedKeys.iterator; 57 for (;;) { 58 final SelectionKey k = i.next; 59 final Object a = k.attachment; 60 i.remove; 61 62 if (a instanceof AbstractNioChannel) { 63 // 处理ServerSocketChannl的事件,如accept 64 processSelectedKey(k, (AbstractNioChannel) a); 65 } else { 66 @SuppressWarnings("unchecked") 67 NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a; 68 processSelectedKey(k, task); 69 } 70 71 if (!i.hasNext) { 72 break; 73 } 74 75 if (needsToSelectAgain) { 76 selectAgain; 77 selectedKeys = selector.selectedKeys; 78 79 // Create the iterator again to avoid ConcurrentModificationException 80 if (selectedKeys.isEmpty) { 81 break; 82 } else { 83 i = selectedKeys.iterator; 84 } 85 } 86 } 87 }

这个就是Netty最后的Reactor模式的事件循环了,在这个循环中调用selector的select方法查询需要处理的key,然后processSelectedKeys方法进行处理。在这里因为之前在注册NioServerSocketChannel的时候把channel当作attachment当做attachment,所以如果key的attachement是AbstractNioChannel说明这个是ServerSocketChannel的事件,如connect,read,accept。

其实还有一些问题没有写清楚,如下:

1. ServerSocketChannel的interestOps的注册 2. accept请求的处理 3. 线程模型 4. pipeline的链式调用 5. buffer 。。。
这些我会继续写文章进行说明~(希望可以=_=)
分享到:
评论

相关推荐

    netty源码深入分析

    《Netty源码深入分析》是由美团基础架构部的闪电侠老师所分享的一系列关于Netty源码解析的视频教程。以下将根据标题、描述、标签以及部分内容等信息,对Netty及其源码进行深入剖析。 ### Netty简介 Netty是基于...

    netty源码和相关中文文档

    3. **Bootstrap**:启动引导类,用于配置并创建一个新的 ServerBootstrap 或 ClientBootstrap,用于启动服务器或客户端。 4. **Pipeline(管道)**:数据在 Channel 之间传输时会经过一系列处理步骤,这些步骤构成...

    netty源码分析之服务端启动全解析

    Netty是一款高性能的网络应用程序框架,它使用Java编程语言开发,主要用于网络应用程序的快速和易于开发,支持TCP和UDP...通过对Netty源码的深入分析,可以更好地理解其工作机制,对开发高性能的网络应用有极大的帮助。

    Netty源码解析-服务启动过程.pdf

    ### Netty源码解析——服务启动过程 #### 一、Netty概述 Netty是一个高性能、异步事件驱动的网络应用框架,它被广泛应用于快速开发高性能协议服务器和客户端。Netty通过高度优化的设计和实现提供了低延迟和高吞吐...

    netty源码解析视频

    ### Netty源码解析知识点概览 #### 一、Netty简介与应用场景 - **Netty**是一款由JBOSS提供的高性能的异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。 - **应用场景**:Netty广泛...

    Netty5.0架构剖析和源码解读.pdf

    NIO服务端可以通过ServerBootstrap辅助类来启动,而NIO客户端则可以通过SocketChannel来建立连接。NIO的出现解决了传统的BIO通信中的问题,提高了IO的效率。 Netty架构剖析 Netty架构可以分为三个部分:服务端、...

    netty源码 4.*版本

    通过阅读 Netty 源码,我们可以学习到以下知识点: - 理解 Java NIO 的工作原理,如何利用 Selector 监听多个 Channel 的事件。 - 掌握 ByteBuf 的内存管理策略,如何避免不必要的内存拷贝。 - 学习如何构建 ...

    Netty权威指南-Netty源码

    源码分析时,首先需要关注的是 Netty 的启动流程,这通常从 `ServerBootstrap` 类开始。ServerBootstrap 配置了 EventLoopGroup(包含多个 EventLoop)和 Channel 实例,如 NioServerSocketChannel。然后通过绑定...

    Netty源码依赖包

    在深入探讨Netty源码依赖包的相关...无论是想要深入了解Netty的内部机制还是希望通过学习优秀的开源项目来提升自己的技术能力,Netty源码依赖包都是一个非常宝贵的资源。希望本文能为你进一步探索Netty提供一定的帮助。

    Netty3.x 源码解析

    Netty源码阅读的目的通常有两个:一是因为工作中使用到了Netty,希望通过阅读源码来更加深入地了解它;二是出于对Java网络编程的兴趣,希望通过学习Netty来探索如何构建高性能网络应用。同时,Netty的代码结构组织...

    netty源码深入剖析.txt

    《Netty源码深入剖析》一书旨在帮助读者深入了解Netty框架的工作原理和技术细节,从基础知识入手,逐步过渡到高级优化技巧,使开发者能够更好地掌握并应用Netty于实际项目中。 ### 一、Netty简介与核心特性 Netty...

    Netty源码教程-4

    在本篇Netty源码教程的第四部分,我们将深入探讨Netty框架的核心组件和工作原理,以便更好地理解和利用这个高性能、异步事件驱动的网络应用框架。Netty被广泛应用于分布式系统、高并发服务器和复杂网络协议的实现,...

    高清Netty5.0架构剖析和源码解读

    NIO客户端13 3.Netty源码分析16 3.1. 服务端创建16 3.1.1. 服务端启动辅助类ServerBootstrap16 3.1.2. NioServerSocketChannel 的注册21 3.1.3. 新的客户端接入25 3.2. 客户端创建28 3.2.1. 客户端连接辅助类...

    netty实战源码13章

    在描述中提到的 "netty 实战源码 13 章",可能是指一系列关于 Netty 使用和实现的教程,覆盖了从基础到进阶的多个主题。这通常会包括 Channel、Handler、ByteBuf、Pipeline 等核心组件的使用,以及如何构建服务器和...

    Netty3.x源码解析.docx

    Netty 是一个功能强大且高效的网络应用框架,阅读 Netty 的源码可以帮助我们更好地理解网络编程的领域知识和代码结构组织的方法。同时,Netty 的设计思想和编程模型也能够帮助我们提高自己的编程技能。

    netty5.0架构剖析和源码解读

    Netty源码的分析主要围绕服务端和客户端的创建、读写操作等方面进行。服务端的启动涉及到ServerBootstrap类,以及NioServerSocketChannel的注册,新的客户端接入,客户端连接的建立通过Bootstrap类实现。读写操作则...

Global site tag (gtag.js) - Google Analytics