现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomcat,Jetty。学习和掌握NIO技术已经不是一个JAVA攻城狮的加分技能,而是一个必备技能
Netty 底层实现就是基于我们的NIO,因为网络编程使用NIO非常复杂,容易写出Bub,netty封装了nio,大大的简化了学习成本与提高了编码效率。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。
buffer对于传统IO(inputstream outputstream的区别),比较明显的就是buffer为双向的,读的同事也可以写
例如
public static void main(String[] args) { IntBuffer buffer = IntBuffer.allocate(10); // 写 buffer.put(1); buffer.put(4); buffer.put(3); buffer.put(2); // 读写切换* buffer.flip(); // 读 while (buffer.hasRemaining()) { System.out.println(buffer.get()); } }
比较重要的一个就是buffer的读写切换,flip
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等
三者对应关系如下图
传统IO与NIO读取一个文件的区别(代码演示)
传统IO
public static void readFile(){ InputStream in = null; try{ in = new BufferedInputStream(new FileInputStream("/Users/duguotao/Desktop/data.txt")); byte [] buf = new byte[1024]; int bytesRead = in.read(buf); while(bytesRead != -1){ for(int i=0;i
NIO(channel结合buffer)
public static void main(String[] args) throws Exception { File file = new File("/Users/duguotao/Desktop/data.txt"); FileInputStream in = new FileInputStream(file); // 获取通道 FileChannel channel = in.getChannel(); // 创建buffer从channel读取文件 ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); channel.read(byteBuffer); System.out.println(new String(byteBuffer.array())); in.close(); }
小栗子,利用buffer对文件进行copy
public static void main(String[] args) throws Exception { File file = new File("/Users/duguotao/Desktop/data.txt"); FileInputStream in = new FileInputStream(file); FileChannel channel = in.getChannel(); // 创建buffer从channel读取文件 ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); channel.read(byteBuffer); // buffer读写转换 byteBuffer.flip(); FileOutputStream outputStream = new FileOutputStream("/Users/duguotao/Desktop/data(2).txt"); FileChannel fileChannel = outputStream.getChannel(); fileChannel.write(byteBuffer); in.close(); outputStream.close(); }
说完了FileChannel和Buffer, 大家应该对Buffer的用法比较了解了,这里使用SocketChannel来继续探讨NIO。NIO的强大功能部分来自于Channel的非阻塞特性,套接字的某些操作可能会无限期地阻塞。例如,对accept()方法的调用可能会因为等待一个客户端连接而阻塞;对read()方法的调用可能会因为没有数据可读而阻塞,直到连接的另一端传来新的数据。总的来说,创建/接收连接或读写数据等I/O调用,都可能无限期地阻塞等待,直到底层的网络实现发生了什么。慢速的,有损耗的网络,或仅仅是简单的网络故障都可能导致任意时间的延迟。然而不幸的是,在调用一个方法之前无法知道其是否阻塞。NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。
serverSocketChannel.configureBlocking(false);在非阻塞式信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null
@SuppressWarnings("all")
public static void main(String[] args) throws Exception {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
// socket server
ServerSocket socket = new ServerSocket(6666);
System.out.println("启动socket服务器");
// 等待client链接
for (; ; ) {
// 阻塞
final Socket accept = socket.accept();
System.out.println("链接一个客户端");
// 与客户端通讯
threadPool.execute(() -> {
System.out.printf("当前线程:%s", Thread.currentThread().getId());
try {
// 客户端发送到数据
byte[] data = new byte[64];
// 阻塞
InputStream inputStream = accept.getInputStream();
for (; ; ) {
if (inputStream.read(data) == -1) {
break;
}
}
System.out.printf("客户端发送的的数据: %s", new String(data, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) throws Exception {
// 服务器端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 绑定port
// 非阻塞
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
// 服务端 关心链接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
for (; ; ) {
// 无事件发生
if (selector.select(1000) == 0) {
System.out.println("等待链接");
}
// SelectionKey 集合 有事件发生
Set selectionKeys = selector.selectedKeys();
Iterator keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 链接事件
if (key.isAcceptable()) {
// 新的客户端链接
// 给该客户端生成socket channel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端链接成功。。。");
// 非阻塞
socketChannel.configureBlocking(false);
// 注册到selector 读取事件 绑定buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 读取数据
if (key.isReadable()) {
// 读取channel数据
SocketChannel channel = (SocketChannel) key.channel();
// 上方设置的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.printf("客户端发来的数据:%s", new String(buffer.array()));
}
// 删除当前key 避免重复消费
keyIterator.remove();
}
}
}
以后阶段我会针对Netty进行学习。
会不定时的更新自己的学习笔记。