10 | 如何实现高性能的异步网络传输

异步的线程模型,与同步模型的最大区别是,同步模型会阻塞线程等待资源,而异步模型不会阻塞线程,它是等资源准备好后,再通知业务代码来完成后续的资源处理逻辑。这种异步设计的方法,可以很好解决IO等待问题。

我们开发的绝大多数业务系统都是IO密集型系统。跟IO密集型系统相对的另一种系统是计算密集型系统。

IO密集型系统大部分时间都在执行IO操作,这个IO操作主要包括网络IO和磁盘IO,以及与计算机连接的一些外围设备的访问。与之相对的计算密集型系统,大部分时间都是在使用CPU执行计算操作。我们开发的业务系统,很少有非常耗时的计算,更多的是网络收发数据,读写磁盘和数据库这些IO操作。这样的系统基本上都是IIO密集型系统,特别适合使用异步设计来提升系统性能。

应用程序最常使用的IO资源就是磁盘IO和网络IO。由于现在的SSD的速度越来越快,对于本地磁盘的读写,异步的意义越来越小。所以,使用异步设计的方法来提升IO性能,我们更加需要关注的问题是,如何来实现高性能的异步网络传输。

理想的异步网络框架是什么样的?

在我们开发的程序中,若要实现通过网络来传输数据,需要用到开发语言提供的网络通信类库。大部分语言提供的网络通信基础类库都是同步的。一个TCP连接建立后,用户代码会获得一个用于收发数据的通道,每个通道会在内存中开辟两片区域用于收发数据的缓存

发送数据的过程比较简单,我们直接往这个通道里面写入数据就可以了。用户代码在发送时写入的数据会暂存在缓存中,然后操作系统会通过网卡,把发送缓存中的数据传输到对端的服务器上。

只要缓存不满,或者我们发送数据的速度没超过网卡传输速度上线,那这个发送数据操作的耗时,只不过是一次内存写入的时间,这个时间是非常快的。所以,发送数据的时候同步发送就可以了,没有必要异步。

比较麻烦的是接收数据。对于数据的接收方来说,它并不知道什么时候会收到数据。那我们能直接想到的方法就是,用一个线程阻塞等数据,当有数据到来,OS先把数据写入接收缓存,然后给接收数据的线程发一个通知,线程收到通知后结束等待,开始读取数据。处理完这一批数据后,继续阻塞等待下一批数据到来,这样周而复始地处理收到的数据。

接收线程  <-———— 收缓存  <—————— 通道

发送线程  ————--> 发缓存 <—————— 通道

接收线程 <————  收缓存 <—————— 通道

发送线程  ————> 发缓存 ——————> 通道 

这就是同步网络IO的模型。同步网络IO模型在处理少量连接的时候,没有问题。但如果要同时处理非常多的连接,力不从心。

每个连接都要阻塞一个线程等数据,大量连接需要相同数量的收数据线程。当这些TCP连接都在收发时,会有大量线程抢CPU时间,造成频繁CPU上下文切换,导致CPU的负载升高,整个系统的性能会比较慢。

所以,我们需要使用异步模型来解决网络IO,怎么解决呢?

你可先抛开你知道的各种语言的异步类库和各种异步的网络IO框架,想想对业务开发者来说,一个好的异步网络框架,它的API该是如何。

我们希望达到的效果,无非就是,少量线程处理大量连接,有数据到来时,能第一时间处理

 对于开发者来说,最简单的处理方式是,事先定义好收到数据后的处理逻辑,把这个处理逻辑作为一个回调方法,在连接建立前就通过框架提供的API设置好。数据到来,框架自动执行回调方法。

实际上,有无对应的框架?

使用Netty来实现异步网络通信

在java中,大名鼎鼎的Netty框架的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));

    // 设置接收处理函数
    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()
}

上述代码的功能十分简单,就是在本地9999端口,启动了一个socket server接收数据。

1. 首先创建了一个EventLoopGroup的对象,命名为 group,这个 group 对象可以简单理解为一组线程,这组线程就是用来收发数据的。

2. 然后使用 Netty 提供的 ServerBootstrap 来初始化一个 Socket Server,绑定到本地 9999 端口上。

