java nio非常难驾驭,就像我在上一篇文章中处理的文件服务器那样,也只是考虑并处理了部分情况,然而可能还是要出错,可扩展性也不好。
netty就是这样的一种框架,让Java nio变得:
- 网络服务器编程变得容易
- 可用性变高
- 扩展性好
netty的基本工作方式
那么,Netty究竟是怎么运行的? Netty使用多Reactor多线程模型。
这种模型是把Reactor线程拆分了mainReactor和subReactor两个部分,mainReactor只处理连接事件,读写事件交给subReactor来处理。mainRactor只处理连接事件,一个端口用一个线程来处理。处理读写事件的subReactor个数一般和操作系统核数相关,一个连接对应一个线程.业务逻辑由业务线程池处理。
本文会引用一个例子,先谈谈netty使用的基本数据结构,然后梳理清楚使用netty建立连接的过程。
从一个例子开始
maven包依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.72.Final</version>
</dependency>
复制代码
server
server可以认为和我在nio的实现里面的区别是:acceptor单独一个线程池,其他io事件或者任务一个线程池。然而我当时没有这么实现,只是给业务流程一个线程池。
netty server 的代码示意如下:
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup parentGroup = new NioEventLoopGroup();
//NettyRuntime.availableProcessors() * 2 线程数
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());//解码为字符串
pipeline.addLast(new StringEncoder());//编码为二进制
pipeline.addLast(new DemoSocketServerHandler());
}
});
ChannelFuture future = bootstrap.bind(8888).sync();
System.out.println("future.channel() = " + future.channel());
System.out.println("服务器已启动。。。");
future.channel().closeFuture().sync();
} finally {
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
复制代码
server bind之后会启动一个线程阻塞在select,等待着连接了。
client
netty client的编码模型简单很多,如下:
netty client 的代码示意如下:
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new DemoSocketClientHandler());
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
}
});
ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
复制代码
客户端也是一个连接池,每个连接一个线程,这里只使用一个有点浪费了,但是这里只是一个简单的demo,暂且这样处理吧。
demo总结
可以看到,总体编码简单易懂,但要明白具体的运行机制,却要费一番功夫。下面先介绍demo中用到的基本数据结构,然后再试图弄清楚netty用于连接的机理。
数据结构
如果已经比较了解这块的数据结构,可直接跳到流程部分。
Bootstrap
- ServerBootstrap是为netty服务器设置服务的,像上面这样通过ServerBootstrap就可以配置出一个完善的netty服务来
- Bootstrap是为netty客户端设置服务的,像上面这样通过Bootstrap就可以配置出一个完善的netty客户端
Channel
可以认为channel是一个连接,Channel聚合了一组功能,不但包括网络IO操作,还包括获取该Channel的eventloop、以及获取缓冲分配器allocator, 和pipeline等。所以channel是netty里面最重要的数据结构。
NioEventLoop
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为是一个线程池。
NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行I/O任务和非 I/O 任务:
- I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
- 非IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务以及一些定时任务,由 runAllTasks 方法触发。
两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。
异步的api
nio编程的时候讨论到的是非阻塞的api,非阻塞是不够方便的,往往要和循环放在一起操作,比如之前的文件服务器。
netty的设计却不同,主要需要使用到异步的api,这里谈到的异步的api其实是一种软件设计上的事情,引入这个,对于Java nio编码带来了极大的帮助。
下面先了解一下什么是异步的api
Future
Future可能大家已经非常熟知了,Future是JDK中的接口,当引入线程池的时候,Future也引入了,可以用来表示提交的任务的结果。 Future接口提供两个方法:
- get: 同步阻塞当前调用的线程,直到结果被设置
- cancel:取消异步操作,