本篇主要从学习角度整理java的几个网络模型,包括:
- BIO通信模型
- 伪异步通信模型
- NIO通信模型
- NIO2.0(AIO)
BIO通信模型
BIO同步阻塞I/O通信模型BIO通信模型最大的特点是,当服务端程序收到一条网络连接请求时,需要单独为其分配一个处理线程,服务端处理完成之后,将输出流返回给客户端,此时才销毁线程。
例如上图中的案例,acceptor在编程时一般就是ServerSocket,通过一个无限循环的accept操作获取客户端请求,然后分配一个线程为其进行处理,类似的代码如下:
//BIO 服务端示例代码
//其中SomeHandler为具体的网络业务处理器
ServerSocket server = new ServerSocket(port);
while(true){
Socket socket = server.accept();
new Thread(SomeHandler(socket)).start();
}
该模型最大的问题是缺乏弹性伸缩能力,客户端和服务端线程个数的比例为1:1,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀,系统性能也将急剧下降,随着并发访问量增大,系统会发生线程堆栈溢出,创建线程失败等问题。
伪异步I/O通信模型
伪异步I/O通信模型为了改进BIO的一个线程一个连接的模型,引入线程池或者消息队列来实现1个或者多个线程处理N个客户端的模型,但由于底层仍然使用同步阻塞I/O,因此被称为“伪异步”。
服务端的示例代码如下:
//伪异步网络通信服务端示例代码
//其中SomeHandler为具体的网络业务处理器
ServerSocket server = new ServerSocket(port);
ExecutorService executor = Executors.newFixedThreadPool(100);
while(true){
Socket socket = server.accept();
executor.submit(SomeHandler(socket));
}
最大的不同可以看出是在处理网络请求的地方,伪异步使用了线程池。这样可以避免线程的不断销毁和重新创建,但是本质上,一条连接任然独占一个线程,意思是如果一条连接不断开,这个线程将被一直阻塞,不管期间有没有数据传输。
NIO编程模型
NIO可以称为非阻塞I/O(Non-block I/O)。它提供了高速的、面向块的I/O。补充一下NIO的一些概念,以便作说明。
缓冲区Buffer
BIO编程中,数据的输入输出靠的是流。NIO通过Buffer来缓存操作期间的数据,相比之下,缓冲区提供了对数据的结构化访问。最常用的缓冲区是ByteBuffer,它提供了一组功能来操作byte数组。(事实上,每一种Java基本类型,除了Boolean,都有对应的一种缓冲区,例如CharBuffer、ShortBuffer等)。
通道Channel
理解通道就可以认为它像一条水管,网络数据可以在Channel上任意的写入和读取,它是双向的(区别于流的单向)。
多路复用器
NIO编程的基础就是多路复用器Selector,它提供选择已经就绪的任务的能力。通常在selector上会注册很多channel(来自于客户端的网络请求),selector通不过不断轮询,侦测哪一个channel上有数据的读写信号,就通过SelectionKey让该channel激活进行相应的读写操作。
示例
服务端时序图
NIO服务端时序图上图描述了NIO编程过程中的通信时序图,异步的网络请求代码更不易编写,下面看看服务端的简单示例代码:
private Selector selector;
private ServerSocketChannel serverChannel;
public void init(){
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.configurateBlocking(false);
serverChannel.socket.bind(port);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void run(){
while(true){
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
handleInput(key); // 网络请求处理器
}
}
}
简单的看这段代码,在初始化时需要执行的操作包括:
- open一个selector和ServerSocketChannel
- 设置非阻塞模式
- 绑定端口号
- 将channel注册到selector上,接受accept事件
接着主循环要做的事情包括:
- 轮询select
- 从selector中获取触发了信号的SelectionKey
- 将SelectionKey交给网络请求处理器进行处理(处理器要完成的事情包括接受连接、接收数据,解码数据,写回数据等)
客户端时序图
NIO客户端时序图客户端的代码编写逻辑也很类似,基本的原理就是创建一个channel,将其注册到selector上,等待轮询信号。示例代码如下:
private Selector selector;
private SocketChannel socketChannel;
public void init(){
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configurateBlocking(false);
}
public void run(){
doConnect();//执行连接服务器的操作
while(true){
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
handleInput(key); // 网络请求处理器
}
}
}
public void doConnect(){
if(socketChannel.connect(port)){
socketChannel.register(selector, SelectionKey.OP_READ);
}else{
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
可以对比与服务端的代码,不同的地方包括:服务端会注册OP_ACCEPT事件,用于接受客户端的连接,客户端会注册OP_CONNECT事件,用于连接服务端;此外,他们使用的channel也不一样,服务端使用的是ServerSocketChannel,客户端使用的是SocketChannel。
除了这两个特殊事件,他们还都能够注册OP_READ和OP_WRITE事件,用于网络数据的读和写。
我在这里更偏向于网络模型的对比,因此网络数据的实际读写代码不在这里编写,需要注意的是,网络数据的读写需要程序员自己操作buffer对象,同时还要面对“半包读写问题”
优势
使用NIO编程的优势主要有:
- 客户端发起的连接是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
- SocketChannel的读写操作都是异步的,没有可读写的数据时,它不会同步等待,I/O通信线程就可以处理其他的连接。
- 线程模型得到优化,一个seletor线程可以同时处理成千上万条连接。
AIO编程
在JDK1.7以后升级了NIO类库,被称为NIO2.0。它提供了与UNIX网络编程事件驱动I/O相对应的AIO。AIO不需要通过多路复用器(selector)对注册的通道进行轮询操作即可实现异步读写,简化了NIO的编程模型。事实上,它传递的是一个信号变量。
AIO编程的示例代码我这里就不再列举,接下来主要看一下,四种方式的对比。
模型对比
同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步I/O(AIO) | |
---|---|---|---|---|
客户端个数 | 1:1 | M:N | M:1 | M:0 |
I/O类型 | 阻塞I/O | 阻塞I/O | 非阻塞I/O | 非阻塞I/O |
I/O类型 | 同步 | 同步 | 异步 | 异步 |
API使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
虽然我们本系列主要是学习NIO框架Netty,但是并非意味着所有的Java网络编程都得用NIO和Netty。通过对比我们可以看出,BIO、伪异步I/O也有自己的优势:简单,因此根据业务应用场景,如果客户端并发数不多,服务器负载也低,那就完全可以考虑直接使用较为低级的网络编程模型。
下一篇开始,我们真正开始学习netty。
网友评论