3. 在真正启动服务前,给serverBootstrap传入一个MyHandler对象,这个对象是自己实现的Netty提供的一个抽象类,在这个MyHandler里定义接收数据后的处理逻辑。这个设置Handler就是前述预定义回调的过程。

4. 最后真正绑定本地端口,启动socket服务。

服务启动,若有客户端链接,Netty会自动接收并创建一个Socket连接。你可以看到,我们代码中没有像一些同步网络框架中那样,要用户调用 Accept() 方法来接收创建连接的情况,在Netty中,该过程是自动的。

当收到来自客户端的数据后,Netty会在我们第一行提供的 EventLoopGroup 对象中,获取一个IO线程,在此IO线程中调用接收数据的回调方法,执行接收数据的业务逻辑。

Netty本身是全异步的设计,上节课提到异步设计会带来复杂度,但其实Netty提供了非常有好的API。

真正实现业务代码只需启动初始化的服务,实现收发消息的业务Handler。线程控制,缓存管理,连接管理这些异步网络IO中通用问题,Netty已处理完成。

在这种设计中,Netty自己维护一组线程来执行数据收发的业务逻辑。若需要更灵活的实现,自己维护收发数据的线程,可使用更底层的Java NIO,Netty自身是基于NIO实现的。

使用NIO来实现异步网络通信

在java的NIO中,它提供了一个Selector对象,解决一个线程在多个网络连接上的多路复用问题。在NIO中,每个已经建立好的连接用Channel表示。在一个线程里,接收来自多个channel的数据,即任何时候channel来数据,都能即时被同一个线程处理到。

我们可以想到,一个线程对应多channel,有可能出现两种情况:

1. 线程在忙着处理收到的数据,此时channel又收到了新数据。

2. 线程闲着没事儿干,所有channel中都没收到数据,不能确定哪个channel何时来数据。

 Selector 通过一种类似事件的机制来解决该问题。首先要把连接,即channel绑定到Selector,而后通过接收线程调Selector.select()等数据。这个select方法是一个阻塞方法,线程会一直卡在这儿,直到这些channel中任意一个有数据,就会结束等待返回数据。返回值是迭代器,可获取所有channel收到的数据,执行业务逻辑。

你可以选择直接在线程里来执行接收数据的业务逻辑,也可以将任务分发个其他线程执行。可由业务代码进行控制。

小结

传统的同步网络IO,一般采用一个线程对应一个channel接收数据,很难支持高并发和高吞吐量。此时,我们需要使用异步的网络IO框架解决问题。

然后讲了Netty和NIo两种异步网络框架的API和使用方法。这里需要体会二者API的设计差异。Netty自动解决了线程控制,缓存管理,连接管理这些问题,用户只需实现对应的Handler来处理收到的数据即可。

而NIO是更底层的API,提供了Selector机制,用单个线程同时管理多个连接,解决了多路复用这个异步网络通信的核心问题。

思考题

刚刚我们提到过,Netty本身是基于NIO的API来实现的。课后可以思考,针对接收数据这个流程,Netty是如何用NIO来实现的呢?

关于Java的网络:

例子:有一个养鸡的农场,里面养着来自各个农户(Thread)的鸡(Socket),每家农户在农场中建立了鸡舍(SocketChannel)。

1、BIO:Block IO,每个农户盯着自己的鸡舍,一旦有鸡下蛋,就去做捡蛋。

2、NIO:No-Block IO,单Selector,农户们花钱请了一个饲养员(Selector),并告诉饲养员(register)如果哪家鸡有任何情况(下蛋)均要向这家农户报告(select keys)

3、NIO:No-Block IO,多Selector,当农场中的鸡舍逐渐增多时,一个饲养员巡视一次耗时不断增长,这样农户收到通知延时增长。可以通过多请几个饲养员解决(多Selector),每个饲养员分配管理鸡舍。

4、Epoll模式:农场问题该如何改进。其实就是饲养员不需要再巡视鸡舍,而是鸡打鸣(活跃socket),就知道哪家鸡下蛋了。

5、AIO:Asynchronous I/O,不同于NIO,AIO模式取蛋由饲养员自己负责,然后取完后,直接通知农户来拿,不需要农户自己去鸡舍取蛋。

网络模型参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值