如何实现高性能的异步网络传输?
- **异步与同步模型最大的区别是,同步模型会阻塞线程等待资源,而异步模型不会阻塞线程,它是等资源准备好后,再通知业务代码来完成后续的资源处理逻辑。**这种异步设计的方法,可以很好地解决 IO 等待的问题。
- 我们开发的绝大多数业务系统,都是 IO 密集型系统。
- 跟 IO 密集型系统相对的另一种系统叫计算密集型系统。
- IO 密集型系统大部分时间都在执行 IO 操作,这个 IO 操作主要包括网络 IO 和磁盘 IO,以及与计算机连接的一些外围设备的访问。
- 与之相对的计算密集型系统,大部分时间都是在使用 CPU 执行计算操作。
- 我们开发的业务系统,很少有非常耗时的计算,更多的是网络收发数据,读写磁盘和数据库这些 IO 操作。
- 这样的系统基本上都是 IO 密集型系统,特别适合使用异步的设计来提升系统性能。
- 应用程序最常使用的 IO 资源,主要包括磁盘 IO 和网络 IO。
- 由于现在的 SSD 的速度越来越快,对于本地磁盘的读写,异步的意义越来越小。
- 所以,使用异步设计的方法来提升 IO 性能,我们更加需要关注的问题是,如何来实现高性能的异步网络传输。
理想的异步网络框架 Netty
- 大部分语言提供的网络通信基础类库都是同步的。
- 一个 TCP 连接建立后,用户代码会获得一个用于收发数据的通道,每个通道会在内存中开辟两片区域用于收发数据的缓存。
- 发送数据的过程比较简单,我们直接往这个通道里面写入数据就可以了。
- 用户代码在发送时写入的数据会暂存在缓存中,然后操作系统会通过网卡,把发送缓存中的数据传输到对端的服务器上。
- 只要这个缓存不满,或者说,我们发送数据的速度没有超过网卡传输速度的上限,那这个发送数据的操作耗时,只不过是一次内存写入的时间,这个时间是非常快的。
- 所以,发送数据的时候同步发送就可以了,没有必要异步。
- 对于数据的接收方来说,它并不知道什么时候会收到数据。
- 那我们能直接想到的方法就是,用一个线程阻塞在那⼉等着数据,当有数据到来的时候,操作系统会先把数据写⼊接收缓存,然后给接收数据的线程发一个通知,线程收到通知后结束等待,开始读取数据。
- 处理完这一批数据后,继续阻塞等待下一批数据到来,这样周而复始地处理收到的数据。
- 同步网络 IO 的模型(BIO)
- 同步网络 IO 模型在处理少量连接的时候,是没有问题的。
- 但是如果要同时处理非常多的连接,同步的网络 IO 模型就有点力不从心了。
- 因为,每个连接都需要阻塞一个线程来等待数据,大量的连接数就会需要相同数量的数据接收线程。
- 当这些 TCP 连接都在进行数据收发的时候,会有大量的线程来抢占 CPU 时间,造成频繁的 CPU 上下文切换,导致 CPU 的负载升高,整个系统的性能就会比较慢。
- 对于业务开发者来说,一个好的异步网络框架,它的 API 应该是什么样的呢?
- 我们希望达到的效果,无非就是,只用少量的线程就能处理大量的连接,有数据到来的时候能第一时间处理就可以了。
- 对于开发者来说,最简单的方式就是,事先定义好收到数据后的处理逻辑,把这个处理逻辑作为一个回调方法,在连接建立前就通过框架提供的 API 设置好。
- 当收到数据的时候,由框架自动来执行这个回调方法就好了。
- 接下来我们看一下如何使用 Netty 实现异步接收数据。
// 创建⼀组线性 EventLoopGroup group = new NioEventLoopGroup(); try{ // 初始化 Server ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(group); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.localAddress(new InetSocketAddress("localhost", 9999)); // 设置收到数据后的处理的 Handler serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel socketChannel) throws Exception socketChannel.pipeline().addLast(new MyHandler()); } }); // 绑定端⼝,开始提供服务 ChannelFuture channelFuture = serverBootstrap.bind().sync(); channelFuture.channel().closeFuture().sync(); } catch(Exception e){ e.printStackTrace(); } finally { group.shutdownGracefully().sync(); }
- 首先我们创建了一个 EventLoopGroup 对象,命名为 group,这个 group 对象你可以简单把它理解为一组线程。这组线程的作用就是来执行收发数据的业务逻辑。
- 然后,使用 Netty 提供的 ServerBootstrap 来初始化⼀个 Socket Server,绑定到本地 9999 端口上。
- 在真正启动服务之前,我们给 serverBootstrap 传入了一个 MyHandler 对象,这个 MyHandler 是我们自己来实现的一个类,它需要继承 Netty 提供的一个抽象类:ChannelInboundHandlerAdapter,在这个 MyHandler 里面,我们可以定义收到数据后的处理逻辑。这个设置 Handler 的过程,就是预先来定义回调方法的过程。
- 最后就可以真正绑定本地端口,启动 Socket 服务了。
- 真正需要业务代码来实现的就两个部分:
- 一个是把服务初始化并启动起来。
- 还有就是,实现收发消息的业务逻辑 MyHandler。
- 像线程控制、缓存管理、连接管理这些异步网络 IO 中通用的、比较复杂的问题,Netty 已经自动帮你处理好了。
使用 NIO 来实现异步网络通信
- 在 Java 的 NIO 中,它提供了一个 Selector 对象,来解决一个线程在多个网络连接上的多路复用问题。
-
在 NIO 中,每个已经建立好的连接用一个 Channel 对象来表示。
-
我们希望能实现,在一个线程里,接收来自多个 Channel 的数据。
- Selecor 通过一种类似于事件的机制来解决这个问题。
- 首先你需要把你的连接,也就是 Channel 绑定到 Selector 上,然后你可以在接收数据的线程来调用 Selector.select() 方法来等待数据到来。
- 这个 select 方法是一个阻塞方法,这个线程会一直卡在这儿,直到这些 Channel 中的任意一个有数据到来,就会结束等待返回数据。
- 它的返回值是一个迭代器,你可以从这个迭代器里面获取所有 Channel 收到的数据,然后来执行你的数据接收的业务逻辑。
-