按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
Java nio 2.0的主要改进就是引入了异步IO(包括文件和网络),这里主要介绍下异步网络IO API的使用以及框架的设计,以TCP服务端为例。首先看下为了支持AIO引入的新的类和接口:
java.nio.channels.AsynchronousChannel
标记一个channel支持异步IO操作。
java.nio.channels.AsynchronousServerSocketChannel
ServerSocket的aio版本,创建TCP服务端,绑定地址,监听端口等。
java.nio.channels.AsynchronousSocketChannel
面向流的异步socket channel,表示一个连接。
java.nio.channels.AsynchronousChannelGroup
异步channel的分组管理,目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池,这个线程池执行两个任务:处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个 AsynchronousChannelGroup,那么通过AsynchronousServerSocketChannel创建的 AsynchronousSocketChannel将同属于一个组,共享资源。
java.nio.channels.CompletionHandler
异步IO操作结果的回调接口,用于定义在IO操作完成后所作的回调工作。AIO的API允许两种方式来处理异步操作的结果:返回的Future模式或者注册CompletionHandler,我更推荐用CompletionHandler的方式,这些handler的调用是由 AsynchronousChannelGroup的线程池派发的。显然,线程池的大小是性能的关键因素。AsynchronousChannelGroup允许绑定不同的线程池,通过三个静态方法来创建:
需要根据具体应用相应调整,从框架角度出发,需要暴露这样的配置选项给用户。
在介绍完了aio引入的TCP的主要接口和类之后,我们来设想下一个aio框架应该怎么设计。参考非阻塞nio框架的设计,一般都是采用Reactor模式,Reacot负责事件的注册、select、事件的派发;相应地,异步IO有个Proactor模式,Proactor负责 CompletionHandler的派发,查看一个典型的IO写操作的流程来看两者的区别:
Reactor: send(msg) -> 消息队列是否为空,如果为空 -> 向Reactor注册OP_WRITE,然后返回 -> Reactor select -> 触发Writable,通知用户线程去处理 ->先注销Writable(很多人遇到的cpu 100%的问题就在于没有注销),处理Writeable,如果没有完全写入,继续注册OP_WRITE。注意到,写入的工作还是用户线程在处理。
Proactor: send(msg) -> 消息队列是否为空,如果为空,发起read异步调用,并注册CompletionHandler,然后返回。 -> 操作系统负责将你的消息写入,并返回结果(写入的字节数)给Proactor -> Proactor派发CompletionHandler。可见,写入的工作是操作系统在处理,无需用户线程参与。事实上在aio的API 中,AsynchronousChannelGroup就扮演了Proactor的角色。
CompletionHandler有三个方法,分别对应于处理成功、失败、被取消(通过返回的Future)情况下的回调处理:
其中的泛型参数V表示IO调用的结果,而A是发起调用时传入的attchment。
在初步介绍完aio引入的类和接口后,我们看看一个典型的tcp服务端是怎么启动的,怎么接受连接并处理读和写,这里引用的代码都是yanf4j 的aio分支中的代码,可以从svn checkout,svn地址: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio
第一步,创建一个AsynchronousServerSocketChannel,创建之前先创建一个 AsynchronousChannelGroup,上文提到AsynchronousServerSocketChannel可以绑定一个 AsynchronousChannelGroup,那么通过这个AsynchronousServerSocketChannel建立的连接都将同属于一个AsynchronousChannelGroup并共享资源:
然后初始化一个AsynchronousServerSocketChannel,通过open方法:
通过nio 2.0引入的SocketOption类设置一些TCP选项:
绑定本地地址:
其中的100用于指定等待连接的队列大小(backlog)。完了吗?还没有,最重要的监听工作还没开始,监听端口是为了等待连接上来以便accept产生一个AsynchronousSocketChannel来表示一个新建立的连接,因此需要发起一个accept调用,调用是异步的,操作系统将在连接建立后,将最后的结果——AsynchronousSocketChannel返回给你:
注意,重复的accept调用将会抛出PendingAcceptException,后文提到的read和write也是如此。accept方法的第一个参数是你想传给CompletionHandler的attchment,第二个参数就是注册的用于回调的CompletionHandler,最后返回结果Future<AsynchronousSocketChannel>。你可以对future做处理,这里采用更推荐的方式就是注册一个CompletionHandler。那么accept的CompletionHandler中做些什么工作呢?显然一个赤裸裸的 AsynchronousSocketChannel是不够的,我们需要将它封装成session,一个session表示一个连接(mina里就叫 IoSession了),里面带了一个缓冲的消息队列以及一些其他资源等。在连接建立后,除非你的服务器只准备接受一个连接,不然你需要在后面继续调用pendingAccept来发起另一个accept请求:
注意到了吧,我们在failed和completed方法中在最后都调用了pendingAccept来继续发起accept调用,等待新的连接上来。有的同学可能要说了,这样搞是不是递归调用,会不会堆栈溢出?实际上不会,因为发起accept调用的线程与CompletionHandler回调的线程并非同一个,不是一个上下文中,两者之间没有耦合关系。要注意到,CompletionHandler的回调共用的是 AsynchronousChannelGroup绑定的线程池,因此千万别在CompletionHandler回调方法中调用阻塞或者长时间的操作,例如sleep,回调方法最好能支持超时,防止线程池耗尽。
连接建立后,怎么读和写呢?回忆下在nonblocking nio框架中,连接建立后的第一件事是干什么?注册OP_READ事件等待socket可读。异步IO也同样如此,连接建立后马上发起一个异步read调用,等待socket可读,这个是Session.start方法中所做的事情:
AsynchronousSocketChannel的read调用与AsynchronousServerSocketChannel的accept调用类似,同样是非阻塞的,返回结果也是一个Future,但是写的结果是整数,表示写入了多少字节,因此read调用返回的是 Future<Integer>,方法的第一个参数是读的缓冲区,操作系统将IO读到数据拷贝到这个缓冲区,第二个参数是传递给 CompletionHandler的attchment,第三个参数就是注册的用于回调的CompletionHandler。这里保存了read的结果Future,这是为了在关闭连接的时候能够主动取消调用,accept也是如此。现在可以看看read的CompletionHandler的实现:
如果IO读失败,会返回失败产生的异常,这种情况下我们就主动关闭连接,通过session.close()方法,这个方法干了两件事情:关闭channel和取消read调用:
在读成功的情况下,我们还需要判断结果result是否小于0,如果小于0就表示对端关闭了,这种情况下我们也主动关闭连接并返回。如果读到一定字节,也就是result大于0的情况下,我们就尝试从读缓冲区中decode出消息,并派发给业务处理器的回调方法,最终通过pendingRead继续发起read调用等待socket的下一次可读。可见,我们并不需要自己去调用channel来进行IO读,而是操作系统帮你直接读到了缓冲区,然后给你一个结果表示读入了多少字节,你处理这个结果即可。而nonblocking IO框架中,是reactor通知用户线程socket可读了,然后用户线程自己去调用read进行实际读操作。这里还有个需要注意的地方,就是decode出来的消息的派发给业务处理器工作最好交给一个线程池来处理,避免阻塞group绑定的线程池。
IO写的操作与此类似,不过通常写的话我们会在session中关联一个缓冲队列来处理,没有完全写入或者等待写入的消息都存放在队列中,队列为空的情况下发起write调用:
write调用返回的结果与read一样是一个Future<Integer>,而write的CompletionHandler处理的核心逻辑大概是这样:
compete方法中的result就是实际写入的字节数,然后我们判断消息的缓冲区是否还有剩余,如果没有就将消息从队列中移除,如果队列中还有消息,那么继续发起write调用。
重复一下,这里引用的代码都是yanf4j aio分支中的源码,感兴趣的朋友可以直接check out出来看看:http://yanf4j.googlecode.com/svn/branches/yanf4j-aio。
在引入了aio之后,java对于网络层的支持已经非常完善,该有的都有了,java也已经成为服务器开发的首选语言之一。java的弱项在于对内存的管理上,由于这一切都交给了GC,因此在高性能的网络服务器上还是Cpp的天下。java这种单一堆模型比之erlang的进程内堆模型还是有差距,很难做到高效的垃圾回收和细粒度的内存管理。
这里仅仅是介绍了aio开发的核心流程,对于一个网络框架来说,还需要考虑超时的处理、缓冲buffer的处理、业务层和网络层的切分、可扩展性、性能的可调性以及一定的通用性要求。
文章浏览阅读2.3k次。火山图是散点图的一种,它将统计测试中的统计显著性量度(如p value)和变化幅度相结合,从而能够帮助快速直观地识别那些变化幅度较大且具有统计学意义的数据点(基因等)。常应用于转录组研究,也能应用于基因组,蛋白质组,代谢组等统计数据。所以关注火山图(其它类型图也是),先理解每个点是什么(点代表基因、样品、通路或其它的,这个认识可以来自于常识,更准确的是看作者的描述),然后看横轴代表什么、纵轴代表..._volcano plot
文章浏览阅读615次。区块链作为一种崭新的、颠覆性的技术,是国内外活跃的研究领域和毕业设计选题方向。 本文列出最新的一组区块链方面的论文,希望可以对选择区块链毕业设计的同学们有所帮助, 这是汇智网编辑整理的区块链毕业设计论文系列中的第11篇。区块链相关链接: - 以太坊智能合约与Dapp入门教程 - 以太坊去中心化电商Dapp实战教程 - 以太坊ERC721数字资产实战教程 - 比特币数据分析ETL工具 - Fabri..._区块链大作业及源代码
文章浏览阅读3.4k次,点赞3次,收藏27次。HTML、CSS精美按钮输入框图形样式_好看的输入框样式
文章浏览阅读2k次,点赞2次,收藏4次。回答问题可以的。变量修改的本质是存储空间中的数据被改变,我们通过变量名、指针等方式改变数据都是找到数据的存储位置,对存储空间进行操作。换句话说,空间的存储位置决定了一个变量能不能被修改。局部变量存放在栈区,程序运行完毕释放内存。而加了const修饰之后,,我们不可以使用变量名对这块内存进行修改,却可以通过其他方式对修改内存从而达到对变量的修改。全局变量存放在数据段,加const修饰之后,存放在只读数据段,是无法对内存进行操作的。所以一个数据能不能被修改,还是看它存放的位置。变量名和变量我们首_c++ static const mutable
文章浏览阅读2.8k次。在给mapbox地图添加比例尺的时候,我没有找到mapbox自带的比例尺,所以就自己写了一个。和其他自定义比例尺原理其实都差不多。首先,加载mapbox地图,这个就不再详细叙述,照着mapbox官网的教程打下来就行了。定义一个自定义View,MapScaleView,在这里计算每一级比例尺在屏幕上需要绘制的长度。计算比例尺的原理是获取屏幕中心的一点的每像素代表的实际距离,因为在同一纬度上每..._android mapbox 地图比例尺
文章浏览阅读656次。在Android中,像素(px)、分辨率、密度(dpi)和尺寸之間存在著密切的關係。**像素(px)**是屏幕上顯示圖像的最小單位。每個像素都可以顯示一種顏色,多個像素組合在一起可以顯示複雜的圖像。是指屏幕上像素的數量。它通常表示為寬度和高度的乘積,例如1920x1080。分辨率越高,屏幕上可以顯示的細節就越多。**密度(dpi)**是指屏幕上每英寸的像素數量。它用於衡量屏幕的清晰度。密度越高,同樣大小的圖像就會顯得更清晰。是指屏幕的物理大小。它通常以英寸為單位表示。_安卓手机屏幕倍数
文章浏览阅读1.5k次。Start Emacs是一个功能相对较多而且很复杂的texteditor,所以想要熟练使用必须要对emacs的一些基本概念进行了解,了解这些概念最好的教程就是emacs自带的 “Emacs _emacs c++代码浏览插件
文章浏览阅读845次,点赞16次,收藏18次。登录注册功能基于Shiro实现RBAC模型的权限控制系统, 分为普通用户, 社团管理员, 系统管理员三个角色实现会员的增加, 删除, 修改, 搜索查询功能实现物品的信息管理功能支持对活动信息进行维护, 实现增删改查的功能能够对社团信息进行维护管理。
文章浏览阅读55次。在主循环中,通过调用 sm.tx_fifo(data) 方法将单个字节的数据写入状态机的发送 FIFO 中。这是一个利用状态机的 rp2.StateMachine.put() 方法来向 TX FIFO 中写入数据,以及 rp2.StateMachine.get() 方法来从 RX FIFO 中读取数据的例子。当需要在程序中控制状态机的数据写入速度或频率时,例如在使用状态机模拟高速或高频的 I/O 行为时,根据 TX FIFO 中的字数来决定何时调用 StateMachine.put() 方法。
文章浏览阅读494次。教材《数据结构与算法》排序,多么经典的话题。计算机排序无法像人一样看到全部,只能对最近的两个单位进行比较,所以,计算机排序只能是俩俩比较。冒泡排序选择排序插入排序所有的排序都在循环进行两步: 比较两个数据项交换两个数据项,或者复制其中一个。冒泡排序: 就是按照一个一个比较的方式,对相邻的两个数据进行比对,并同时进行交换位置;。即 for( ){ for(){ } }遍历,这是最简..._使用简单数组实现下面各种排序算法(任选3个):
文章浏览阅读632次。扩展配置自定义分词在elasticsearch-7.6.2/plugins/ik/config编写dic(qpdiy.dic)可能遇到的问题:乱码全局的情况下:即所有用户都能用这个配置文件地址:/etc/vimrc在文件中添加:set fileencodings=utf-8,ucs8bom,gb18030,gbk,gb2312,cp936set termencoding=utf-8set encoding=utf-8进入配置文件IKAnalyzer.cfg.xml<?xml ve_分词器扩展没有生效
文章浏览阅读66次。Jquery学习笔记jquery简介:(1)jquery是干什么的呢?l为了简化JavaScript的开发,一些JavsScript库诞生了.JavaScript库封装了很多预定义的对象和实用函数。能帮助使用者建立有高难度交互的Web2.0特性的富客户端页面,并且兼容各大浏览器l当前流行的JavaScript库有...