内容来自:
http://blog.csdn.net/yinwenjie/article/details/48274255
http://blog.csdn.net/yinwenjie/article/details/48344989
http://blog.csdn.net/yinwenjie/article/details/48472237
http://blog.csdn.net/yinwenjie/article/details/48522403
http://blog.csdn.net/yinwenjie/article/details/48784375
系统间通信是架构师需要掌握的又一个关键技术领域,如果说理解和掌握负载均衡层技术需要您有一定的linux系统知识和操作系统知识的话,那么理解和掌握系统间通信层技术,需要您有一定的编程经验(最好是JAVA编程经验,因为我们会主要以JAVA技术作为实例演示)。
1、一个场景
首先我们来看一个显示场景:在现实生活中有两个人技术人员A和B,在进行一问一答形式的交流。如下图所示:
我们来看这幅图的中的几个要点:
- 他们两都使用中文进行交流。如果他们一人使用的是南斯拉夫语另一人使用的是索马里语,并且相互都不能理解对方的语系,很显然A所要表达的内容B是无法理解的。
- 他们的声音是在空气中进行传播的。空气除了支撑他们的呼吸外,还支撑了他们声音的传播。如果没有空气他们是无法知道对方用中文说了什么。
- 他们的交流方式是协调一致的,即A问完一个问题后,等待B进行回答。收到B的回答后,A才能问下一个问题。
- 由于都是人类,所以他们处理信息的方式也是一样的:用嘴说话,用耳朵听话,用大脑处理形成结果。
- 目前这个交流场景下,只有A和B两个人。但是随时有可能增加N个人进来。第N个人可能不是采用中文进行交流。
2、信息格式
很明显通过中文的交谈,两个人相互明白了对方的意图。为了保证信息传递的高效性,我们一定会将信息做成某种参与者都理解的格式。例如:中文有其特定的语法结构,例如主谓宾,定状补。
在计算机领域为了保证信息能够被处理,信息也会被做成特定的格式,而且要确保目标能够明白这种格式。常用的信息格式包括:
XML:
可扩展标记语言,这个语言由W3C(万维网联盟)进行发布和维护。XML语言应用之广泛,扩展之丰富。适合做网络通信的信息描述格式(一般是“应用层”协议了)。例如Google 定义的XMPP通信协议就是使用XML进行描述的;不过XML的更广泛使用场景是对系统环境进行描述(因为它会造成较多的不必要的内容传输),例如服务器的配置描述、Spring的配置描述、Maven仓库描述等等。
JSON:
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它和XML的设计思路是一致的:和语言无关(流行的语言都支持JSON格式描述:Go、Python、C、C++、C#、JAVA、Erlang、JavaScript等等);但是和XML不同,JSON的设计目标就是为了进行通信。要描述同样的数据,JSON格式的容量会更小。
protocol buffer(PB):
protocolbuffer(以下简称PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了三种语言的实现:java、c++ 和 python,每一种实现都包含了相应语言的编译器以及库文件。
TLV(三元组编码):
T(标记/类型域)L(长度/大小域)V(值/内容域),通常这种信息格式用于金融、军事领域。它通过字节的位运算来进行信息的序列化/反序列化(据说微信的信息格式也采用的是TLV,但实际情况我不清楚):
TLV格式所携带的内容是最有效的,它就连JSON中用于分割层次的“{}”符号都没有。
自定义的格式
当然,如果您的两个内部系统已经约定好了一种信息格式,您当然可以使用自己定制的格式进行描述。您可以使用C++描述一个结构体,然后序列化/反序列它,或者使用一个纯文本,以“|”号分割这些字符串,然后序列化/反序列它。
3、网络协议
如文中第一张图描述的场景,有一个我们看不到但是却很重要的元素:空气。声音在空气中完成传播,真空无法传播声音。同样信息是在网络中完成传播的,没有网络就没法传播信息。网络协议就是计算机领域的“空气”,下图中我们以OSI模型作为参考:
- 物理层:物理层就是我们的网络设备层,例如我们的网卡、交换机等设备,在他们之间我们一般传递的是电信号或者光信号。
- 数据链路层:数据链路又分为物理链路和逻辑链路。物理链路负责组合一组电信号,称之为“帧”;逻辑链路层通过一些规则和协议保证帧传输的正确性,并且可以使来自于多个源/目标 的帧在同一个物理链路上进行传输,实现“链路复用”。
- 网络层:网络层使用最广泛的协议是IP协议(又分为IPV4协议和IPV6协议),IPX协议。这些协议解决的是源和目标的定位问题,以及从源如何到达目标的问题。
- 传输层:TCP、UDP是传输层最常使用的协议,传输层的最重要工作就是携带内容信息了,并且通过他们的协议规范提供某种通信机制。举例来说,TCP协议中的通信机制是:首先进行三次通信握手,然后再进行正式数据的传送,并且通过校验机制保证每个数据报文的正确性,如果数据报文错误了,则重新发送。
- 应用层:HTTP协议、FTP协议、TELNET协议这些都是应用层协议。应用层协议是最灵活的协议,甚至可以由程序员自行定义应用层协议。下图我们表示了HTTP协议的工作方式:
4、通信方式/框架
在文章最前面我们看到其中一个人规定了一种沟通方式:“你必须把我说的话听完,然后给我反馈后。我才会问第二个问题”。这种沟通方式虽然沟通效率不高,但是很有效:一个问题一个问题的处理。
但是如果参与沟通的人处理信息的能力比较强,那么他们还可以采用另一种沟通方式:“我给我提的问题编了一个号,在问完第X个问题后,我不会等待你返回,就会问第X+1个问题,同样你在听完我第X个问题后,一边处理我的问题,一边听我第X+1个问题。”
实际上以上两种现实中的沟通方式,在计算机领域是可以找到对应的通信方式的,这就是BIO(阻塞模式)通信和NIO(非阻塞模式)。
目前常用的IO通信模型包括四种(这里说的都是网络IO):阻塞式同步IO、非阻塞式同步IO、多路复用IO、和真正的异步IO。这些IO模式都是要靠操作系统进行支持,应用程序只是提供相应的实现,对操作系统进行调用。
4-1、BIO通信方式(传统阻塞模式)
BIO就是:blocking IO。最容易理解、最容易实现的IO工作方式,应用程序向操作系统请求网络IO操作,这时应用程序会一直等待;另一方面,操作系统收到请求后,也会等待,直到网络上有数据传到监听端口;操作系统在收集数据后,会把数据发送给应用程序;最后应用程序受到数据,并解除等待状态。如下图所示:
(请您注意,上图中交互的两个元素是应用程序和它所使用的操作系统)就TCP协议来说,整个过程实际上分成三个步骤:三次握手建立连接、传输数据(包括验证和重发)、断开连接。当然,断开连接的过程并不在我们讨论的IO的主要过程中。但是我们讨论IO模型,应该把建立连接和传输数据的者两个过程分开讨论。
以前大多数网络通信方式都是阻塞模式的,即:
- 客户端向服务器端发出请求后,客户端会一直等待(不会再做其他事情),直到服务器端返回结果或者网络出现问题。
- 服务器端同样的,当在处理某个客户端A发来的请求时,另一个客户端B发来的请求会等待,直到服务器端的这个处理线程完成上一个处理。
如下图所示:
传统的BIO通信方式存在几个问题:
- 同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。
- 由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。
上面说的情况是服务器只有一个线程的情况,那么读者会直接提出我们可以使用多线程技术来解决这个问题:
- 当服务器收到客户端X的请求后,(读取到所有请求数据后)将这个请求送入一个独立线程进行处理,然后主线程继续接受客户端Y的请求。
- 客户端一侧,也可以使用一个子线程和服务器端进行通信。这样客户端主线程的其他工作就不受影响了,当服务器端有响应信息的时候再由这个子线程通过 监听模式/观察模式(等其他设计模式)通知主线程。
如下图所示:
但是使用线程来解决这个问题实际上是有局限性的:
- 虽然在服务器端,请求的处理交给了一个独立线程进行,但是操作系统通知accept()的方式还是单个的。也就是,实际上是服务器接收到数据报文后的“业务处理过程”可以多线程,但是数据报文的接受还是需要一个一个的来。
- 在linux系统中,可以创建的线程是有限的。我们可以通过cat /proc/sys/kernel/threads-max 命令查看可以创建的最大线程数。当然这个值是可以更改的,但是线程越多,CPU切换所需的时间也就越长,用来处理真正业务的需求也就越少。
- 创建一个线程是有较大的资源消耗的。JVM创建一个线程的时候,即使这个线程不做任何的工作,JVM都会分配一个堆栈空间。这个空间的大小默认为128K,您可以通过-Xss参数进行调整。
- 当然您还可以使用ThreadPoolExecutor线程池来缓解线程的创建问题,但是又会造成BlockingQueue积压任务的持续增加,同样消耗了大量资源。另外,如果您的应用程序大量使用长连接的话,线程是不会关闭的。这样系统资源的消耗更容易失控。
- 最后,无论您是使用的多线程、还是加入了非阻塞模式,这都是在应用程序层面的处理,而底层socketServer所匹配的操作系统的IO模型始终是“同步IO”,最根本的问题并没有解决。
- 那么,如果你真想单纯使用线程解决阻塞的问题,那么您自己都可以算出来您一个服务器节点可以一次接受多大的并发了。看来,单纯使用线程解决这个问题不是最好的办法。
4-2、BIO通信方式深入分析
JAVA对阻塞模式的支持,就是java.net包中的Socket套接字实现。这里要说明一下,Socket套接字是TCP/UDP等传输层协议的实现。例如客户端使用TCP协议连接这台服务器的时候,当TCP三次握手成功后,应用程序就会创建一个socket套接字对象(注意,这是还没有进行数据内容的传输),当这个TCP连接出现数据传输时,socket套接字就会把数据传输的表现告诉程序员(例如read方法接触阻塞状态)
下面这段代码是java对阻塞模式的支持:
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.BasicConfigurator;
public class SocketServer1 {
static {
BasicConfigurator.configure();
}
/**
* 日志
*/
private static final Log LOGGER = LogFactory.getLog(SocketServer1.class);
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(83);
try {
while(true) {
//这里JAVA通过JNI请求操作系统,并一直等待操作系统返回结果(或者出错)
Socket socket = serverSocket.accept();
//下面我们收取信息(这里还是阻塞式的,一直等待,直到有数据可以接受)
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
Integer sourcePort = socket.getPort();
int maxLen = 2048;
byte[] contextBytes = new byte[maxLen];
int realLen;
StringBuffer message = new StringBuffer();
//read的时候,程序也会被阻塞,直到操作系统把网络传来的数据准备好。
while((realLen = in.read(contextBytes, 0, maxLen)) != -1) {
message.append(new String(contextBytes , 0 , realLen));
/*
* 我们假设读取到“over”关键字,
* 表示客户端的所有信息在经过若干次传送后,完成
* */
if(message.indexOf("over") != -1) {
break;
}
}
//下面打印信息
SocketServer1.LOGGER.info("服务器收到来自于端口:" + sourcePort + "的信息:" + message);
//下面开始发送信息
out.write("回发响应信息!".getBytes());
//关闭
out.close();
in.close();
socket.close();
}
} catch(Exception e) {
SocketServer1.LOGGER.error(e.getMessage(), e);
} finally {
if(serverSocket != null) {
serverSocket.close();
}
}
}
}
上面的服务器端代码可以直接运行。代码执行到serverSocket.accept()的位置就会等待,这个调用的含义是应用程序向操作系统请求客户端连接的接收,这是代码会阻塞,而底层调用的位置在DualStackPlainSocketImpl这个类里面(注意我使用的测试环境是windows 8 ,所以是由这个类处理;如果您是在windows 7环境下进行测试,那么处理类是TwoStacksPlainSocketImpl,这是Windows环境;如果您使用的测试环境是Linux,那么视Linux的内核版本而异,具体的处理类又是不一样的)。
很明显,我们在代码里面并没有设置timeout属性,所以运行的是“if”这段的代码,很明显在调用JNI后,下层也在等待有客户端连接上来。这种调用方式当然有问题:
- 同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。
- 由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。
- 实际上以上的问题是可以通过多线程来解决的,实际上就是当accept接收到一个客户端的连接后,服务器端启动一个新的线程,来读写客户端的数据,并完成相应的业务处理。但是你无法影响操作系统底层的“同步IO”机制。
实际上我们又可以看出,BIO的问题关键不在于是否使用了多线程(包括线程池)处理这次请求,而在于accept()、read()的操作点都是被阻塞。
那么重点的问题并不是“是否使用了多线程”,而是为什么accept()、read()方法会被阻塞。即:异步IO模式 就是为了解决这样的并发性存在的。但是为了说清楚异步IO模式,在介绍IO模式的时候,我们就要首先了解清楚,什么是阻塞式同步、非阻塞式同步、多路复用同步模式。
这里我要特别说明一下,在一篇网文http://justjavac.iteye.com/blog/1998207中,作者主要讲到了自己对非阻塞方式下硬盘操作的理解。按照我的看法,只要有IO的存在,就会有阻塞或非阻塞的问题,无论这个IO是网络的,还是硬盘的。这就是为什么基本的JAVA NIO框架中会有FileChannel(而且FileChannel在操作系统级别是不支持非阻塞模式的)、DatagramChannel和SocketChannel的原因。NIO并不只是为了解决磁盘读写的性能而存在的,它的出现原因、要解决的问题更为广阔;但是另外一个方面,文章作者只是表达自己的思想,没有必要争论得“咬文嚼字”。
API文档中对于serverSocket.accept()方法的使用描述:
Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.
那么我们首先来看看为什么serverSocket.accept()会被阻塞。这里涉及到阻塞式同步IO的工作原理:
- 服务器线程发起一个accept动作,询问操作系统 是否有新的socket套接字信息从端口X发送过来。
- 注意,是询问操作系统。也就是说socket套接字的IO模式支持是基于操作系统的,那么自然同步IO/异步IO的支持就是需要操作系统级别的了。如下图:
- 如果操作系统没有发现有套接字从指定的端口X来,那么操作系统就会等待。这样serverSocket.accept()方法就会一直等待。这就是为什么accept()方法为什么会阻塞:它内部的实现是使用的操作系统级别的同步IO。
阻塞IO 和 非阻塞IO 这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
同步IO 和 非同步IO,这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何相应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
4-3、NIO通信框架(多路复用IO模型)(IO Multiplex)
目前流行的NIO框架非常的多。在论坛上、互联网上大家讨论和使用最多的有以下几种:
原生JAVA NIO框架:
JAVA NIO通信框架基于多路复用IO原理,我们将详细讲解它的工作原理。
APACHE MINA 2:
是一个网络应用程序框架,用来帮助用户简单地开发高性能和高可扩展性的网络应用程序。它提供了一个通过Java NIO在不同的传输例如TCP/IP和UDP/IP上抽象的事件驱动的异步API。
NETTY 4/5:
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。我们将讲解NETTY 4 的工作原理。另外说一句:MANA和NETTY的主要作者是同一人Trustin Lee。
Grizzly:
Grizzly是一种应用程序框架,专门解决编写成千上万用户访问服务器时候产生的各种问题。使用JAVA NIO作为基础,并隐藏其编程的复杂性。
我们试想一下这样的现实场景:
一个餐厅同时有100位客人到店,当然到店后第一件要做的事情就是点菜。但是问题来了,餐厅老板为了节约人力成本目前只有一位大堂服务员拿着唯一的一本菜单等待客人进行服务。
那么最笨(但是最简单)的方法是(方法A),无论有多少客人等待点餐,服务员都把仅有的一份菜单递给其中一位客人,然后站在客人身旁等待这个客人完成点菜过程。在记录客人点菜内容后,把点菜记录交给后堂厨师。然后是第二位客人。。。。然后是第三位客人。很明显,只有脑袋被门夹过的老板,才会这样设置服务流程。因为随后的80位客人,再等待超时后就会离店(还会给差评)。
于是还有一种办法(方法B),老板马上新雇佣99名服务员,同时印制99本新的菜单。每一名服务员手持一本菜单负责一位客人(关键不只在于服务员,还在于菜单。因为没有菜单客人也无法点菜)。在客人点完菜后,记录点菜内容交给后堂厨师(当然为了更高效,后堂厨师最好也有100名)。这样每一位客人享受的就是VIP服务咯,当然客人不会走,但是人力成本可是一个大头哦(亏死你)。
另外一种办法(方法C),就是改进点菜的方式,当客人到店后,自己申请一本菜单。想好自己要点的才后,就呼叫服务员。服务员站在自己身边后记录客人的菜单内容。将菜单递给厨师的过程也要进行改进,并不是每一份菜单记录好以后,都要交给后堂厨师。服务员可以记录号多份菜单后,同时交给厨师就行了。那么这种方式,对于老板来说人力成本是最低的;对于客人来说,虽然不再享受VIP服务并且要进行一定的等待,但是这些都是可接受的;对于服务员来说,基本上她的时间都没有浪费,基本上被老板压杆了最后一滴油水。
如果您是老板,您会采用哪种方式呢?
- 到店情况:并发量。到店情况不理想时,一个服务员一本菜单,当然是足够了。所以不同的老板在不同的场合下,将会灵活选择服务员和菜单的配置。
- 客人:客户端请求
- 点餐内容:客户端发送的实际数据
- 老板:操作系统
- 人力成本:系统资源
- 菜单:文件状态描述符。操作系统对于一个进程能够同时持有的文件状态描述符的个数是有限制的,在linux系统中$ulimit -n查看这个限制值,当然也是可以(并且应该)进行内核参数调整的。
- 服务员:操作系统内核用于IO操作的线程(内核线程)
- 厨师:应用程序线程(当然厨房就是应用程序进程咯)
- 餐单传递方式:包括了阻塞式和非阻塞式两种。
- 方法A:阻塞式/非阻塞式 同步IO
- 方法B:使用线程进行处理的 阻塞式/非阻塞式 同步IO
- 方法C:阻塞式/非阻塞式 多路复用IO
目前流程的多路复用IO实现主要包括四种:select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:
多路复用IO技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。其他情况下多路复用IO技术发挥不出来它的优势。另一方面,使用JAVA NIO进行功能实现,相对于传统的Socket套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。
4-4、JAVA NIO 框架简要设计分析
通过上文的描述,我们知道了多路复用IO技术是操作系统的内核实现。在不同的操作系统,甚至同一系列操作系统的版本中所实现的多路复用IO技术都是不一样的。那么作为跨平台的JAVA JVM来说如何适应多种多样的多路复用IO技术实现呢?面向对象的威力就显现出来了:无论使用哪种实现方式,他们都会有“选择器”、“通道”、“缓存”这几个操作要素,那么可以为不同的多路复用IO技术创建一个统一的抽象组,并且为不同的操作系统进行具体的实现。JAVA NIO中对各种多路复用IO的支持,主要的基础是java.nio.channels.spi.SelectorProvider抽象类,其中的几个主要抽象方法包括:
- public abstract DatagramChannel openDatagramChannel():创建和这个操作系统匹配的UDP 通道实现。
- public abstract AbstractSelector openSelector():创建和这个操作系统匹配的NIO选择器,就像上文所述,不同的操作系统,不同的版本所默认支持的NIO模型是不一样的。
- public abstract ServerSocketChannel openServerSocketChannel():创建和这个NIO模型匹配的服务器端通道。
- public abstract SocketChannel openSocketChannel():创建和这个NIO模型匹配的TCP Socket套接字通道(用来反映客户端的TCP连接)
由于JAVA NIO框架的整个设计是很大的,所以我们只能还原一部分我们关心的问题。这里我们以JAVA NIO框架中对于不同多路复用IO技术的选择器 进行实例化创建的方式作为例子,以点窥豹观全局:
很明显,不同的SelectorProvider实现对应了不同的选择器。由具体的SelectorProvider实现进行创建。另外说明一下,实际上netty底层也是通过这个设计获得具体使用的NIO模型。以下代码是Netty 4.0中NioServerSocketChannel进行实例化时的核心代码片段:
private static ServerSocketChannel newSocket(SelectorProvider provider) {
try {
/**
* Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
* {@link SelectorProvider#provider()} which is called by each ServerSocketChannel.open() otherwise.
*
* See <a href="See https://github.com/netty/netty/issues/2308">#2308</a>.
*/
return provider.openServerSocketChannel();
} catch (IOException e) {
throw new ChannelException(
"Failed to open a server socket.", e);
}
}
- serverChannel.register(Selector sel, int ops, Object att):实际上register(Selector sel, int ops, Object att)方法是ServerSocketChannel类的父类AbstractSelectableChannel提供的一个方法,表示只要继承了AbstractSelectableChannel类的子类都可以注册到选择器中。通过观察整个AbstractSelectableChannel继承关系,下图中的这些类可以被注册到选择器中:
- SelectionKey.OP_ACCEPT:不同的Channel对象可以注册的“我关心的事件”是不一样的。例如ServerSocketChannel除了能够被允许关注OP_ACCEPT时间外,不允许再关心其他事件了(否则运行时会抛出异常)。以下梳理了常使用的AbstractSelectableChannel子类可以注册的事件列表:
实际上通过每一个AbstractSelectableChannel子类所实现的public final int validOps()方法,就可以查看这个通道“可以关心的IO事件”。
- selector.selectedKeys().iterator():当选择器Selector收到操作系统的IO操作事件后,它的selectedKeys将在下一次轮询操作中,收到这些事件的关键描述字(不同的channel,就算关键字一样,也会存储成两个对象)。但是每一个“事件关键字”被处理后都必须移除,否则下一次轮询时,这个事件会被重复处理。
多路复用IO的优缺点:
- 不用再使用多线程来进行IO处理了(包括操作系统内核IO管理模块和应用程序进程而言)。当然实际业务的处理中,应用程序进程还是可以引入线程池技术的
- 同一个端口可以处理多种协议,例如,使用ServerSocketChannel测测的服务器端口监听,既可以处理TCP协议又可以处理UDP协议。
- 操作系统级别的优化:多路复用IO技术可以是操作系统级别在一个端口上能够同时接受多个客户端的IO事件。同时具有之前我们讲到的阻塞式同步IO和非阻塞式同步IO的所有特点。Selector的一部分作用更相当于“轮询代理器”。
- 都是同步IO:目前我们介绍的 阻塞式IO、非阻塞式IO甚至包括多路复用IO,这些都是基于操作系统级别对“同步IO”的实现。我们一直在说“同步IO”,一直都没有详细说,什么叫做“同步IO”。实际上一句话就可以说清楚:只有上层(包括上层的某种代理机制)系统询问我是否有某个事件发生了,否则我不会主动告诉上层系统事件发生了。
4-5、异步IO(真正的NIO)
JAVA 1.7版本中加入的NIO2.0(AIO)对异步IO的实现。在Linux系统中并没有Windows中的IOCP技术,所以Linux技术使用epoll多路复用技术模拟异步IO。
阻塞式同步IO、非阻塞式同步IO、多路复用IO这三种IO模型是由操作系统提供支持,且这三种IO模型都是同步IO,都是采用的“应用程序不询问我,我绝不会主动通知”的方式。
异步IO则是采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数:
- 和同步IO一样,异步IO也是由操作系统进行支持的。微软的windows系统提供了一种异步IO技术:IOCP(I/O Completion Port,I/O完成端口);
- Linux下由于没有这种异步IO技术,所以使用的是epoll对异步IO进行模拟。
JAVA AIO框架简析
- 这里也没有将JAVA AIO框架所有的实现类画完,只是通过这个结构分析要告诉各位读者JAVA AIO中类设计和操作系统的相关性。
- 在文中我们一再说明JAVA AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO,这个从JAVA AIO框架的部分类设计上就可以看出来。例如框架中,在Windows下负责实现套接字通道的具体类是“sun.nio.ch.WindowsAsynchronousSocketChannelImpl”
- 特别说明一下,请注意图中的“java.nio.channels.NetworkChannel”接口,这个接口同样被JAVA NIO框架实现了,如下图所示:
- 注意在JAVA NIO框架中,我们说到了一个重要概念“selector”(选择器)。它负责代替应用查询中所有已注册的通道到操作系统中进行IO事件轮询、管理当前注册的通道集合,定位发生事件的通道等操操作;但是在JAVA AIO框架中,由于应用程序不是“轮询”方式,而是订阅-通知方式,所以不再需要“selector”(选择器)了,改由channel通道直接到操作系统注册监听。
- JAVA AIO框架中,只实现了两种网络IO通道“AsynchronousServerSocketChannel”(服务器监听通道)、“AsynchronousSocketChannel”(socket套接字通道)。但是无论哪种通道他们都有独立的fileDescriptor(文件标识符)、attachment(附件,附件可以使任意对象,类似“通道上下文”),并被独立的SocketChannelReadHandle类实例引用。
- JAVA NIO和JAVA AIO框架,除了因为操作系统的实现不一样而去掉了Selector外,其他的重要概念都是存在的,例如上文中提到的Channel的概念,还有演示代码中使用的Buffer缓存方式。实际上JAVA NIO和JAVA AIO框架您可以看成是一套完整的“高并发IO处理”的实现。
- 记住JAVA AIO 和 JAVA NIO 框架都是要使用线程池的(当然您也可以不用),线程池的使用原则,一定是只有业务处理部分才使用,使用后马上结束线程的执行(还回线程池或者消灭它)。JAVA AIO框架中还有一个线程池,是拿给“通知处理器”使用的,这是因为JAVA AIO框架是基于“订阅-通知”模型的,“订阅”操作可以由主线程完成,但是您总不能要求在应用程序中并发的“通知”操作也在主线程上完成吧^_^。
- 最好的改进方式,当然就是使用Netty或者Mina咯。
4-6、为什么还有Netty
那么有的读者可能就会问,既然JAVA NIO / JAVA AIO已经实现了各主流操作系统的底层支持,那么为什么现在主流的JAVA NIO技术会是Netty和MINA呢?答案很简单:因为更好用,这里举几个方面的例子:
- 虽然JAVA NIO 和 JAVA AIO框架提供了 多路复用IO/异步IO的支持,但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对 Protocol Buffer、JSON这些信息格式的封装,但是Netty框架提供了这些数据格式封装(基于责任链模式的编码和解码功能)
- 要编写一个可靠的、易维护的、高性能的(注意它们的排序)NIO/AIO 服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如:客户端的权限、还有上面提到的信息格式封装、简单的数据读取。这些Netty框架都提供了响应的支持。
- JAVA NIO框架存在一个poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能block意味着CPU的使用率会变成100%(这是底层JNI的问题,上层要处理这个异常实际上也好办)。当然这个bug只有在Linux内核上才能重现。
- 这个问题在JDK 1.7版本中还没有被完全解决:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719。虽然Netty 4.0中也是基于JAVA NIO框架进行封装的(上文中已经给出了Netty中NioServerSocketChannel类的介绍),但是Netty已经将这个bug进行了处理。
- 其他原因,用过Netty后,您就可以自己进行比较了。
5、通信方式
我们都是通过声带发声,通过口型和舌头控制音调、音量。声音传到对方的耳朵里,经过对方的大脑处理,再通过对方发声传到我们的耳朵里,于是我们的大脑得到了答案。
5-1、直接使用单纯HTTP请求
您对“简洁”的理解是什么样的呢?快速开发,快速部署、快速理解,还是调用速度快,并发支持高呢?无论您怎样理解“简洁”,有一个是事实您是无法否定的,很大一部分公司都是用单纯Http协议(使用标准WEB容器)+JSON信息格式的方式进行系统间通信。这种做法有几个好处:
- 上手快:对于做WEB系统有较丰富积累的公司,并不需要思考“这个接口是给终端用户的还是给另外一个系统通信的”,依葫芦画瓢就可以把提供给系统间接口的。
- 实现快:就像上面提到的一样,在不考虑实现细节的情况下,任务开发过WEB系统开发人员都可以接手这个工作,并且在分钟级别的时间内,就可以把接口功能实现出来。
- 速度也不算慢:虽然很少有人去比较RMI和HTTP的速度,或者Dubbo和HTTP调用的速度,但是从各种介绍来看后者的速度虽然没有前者快,但是后者的速度还是可接受的。而且并发问题完全可以交给其他方案来解决(Nginx或者Haproxy,这个相关技术的讲述在“负载均衡层”技术方案系列博文中《负载均衡层技术》)
5-2、直接使用HTTP调用的问题
但是直接使用HTTP,还是有一些问题:
- 由于其基于HTTP和为客户端交互设计的WEB容器,其速度毕竟会是一个问题。在《标准Web系统的架构分层》这篇文章中,我已经详细讲诉了HTTP的通信过程。
- 虽然HTTP协议中有很多方式可以优化访问速度。例如使用keep-alive保持Http Connection的连接复用,使用gzip压缩Body中的传输数据。但是受WEB服务器选择、HTTP通信特点的影响,速度就会受到影响。
- 不好管理:这里所说的管理,并不是几百个接口不能使用word文档进行管理;而是说,当系统持续增大后,接口的复杂性将会成几何级递增。终端客户端一次请求的处理不再由一个系统进行处理,而是要使用多个系统进行关联计算才能得到结果。那,这时候怎么办?当然如果您非要说,各系统怎么交互调用交给终端客户机处理,好吧,我竟无言以对。。。
- 实际上这种调用单纯HTTP + JSON信息格式的实现速度,真不是最快的。。。可能是因为有的团队并没有使用过其他的调用技术(在生产环境下),就没法比较。个人认为:没有最好的技术,只有最合适的技术,所以简单的业务系统间使用单纯的HTTP + JSON信息格式的技术,并没有什么不可以。