BIO、NIO、Netty
文章目录
- BIO
- NIO
- 通道(Channel)和缓冲区(Buffer)
- 缓冲区
- 直接缓冲区和非直接缓冲区
- 通道
- 使用NIO实现文件读写
- 网络NIO
- Selector选择器
- SelectionKey
- serverSocketChannel
- SocketChannel
- NIO和BIO的区别
- Netty
- 线程模型和异步模型
BIO
BIO(basic IO或者block IO),主要应用于文件IO和网络IO,BIO和NIO最大的不同是BIO是阻塞式的,而NIO是非阻塞式的。
下面是一个BIO实现基于TCP网络IO的例子:
TCP服务端:
public class TCPServer {
public static void main(String[] args) throws IOException {
// 服务端socket连接
ServerSocket ss = new ServerSocket(9999);
while(true) {
// 阻塞监听客户端连接
Socket s = ss.accept();
// 阻塞等待客户端写入数据到输入流
InputStream is = s.getInputStream();
byte[] b = new byte[1024];
// 将用户写入的数据保存到数组中
is.read(b);
// 获取客户端IP
String clientIP = s.getInetAddress().getHostAddress();
System.out.println(clientIP + ":" + Arrays.toString(b));
// 获取输出流
OutputStream os = s.getOutputStream();
// 写数据到输出流给客户端
os.write("I am listening 9999 port!".getBytes());
// 关闭
s.close();
}
}
}
TCP客户端:
public class TCPClient {
public static void main(String[] args) throws IOException {
while(true) {
// 打开客户端socket
Socket s = new Socket("127.0.0.1", 9999);
// 阻塞获取输出流
OutputStream os = s.getOutputStream();
System.out.println("please input:");
Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
// 写数据到输出流
os.write(msg.getBytes());
// 获取输入流,读取服务端写的数据,阻塞
InputStream is = s.getInputStream();
byte[] b = new byte[1024];
is.read(b);
System.out.println("server response:" + Arrays.toString(b));
s.close();
}
}
}
NIO
NIO(New IO或者是non-blocking IO),NIO和BIO作用和目的相同,但是他们的实现方式不同。
- BIO采用流的方式处理数据;NIO采用块的方式处理数据,这种一块处理数据的方式 IO效率比流IO效率高很多
- BIO是阻塞式的,NIO是非阻塞式的
- BIO流的方式是单向的,即有一个输入流和一个输出流;NIO采用的缓冲区是双向的,数据可以从缓冲区写入到通道中,也可以从通道读入到缓冲区
在面向缓冲区的NIO中,磁盘\网络数据和应用程序进行传输要建立通道,通道可以想象成一条铁路,用于两个地点的连接,实际上是通过火车运输的。类似的,通道也是,它只是磁盘/网络文件和程序之间建立的一个连接,本身不传输数据,传输数据用的是缓冲区(类似火车)。通道是双向的,数据放到缓冲区中进行传输,磁盘/网络数据传递给应用程序,应用程序取出数据后可以放入新的数据到缓冲区中,然后缓冲区又把数据发送到磁盘/网络上。
通道(Channel)和缓冲区(Buffer)
NIO系统的核心在于:通道(Channel)、缓冲区(Buffer)和选择器(selector)。通道表示打开到IO设备(如文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,通道负责传输数据,缓冲区负责存储数据。
缓冲区
缓冲区实际上是一个容器,是一个特殊数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
根据数据类型不同,提供了相应类型的缓冲区:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
缓冲区中的基本方法和属性如下示例:
public class Test {
public static void main(String[] args) {
IntBuffer buf = IntBuffer.allocate(1024);
/** * 属性: * position:当往缓冲区中写数据的时候,position指向当前缓冲区 * 可用位置头部;当读取数据模式时,position指向缓冲区数据的头部 * limit:当写数据时,limit指向缓冲区尾部;当读数据模式时,指向缓冲区 * 有数据区的尾部 * capacity:指向缓冲区尾部 */
System.out.println("---allocate---");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// 写数据到缓冲区
buf.put(1);
buf.put(2);
System.out.println("---put---");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// 切换到读模式
buf.flip();
System.out.println("---flip---");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// 读取缓冲区中的数据
int[] dst = new int[buf.limit()];
buf.get(dst);
System.out.println("---get---");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
// 重新读取缓冲区数据,使position重新执行数据头部
buf.rewind();
System.out.println("---rewind---");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
dst = new int[buf.limit()];
buf.get(dst); // 如果不进行rewind()操作,则这里会抛异常:java.nio.BufferUnderflowException
// 清空缓冲区,但是缓冲区中的数据依旧存在,只是数据不能再被读取
buf.clear();
System.out.println("---clear---");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
}
}
结果:
---allocate---
0
1024
1024
---put---
2
1024
1024
---flip---
0
2
1024
---get---
2
2
1024
---rewind---
0
2
1024
---clear---
0
1024
1024
直接缓冲区和非直接缓冲区
- 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。
通过allocate()方法分配非直接缓冲区 - 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。
通过allocateDirect()方法分配直接缓冲区
可以提高数据读取效率,但是分配和销毁物理内存映射区会耗费性能
通过调用缓冲区对象的isDirect()方法来获取是那种缓冲区(直接缓冲区返回true,非直接缓冲区返回false)
非直接缓冲区:
数据先从物理磁盘读到内核地址空间,再copy到用户地址空间(JVM)
直接缓冲区:
NIO增加了直接缓冲区,去掉了内核地址空间和用户地址空间直接的copy,而是创建了一个物理内存映射文件,用来存储数据。
通道
类似于BIO中的流,如FileInputStream对象,通道用来建立IO源与目标(文件、网络套接字、硬件设备等)的一个连接,通道本身不能访问数据,而是只能与buffer进行交互。
BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来写操作。常用Channel类:
- FileChannel:用于文件的数据读写
- DatagramChannel:用于UDP的数据读写
- ServerSocketChannel:用于服务端TCP的数据读写
- SocketChannel用于客户端TCP的数据读写
使用NIO实现文件读写
// 通过NIO实现文件IO
public class TestNIO {
// 往本地文件中写数据
@Test
public void test1() throws IOException {
// 创建输出流
FileOutputStream fos = new FileOutputStream("basic.txt");
// 从流中得到一个通道
FileChannel fc = fos.getChannel();
// 创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
String str = "hello";
// 把一个字节数组存入缓冲区
buffer.put(str.getBytes());
// 调整缓存区的position和limit的位置,切换到读模式
buffer.flip();
// 把缓冲区写到通道中
fc.write(buffer);
// 关闭
fos.close();
}
// 从本地文件读数据
@Test
public void test2() throws IOException {
File file = new File("basic.txt");
// 创建输入流
FileInputStream fis = new FileInputStream("basic.txt");
// 获取管道
FileChannel fc = fis.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
// 从通道读取数据存入缓冲区
fc.read(buffer);
System.out.println(Arrays.toString(buffer.array()));
}
// 使用NIO实现文件复制
@Test
public void test3() throws IOException {
// 创建输入缓冲区和输入缓冲区
FileInputStream fis = new FileInputStream("basic.txt");
FileOutputStream fop = new FileOutputStream("basic2.txt");
// 获取输入缓冲区和输出缓冲区的管道
FileChannel channel1 = fis.getChannel();
FileChannel channel2 = fop.getChannel();
// 缓冲区数据的复制(把输入缓冲区的数据写到输出缓冲区中)
channel2.transferFrom(channel1, 0, channel1.size());
fis.close();
fop.close();
}
}
上面用到的FileChannel并不支持非阻塞操作,NIO的主要用途是进行网络IO。
网络NIO
Java NIO中的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些技术通信的服务等等。
在Java中编写Socket服务器,通常有以下几种模式:
- 一个客户端连接用一个线程
优点:编程简单
缺点:连接非常多时,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃 - 把每一个客户端连接交给一个拥有固定数量线程的连接池
优点:编程简单,可以处理大量连接
缺点:线程的开销非常大,如果连接非常多,排队现象会比较严重 - 使用java的NIO,用非阻塞的IO方式处理。这种模式可以用一个线程处理大量的客户端连接。
问题:
- NIO如何做到非阻塞?
- NIO如何做到一个线程可以处理多个客户端连接?
Java NIO能做到如上两点,主要是下面这四个核心类及其API。
Selector选择器
能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,即多个连接。这样使得只有连接真正有读写事件发生时,才会调用函数来进行读写操作,大大减小了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了线程之间的上下文切换导致的开销。
该类的常用方法:
SelectionKey
SelectionKey代表了Selector和serverSocketChannel的注册关系,可以理解为读、写、连接事件,一共有四种:
- int OP_ACCEPT,有新的网络连接可以accept,值为16
- int OP_CONNECT,代表连接已经建立,值为8
- int OP_READ、int OP_WRITE,代表读、写操作,值为1和4
该类的常用方法:
serverSocketChannel
serverSocketChannel用来在服务器端监听新的客户端Socket连接。
该类的常用方法:
SocketChannel
网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。
该类的常用方法:
示例:
服务器端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 打开服务端负责监听客户端连接的管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 打开一个选择器,监听客户端的行为
Selector selector = Selector.open();
// 给服务端绑定端口
serverSocketChannel.bind(new InetSocketAddress(9999));
// 设置NIO为非阻塞模型
serverSocketChannel.configureBlocking(false);
// 将serverSocketChannel注册到选择器中国
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
Iterator<SelectionKey> iterator;
// 开始处理客户端的连接、读、写操作
while (true) {
if (selector.select(2000) == 0) {
System.out.println("server:没有客户端事件,服务端可以处理其他的任务");
continue;
}
// 获取selectionKey集合的迭代器,遍历这个集合
// 这里一定要使用迭代器,因为涉及到一遍遍历一遍删除的情况
iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 客户端连接事件
if(key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 读取客户端数据
if(key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("客户端数据:"+new String(buffer.array()));
}
// 将处理过的事件从集合中移除
selector.selectedKeys().remove(key);
}
}
}
}
客户端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
// 得到一个网络通道
SocketChannel channel = SocketChannel.open();
// 设置NIO为非阻塞模式
channel.configureBlocking(false);
// 服务端的ip和端口号
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
// 尝试连接
if(! channel.connect(address)) {
// 一直尝试连接
while (! channel.finishConnect()) {
// 连接的过程中,客户端可以处理其他任务,这就是NIO非阻塞模型的优势
System.out.println("客户端连接服务端的同时,可以处理其他任务");
}
}
String msg = "hello server";
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
// 将缓冲区数据写入通道
channel.write(byteBuffer);
// 不让客户端程序退出
System.in.read();
}
}
NIO和BIO的区别
- BIO采用流的方式处理数据;NIO采用块的方式处理数据,这种一块处理数据的方式 IO效率比流IO效率高很多
- BIO流的方式是单向的,即有一个输入流和一个输出流;NIO采用的缓冲区是双向的,数据可以从缓冲区写入到通道中,也可以从通道读入到缓冲区
- BIO是阻塞式的,NIO是非阻塞式的
- BIO方式适合用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中
- NIO方式使用于连接数目多且连接比较短的架构,如聊天服务器,并发局限于应用中,编程复杂
还有一种IO模型是AIO,A表示异步Synchronized,即异步非阻塞模型。对于NIO同步非阻塞模型中,需要轮询检查是否有客户端事件需要处理。而对于AIO异步非阻塞模型中,客户端有事件发生时会去通知服务端进行处理,而不用服务端不停地轮询检测。AIO方式适用于连接数目多且连接比较长的架构,如相册服务器,充分调用OS参与并发操作,编程比较复杂。
Netty
Netty是一个Java开源框架,Netty提供异步的、基于事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠的网络IO程序。Netty基于NIO开发的。Netty框架的目的就是使业务逻辑和网络编码部分分离出来。
线程模型和异步模型
讲解Netty之前,先来了解下Java的线程模型:
- 单线程模型,服务端只用一个线程通过多路复用搞定所有的IO操作(连接、读、写),编码简单,但是如果客户端连接数量较多,将无法支持,如上面网络NIO的示例,只有一个selector线程处理客户端的连接、读、写事件
- 线程池模型 ,服务端采用一个线程专门处理客户端的连接请求,采用一组线程(放在线程池中)负责IO操作。在绝大多数场景下,该模型都能够满足使用。
- Netty模型,改进线程池模型。采用了两个线程池,一个线程池(称为Boos Group)中的一组线程负责客户端的连接请求,另一个线程池(称为Worker Group)中的一组线程负责客户端的IO操作。NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的secket网络通道。NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终是由IO程序NioEventLoop负责。
上面就是Netty的线程模型,下面来说说Netty的异步模型。
Netty的异步模型是建立在future和callback上的。future的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,然后程序可以继续往下执行其他线程,后续可以通过future去监控方法fun的处理过程。当fun方法执行完成返回后,通过callback方法去接受返回并处理相应逻辑。
还没有评论,来说两句吧...