《跟闪电侠学Netty》阅读笔记 - 开篇入门Netty(一)https://developer.aliyun.com/article/1395284
简单介绍AIO
JDK的AIO不是很成熟,AIO底层依然因为Epoll的遗留问题存在臭名昭著的空轮询BUG,这里并不推荐读者使用JDK的AIO进行编程。
Java AIO 的核心在于两个关键类:AsynchronousSocketChannel 和 AsynchronousServerSocketChannel。
AsynchronousSocketChannel 实现异步套接字通信,可以让我们在不同的客户端连接之间切换,而无需创建新的线程或线程池。
AsynchronousServerSocketChannel 则用于异步地监听客户端的连接请求。
Java 实现代码
这里用ChatGPT生成了一段JDK的AIO代码,为了更好理解顺带让它把注释一块生成了。
public class AIOServer { public static void main(String[] args) throws IOException { // 创建一个 ExecutorService,用于处理异步操作的线程池 ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个 AsynchronousChannelGroup,将线程池与该 Channel 组关联 AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(executor); // 创建 AsynchronousServerSocketChannel,并绑定到指定地址和端口 final AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup); InetSocketAddress address = new InetSocketAddress("localhost", 12345); serverSocketChannel.bind(address); System.out.println("Server started on port " + address.getPort()); // 调用 accept 方法接收客户端连接,同时传入一个 CompletionHandler 处理连接结果 serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { // 当连接成功时会调用 completed 方法,传入客户端的 SocketChannel 实例作为参数 @Override public void completed(AsynchronousSocketChannel clientSocketChannel, Object attachment) { // 继续接受下一个客户端连接,并处理当前客户端的请求 serverSocketChannel.accept(null, this); handleClient(clientSocketChannel); } // 当连接失败时会调用 failed 方法,传入异常信息作为参数 @Override public void failed(Throwable exc, Object attachment) { System.out.println("Error accepting connection: " + exc.getMessage()); } }); // 在主线程中等待,防止程序退出 while (true) { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { break; } } } private static void handleClient(AsynchronousSocketChannel clientSocketChannel) { ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取客户端发送的数据,同时传入一个 CompletionHandler 处理读取结果 clientSocketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() { // 当读取成功时会调用 completed 方法,传入读取到的字节数和附件对象(此处不需要) @Override public void completed(Integer bytesRead, Object attachment) { if (bytesRead > 0) { // 将 Buffer 翻转,以便进行读取操作 buffer.flip(); byte[] data = new byte[bytesRead]; buffer.get(data, 0, bytesRead); String message = new String(data); System.out.println("Received message: " + message); // 向客户端发送数据 clientSocketChannel.write(ByteBuffer.wrap(("Hello, " + message).getBytes())); buffer.clear(); // 继续读取下一批数据,并传入当前的 CompletionHandler 以处理读取结果 clientSocketChannel.read(buffer, null, this); } else { try { // 当客户端关闭连接时,关闭该 SocketChannel clientSocketChannel.close(); } catch (IOException e) { System.out.println("Error closing client socket channel: " + e.getMessage()); } } } // 当读取失败时会调用 failed 方法,传入异常信息和附件对象(此处不需要) @Override public void failed(Throwable exc, Object attachment) { System.out.println("Error reading from client socket channel: " + exc.getMessage()); } }); } }
AIO 编程模型优缺点
优点:
并发性高、CPU利用率高、线程利用率高 。
缺点:
不适合轻量级数据传输,因为进程之间频繁的通信在追错、管理,资源消耗上不是很可观。
适用场景:
对并发有需求的重量级数据传输。
从上面的代码也可以看出,AIO的API和NIO又是截然不同的写法,为了不继续增加学习成本,这里点到为止,不再深入AIO编程模型的部分了,让我们继续回到Netty,了解Netty的编程模型。
使用Netty 带来的好处
- Netty不需要了解过多概念
- 底层IO模型随意切换
- 自带粘包拆包的问题处理
- 解决了空轮询问题
- 自带协议栈,支持通用协议切换
- 社区活跃,各种问题都有解决方案
- RPC、消息中间件实践,健壮性极强
网络IO通信框架过程
一个网络IO通信框架从客户端发出请求到接受到结果,基本包含了下面这8个操作:
- 解析指令
- 构建指令对象
- 编码
- 等待响应
- 解码
- 翻译指令对象
- 解析指令
- 执行
下面来看看Netty的编程模型。
Netty 启动模板代码(重要)
经过上面一长串的铺垫,现在来到整体Netty的代码部分:
服务端
首先是服务端代码:
public static void main(String[] args) { ServerBootstrap serverBootstrap = new ServerBootstrap(); NioEventLoopGroup boos = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup(); serverBootstrap .group(boos, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { protected void initChannel(NioSocketChannel ch) { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(msg); } }); } }) .bind(8000); }
初学Netty的时候可能没有NIO的经验,所以我们简单做个类比:
NioEventLoopGroup boos = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup();
可以直接看作
Selector serverSelector = Selector.open(); Selector clientSelector = Selector.open();
其中boss负责处理连接,worker负责读取请求和处理数据。两者的工作模式也是类似的,boss就像是老板负责“接单”,worker 打工仔负责接收单子的内容然后开始打工干活。
客户端
客户端的启动代码如下。
public static void main(String[] args) throws InterruptedException { Bootstrap bootstrap = new Bootstrap(); NioEventLoopGroup eventExecutors = new NioEventLoopGroup(); // 引导器引导启动 bootstrap.group(eventExecutors) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel channel) throws Exception { channel.pipeline().addLast(new StringEncoder()); } }); // 建立通道 Channel channel = bootstrap.connect("127.0.0.1", 8000).channel(); while (true){ channel.writeAndFlush(new Date() + " Hello world"); Thread.sleep(2000); } }
客户端的代码中的NioEventLoopGroup
实际对应了main函数单独开启的线程。上面的代码可以完美的替代调JDK的NIO、AIO、BIO 的API,学习成本大大降低,Netty为使用者做了大量的“准备”工作,提供了很多"开箱即用"的功能,非常方便。
Netty的服务端和客户端的入门程序代码是分析源码的开始,这部分代码需要有较深的印象。
问题
摘录部分Netty入门级别的八股。
Linux网络编程中的五种I/O模型
关键点:
不同的角度理解IO模型的概念会有变化。注意本部分站在用户程序和内核的网络IO交互的角度理解的。
权威:
- RFC标准
- 书籍 《UNIX Network Programming》(中文名《UNIX网络编程-卷一》)第六章。
下面部分总结自:《UNIX Network Programming》(中文名《UNIX网络编程-卷一》)
1)阻塞式I/O
注意原书中阻塞式I/O给出的例子是UDP而不是TCP的例子。recvfrom 函数可以看作是系统调用,在阻塞I/O模型中,recvfrom 的系统调用要等待内核把数据从内核态拷贝到用户的缓冲池或者发生错误的时候(比如信号中断)才进行返回。recvfrom 收到数据之后再执行数据处理。
2)非阻塞式I/O
recvfrom 的系统调用会在设置非阻塞的时候,会要求内核在无数据的时候返回错误,所以前面三次都是错误调用,在第四次调用之后此时recvfrom轮询到数据,于是开始正常的等待内核把数据复制到用户进程缓存。
此处轮询的定义为:对于描述符进行recvfrom循环调用,会增加CPU的开销。注意非阻塞的轮询不一定要比阻塞等待要强,有时候甚至会有无意义的开销反而不如阻塞。
3)I/O复用(select,poll,epoll...)
I/O多路复用是阻塞在select,epoll这样的系统调用,没有阻塞在真正的I/O系统调用如recvfrom。进程受阻于select,等待可能多个套接口中的任一个变为可读。
IO多路复用最大的区别是使用两个系统调用(select和recvfrom)。Blocking IO(BIO)只调用了一个系统调用(recvfrom)。
select/epoll 核心是可以同时处理多个 connection,但是这并不一定提升效率,连接数不高的话性能不一定比多线程+阻塞IO好。但是连接数比较庞大之后会有显著的差距。
多路复用模型中,每一个socket都需要设置为non-blocking,否则是无法进行elect的。
listenerChannel.configureBlocking(false);
这个设置的意义就在于此。
4)信号驱动式I/O(SIGIO)
信号驱动的优势是等待数据报到之前进程不被阻塞,主循环可以继续执行,等待信号到来即可,注意这里有可能是数据已经准备好被处理,或者数据复制完成可以准备读取。
信号驱动IO 也是同步模型,虽然可以通过信号的方式减少交互,但是系统调用过程当中依然需要进行等待,内核也依然是通知何时开启一个IO操作,和前面介绍的IO模型对比发现优势并不明显。
5)异步I/O(POSIX的aio_系列函数)
核心: Future-Listener机制
- IO操作分为两步
- 发起IO请求,等待数据准备(Waiting for the data to be ready)
- 实际的IO操作,将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
前四种IO模型都是同步IO操作,主要的区别在于第一阶段处理方式,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用或者select() 函数。 异步I/O模型内在这两个阶段都要(自行)处理。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。
异步IO模型非常像是我们日常点外卖,我们时不时看看配送进度就是在“轮询”,当外卖员把外卖送到指定位置打电话通知我们去拿即可。
交互几个核心点
再次强调是针对用户程序和内核的网络IO交互角度理解的。
- 阻塞非阻塞说的是线程的状态(重要)
- 同步和异步说的是消息的通知机制(重要) - 同步需要主动读写数据,异步是不需要主动读写数据 - 同步IO和异步IO是针对用户应用程序和内核的交互
为什么Netty使用NIO而不是AIO?
- Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化。
- Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来
- AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多
- Linux上AIO不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)。
结论
Netty整体架构是reactor模型,采用epoll机制,所以往深的说,还是IO多路复用模式,所以也可说netty是同步非阻塞模型(看的层次不一样),只不过实际是异步IO。
Netty 应用场景了解么?
- 作为 RPC 框架的网络通信工具。分布式系统之间的服务器通信可以使用Netty完成,虽然是Java编写的框架,但是性能非常接近 C 和C++ 执行效率。
- 作为 RPC 框架的网络通信工具,Netty本身就是
- 消息队列:比如大名鼎鼎的RocketMq底层完全依赖Netty,编程人员不需要很强的并发编程功底也可以快速上手和维护代码。
- 实现一个即时通讯系统:正好和本书应用场景重合了。
介绍Netty
简短介绍
Netty是一个高性能、异步、NIO编程模型的网络编程框架。它提供了简单易用的API,可以快速地开发各种网络应用程序,如客户端、服务器和协议实现等。同时,Netty还具有良好的可扩展性和灵活性,支持多种传输协议和编解码器。
稍微复杂一点
Netty是由JBOSS提供的一个java开源框架, 是业界最流行的NIO框架,整合了多种协议( 包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,精心设计的框架,在多个大型商业项目中得到充分验证。 1)API使用简单 2)成熟、稳定 3)社区活跃 有很多种NIO框架 如mina 4)经过大规模的验证(互联网、大数据、网络游戏、电信通信行业)。
总结
- 开篇简单介绍了JDK的BIO、NIO和AIO,三者不仅出现时间跨度大,三个团队编写,和JDK的IO编程一样晦涩难懂和不好用,开发人员需要花大量事件学习底层细节。
- 用洗衣机的例子,理解网络编程模型的重要概念:同步、非同步、阻塞、非阻塞。从入门的角度来看,同步和异步可以认为是否是由客户端主动获取数据,而阻塞和非阻塞则是客户端是否需要拿到结果进行处理,两者是相辅相成的。
- Netty 编程模型统一了JDK的编程模型,降低了学习成本,同时效率比原生JDK更高,并且解决了NIO 中的空轮询问题。
- Netty 底层实际上和JDK的网络编程模型密切相关,从案例代码可以看到Netty的客户端API代码可以直接往NIO的Server发送数据。
- 补充书中没有介绍的AIO编程模型,用ChatGPT 生成的代码简单易懂。
- 最后补充有关Netty的问题。
写在最后
开篇部分补充了书中没介绍的一些网络编程模型的基本概念,以及在最后关联了些相关书籍的知识点和,最后顺带归纳了一些八股问题,当然最为重要的部分是熟悉Netty的入门程序代码。
开篇入门篇到此就结束了,如果内容描述有误,欢迎评论或者私信留言。
参考
- 《跟闪电侠学Netty》开篇:Netty是什么? - 简书 (jianshu.com)
- 网课专栏:《高并发系列之百万连接Netty实战课程》
Netty 书籍推荐
- 《Netty权威指南》
- 《Netty进阶之路》