目录
源码工程
写在前面
图解几个重要概念
父子 channel
EventLoop 线程与线程组
通道与 Reactor 线程组
Channel 通道的类型
启动器初步介绍
图解 Bootstrap 执行流程
1: 设置 reactor 线程组
2 : 设置通道的 IO 类型
3: 设置监听端口
4: 设置通道参数
option 设置的参数:
5: 装配流水线
6: 开始绑定 server
7: ChannelFuture
8 优雅关闭 EventLoopGroup
疯狂创客圈 Java 死磕系列
疯狂创客圈 Java 分布式聊天室 [ 亿级流量] 实战系列之 18 [博客园 总入口 ]
源码工程
源码 IDEA 工程获取链接: Java 聊天室 实战 源码
写在前面
大家好, 我是作者尼恩.
今天是百万级流量 Netty 聊天器 打造的系列文章的第 18 篇, 这是一个基础篇, 介绍 Bootstrap.
力争以图文并茂的形式, 做到非常的易懂.
图解几个重要概念
下面的几个概念, 非常重要.
之前没有认真介绍, 下面图解说明一下.
父子 channel
在 Netty 中, Channel 是一个 Socket 连接的抽象, 它为用户提供了关于底层 Socket 状态 (是否是连接还是断开) 以及对 Socket 的读写等操作.
每当 Netty 建立了一个连接后, 都会有一个对应的 Channel 实例.
并且, 有父子 channel 的概念. 服务器连接监听的 channel , 也叫 parent channel. 对应于每一个 Socket 连接的 channel, 也叫 child channel.
EventLoop 线程与线程组
在看本文之前, 如果不明白 reactor 线程和 reactor 模式, 请 查看 疯狂创客圈的专门文章: Reactor 模式 .
在 Netty 中, 每一个 channel 绑定了一个 thread 线程.
一个 thread 线程, 封装到一个 EventLoop , 多个 EventLoop , 组成一个线程组 EventLoopGroup.
反过来说, EventLoop 这个相当于一个处理线程, 是 Netty 接收请求和处理 IO 请求的线程. EventLoopGroup 可以理解为将多个 EventLoop 进行分组管理的一个类, 是 EventLoop 的一个组.
他们的对应关系, 大致如下:
通道与 Reactor 线程组
这里主要是涉及的是服务器端.
服务器端, 一般有设置两个线程组, 监听连接的 parent channel 工作在一个独立的线程组, 这里名称为 boss 线程组 (有点像负责招人的包工头).
连接成功后, 负责客户端连接读写的 child channel 工作在另一个线程组, 这里名称为 worker 线程组, 专门负责搬数据 (有点儿像搬砖).
Channel 通道的类型
除了 TCP 协议以外, Netty 还支持很多其他的连接协议, 并且每种协议还有 NIO(异步 IO) 和 OIO(Old-IO, 即传统的阻塞 IO) 版本的区别.
不同协议不同的阻塞类型的连接都有不同的 Channel 类型与之对应, 下面是一些常用的 Channel 类型:
NioSocketChannel, 代表异步的客户端 TCP Socket 连接.
NioServerSocketChannel, 异步的服务器端 TCP Socket 连接.
NioDatagramChannel, 异步的 UDP 连接
NioSctpChannel, 异步的客户端 Sctp 连接.
NioSctpServerChannel, 异步的 Sctp 服务器端连接.
OioSocketChannel, 同步的客户端 TCP Socket 连接.
OioServerSocketChannel, 同步的服务器端 TCP Socket 连接.
OioDatagramChannel, 同步的 UDP 连接
OioSctpChannel, 同步的 Sctp 服务器端连接.
OioSctpServerChannel, 同步的客户端 TCP Socket 连接.
启动器初步介绍
Bootstrap 是 Netty 提供的一个便利的工厂类, 可以通过它来完成 Netty 的客户端或服务器端的 Netty 初始化.
当然, Netty 的官方解释说, 可以不用这个启动器.
但是, 一点点去手动创建 channel 并且完成一些的设置和启动, 会非常麻烦. 还是使用这个便利的工具类, 会比较好.
有两个启动器, 分别应用在服务器和客户端.
如下图:
两个启动器大致的配置, 都是相同的.
下面以服务器 serverBootstrap 启动类为主要的介绍对象.
图解 Bootstrap 执行流程
首先, 创建了一个引导器 ServerBootstrap 实例, 这个专门用于引导服务端的启动工作, 直接 new 创建即可.(客户端的引导器差不多, 不过是创建 Bootstrap 实例)
- // 启动引导器
- private static ServerBootstrap b = new ServerBootstrap();
启动一个 Bootstrap, 大致有 8 步, 如下图:
代码如下:
- try { //1 设置 reactor 线程
- b.group(bossLoopGroup, workerLoopGroup);
- //2 设置 nio 类型的 channel
- b.channel(NioServerSocketChannel.class);
- //3 设置监听端口
- b.localAddress(new InetSocketAddress(port));
- //4 设置通道选项
- b.option(ChannelOption.SO_KEEPALIVE, true);
- b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
- //5 装配流水线
- b.childHandler(new ChannelInitializer<SocketChannel>()
- {
- // 有连接到达时会创建一个 channel
- protected void initChannel(SocketChannel ch) throws Exception
- {
- ch.pipeline().addLast(new ProtobufDecoder());
- ch.pipeline().addLast(new ProtobufEncoder());
- // pipeline 管理 channel 中的 Handler
- // 在 channel 队列中添加一个 handler 来处理业务
- ch.pipeline().addLast("serverHandler", serverHandler);
- }
- });
- // 6 开始绑定 server
- // 通过调用 sync 同步方法阻塞直到绑定成功
- ChannelFuture channelFuture = b.bind().sync();
- LOGGER.info(ChatServer.class.getName() +
- "started and listen on" +
- channelFuture.channel().localAddress());
- // 7 监听通道关闭事件
- // 应用程序会一直等待, 直到 channel 关闭
- ChannelFuture closeFuture= channelFuture.channel().closeFuture();
- closeFuture.sync();
- } catch (Exception e)
- {
- e.printStackTrace();
- } finally
- {
- // 8 优雅关闭 EventLoopGroup,
- // 释放掉所有资源包括创建的线程
- workerLoopGroup.shutdownGracefully();
- bossLoopGroup.shutdownGracefully();
- }
接下来就是精彩的 8 个步骤.
1: 设置 reactor 线程组
在设置 reactor 反应器线程组之前, 创建了两个 NioEventLoopGroup 线程组:
bossLoopGroup 表示服务器连接监听线程组, 专门接受 accept 新的客户端 client 连接
workerGroup 表示处理每一条连接的数据收发的线程组
在线程组和启动器都创建完成后, 就可以开始设置线程组: 通过 b.group(bossGroup, workerGroup) 方法, 给引导器配置两大线程组.
配置完成之后, 整个引导类的 reactor 线程正式确定. 这里确定的工作模式, 为父子线程的模型.
也可以不设置两个线程组, 只设置一个线程组.
如果只设置一个线程组, 具体的方法为 -- b.group( workerGroup) .
配置完成一个线程组, 则所有的 channel , 包括服务监听通道父亲 channel 和所有的子 channel , 都工作在同一个线程组中.
说明一下, 一个线程组, 可不止一条线程哈.
2 : 设置通道的 IO 类型
Netty 不止支持 Java NIO , 也支持阻塞式的 BIO (在 Netty 中 叫做 OIO).
这里配置的是 NIO, 方法如下.
- //2 设置 nio 类型的 channel
- b.channel(NioServerSocketChannel.class);
如果想指定 IO 模型为 BIO, 那么这里配置上 Netty 的 OioServerSocketChannel.class 类型即可. 由于 NIO 的优势巨大, 通常不会在 Netty 中使用 BIO.
3: 设置监听端口
- //3 设置监听端口
- b.localAddress(new InetSocketAddress(port));
这是最为简单的一步操作.
4: 设置通道参数
childOption() 方法
给每条 child channel 连接设置一些 TCP 底层相关的属性, 比如上面, 我们设置了两种 TCP 属性, 其中 ChannelOption.SO_KEEPALIVE 表示是否开启 TCP 底层心跳机制, true 为开
option() 方法
对于 server Bootstrap 而言, 这个方法, 是给 parent channel 连接设置一些 TCP 底层相关的属性.
TCP 连接的参数详细介绍如下.
option 设置的参数:
SO_RCVBUF ,SO_SNDBUF
这两个选项就是来设置 TCP 连接的两个 buffer 尺寸的.
每个 TCP socket 在内核中都有一个发送缓冲区和一个接收缓冲区, TCP 的全双工的工作模式以及 TCP 的滑动窗口便是依赖于这两个独立的 buffer 以及此 buffer 的填充状态.
SO_SNDBUF
Socket 参数, TCP 数据发送缓冲区大小. 该缓冲区即 TCP 发送滑动窗口, Linux 操作系统可使用命令: cat /proc/sys.NET/ipv4/tcp_smem 查询其大小.
TCP_NODELAY
TCP 参数, 立即发送数据, 默认值为 Ture(Netty 默认为 True 而操作系统默认为 False). 该值设置 Nagle 算法的启用, 改算法将小的碎片数据连接成更大的报文来最小化所发送的报文的数量, 如果需要发送一些较小的报文, 则需要禁用该算法. Netty 默认禁用该算法, 从而最小化报文传输延时.
这个参数, 与是否开启 Nagle 算法是反着来的, true 表示关闭, false 表示开启. 通俗地说, 如果要求高实时性, 有数据发送时就马上发送, 就关闭, 如果需要减少发送次数减少网络交互, 就开启.
SO_KEEPALIVE
底层 TCP 协议的心跳机制. Socket 参数, 连接保活, 默认值为 False. 启用该功能时, TCP 会主动探测空闲连接的有效性. 可以将此功能视为 TCP 的心跳机制, 需要注意的是: 默认的心跳间隔是 7200s 即 2 小时. Netty 默认关闭该功能.
SO_REUSEADDR
Socket 参数, 地址复用, 默认值 False. 有四种情况可以使用:
(1). 当有一个有相同本地地址和端口的 socket1 处于 TIME_WAIT 状态时, 而你希望启动的程序的 socket2 要占用该地址和端口, 比如重启服务且保持先前端口.
(2). 有多块网卡或用 IP Alias 技术的机器在同一端口启动多个进程, 但每个进程绑定的本地 IP 地址不能相同.
(3). 单个进程绑定相同的端口到多个 socket 上, 但每个 socket 绑定的 ip 地址不同.(4). 完全相同的地址和端口的重复绑定. 但这只用于 UDP 的多播, 不用于 TCP.
SO_LINGER
Socket 参数, 关闭 Socket 的延迟时间, 默认值为 - 1, 表示禁用该功能.-1 表示 socket.close() 方法立即返回, 但 OS 底层会将发送缓冲区全部发送到对端. 0 表示 socket.close() 方法立即返回, OS 放弃发送缓冲区的数据直接向对端发送 RST 包, 对端收到复位错误. 非 0 整数值表示调用 socket.close() 方法的线程被阻塞直到延迟时间到或发送缓冲区中的数据发送完毕, 若超时, 则对端会收到复位错误.
SO_BACKLOG
Socket 参数, 服务端接受连接的队列长度, 如果队列已满, 客户端连接将被拒绝. 默认值, Windows 为 200, 其他为 128.
b.option(ChannelOption.SO_BACKLOG, 1024)
表示系统用于临时存放已完成三次握手的请求的队列的最大长度, 如果连接建立频繁, 服务器处理创建新连接较慢, 可以适当调大这个参数.
SO_BROADCAST
Socket 参数, 设置广播模式.
5: 装配流水线
ChannelPipeline 这是 Netty 处理请求的责任链, 这是一个 ChannelHandler 的链表, 而 ChannelHandler 就是用来处理网络请求的内容的.
每一个 channel , 都有一个处理器流水线.
装配 child channel 流水线, 调用 childHandler() 方法, 传递一个 ChannelInitializer 的实例.
在 child channel 创建成功, 开始通道初始化的时候, 在 Bootstrap 启动器中配置的 ChannelInitializer 实例就会被调用.
这个时候, 才真正的执行去执行 initChannel 初始化方法, 开始通道流水线装配.
流水线装配, 主要是在流水线 pipeline 的后面, 增加负责数据读写, 处理业务逻辑的 handler.
- b.childHandler(new ChannelInitializer<SocketChannel>()
- {
- // 有连接到达时会创建一个 channel
- protected void initChannel(SocketChannel ch) throws Exception
- {
- ch.pipeline().addLast(new ProtobufDecoder());
- ch.pipeline().addLast(new ProtobufEncoder());
- // pipeline 管理 channel 中的 Handler
- // 在 channel 队列中添加一个 handler 来处理业务
- ch.pipeline().addLast("serverHandler", serverHandler);
- }
- });
说明一下, ChannelInitializer 这个类中, 有一个泛型参数 SocketChannel, 这里的类型, 需要和前面的 Channel 类型对应上.
顺便说一下处理器.
处理器 ChannelHandler 用来处理网络请求内容, 有 ChannelInboundHandler 和 ChannelOutboundHandler 两种, ChannlPipeline 会从头到尾顺序调用 ChannelInboundHandler 处理网络请求内容, 从尾到头调用 ChannelOutboundHandler 处理网络请求内容.
pipeline 流水线的图, 大致如下:
如何装配 parent 通道呢?
使用 serverBootstrap.handler() 方法 . handler() 方法, 可以和前面分析的 childHandler() 方法对应起来. childHandler() 用于指定处理新连接数据的读写处理逻辑. handler() 方法装配 parent 通道.
比方说:
- serverBootstrap.handler(new ChannelInitializer()
- {
- protected void initChannel(NioServerSocketChannel ch)
- {
- System.out.println("服务端启动中");
- }
- }
- )
handler() 用于指定在服务端启动过程中的一些逻辑, 通常情况下呢, 我们用不着这个方法.
6: 开始绑定 server
- // 通过调用 sync 同步方法阻塞直到绑定成功
- ChannelFuture channelFuture = b.bind().sync();
- LOGGER.info(ChatServer.class.getName() +
- "started and listen on" +
- channelFuture.channel().localAddress());
这个也很简单.
7: ChannelFuture
ChannelFuture 在 Netty 中的所有的 I/O 操作都是异步执行的, 这就意味着任何一个 I/O 操作会立刻返回, 不保证在调用结束的时候操作会执行完成. 因此, 会返回一个 ChannelFuture 的实例, 通过这个实例可以获取当前 I/O 操作的状态.
- // 7 监听通道关闭事件
- // 应用程序会一直等待, 直到 channel 关闭
- ChannelFuture closeFuture= channelFuture.channel().closeFuture();
- closeFuture.sync();
对于客户端来说, Bootstrap 是开发 netty 客户端的基础, 通过 Bootstrap 的 connect 方法来连接服务器端. 该方法返回的也是 ChannelFuture.
8 优雅关闭 EventLoopGroup
- // 8 优雅关闭 EventLoopGroup,
- // 释放掉所有资源包括创建的线程
- workerLoopGroup.shutdownGracefully();
- bossLoopGroup.shutdownGracefully();
这个, 会关闭所有的 child channel, 这是非常重要的.
关闭之后, 会释放掉底层的资源, 如 TCP Socket 文件描述符, 等等.
来源: https://www.cnblogs.com/crazymakercircle/p/9998643.html