本文共 9797 字,大约阅读时间需要 32 分钟。
Java网络编程与IO流目录:
Java中BIO、NIO、AIO的区别是什么?
BIO:阻塞同步通信模式,客户端与服务器连接需要三次握手,使用简单,但吞吐量少;(流)
NIO: 非阻塞同步通信模式,客户端与服务器通过channel连接,采用多路复用器轮询注册的channel,提高吞吐量和可靠性。相比较于BIO,始终只有一个线程,并没有启动额外的线程来处理,解决了BIO线程无线增加的问题。(缓冲)
AIO:非阻塞异步通信模式,采用异步通道实现通信,其基于事件和回调机制。
补充:以一个经典的烧开水的例子通俗地讲解它们之间的区别
BIO (同步阻塞 I/O)
这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 小菠萝一直看着着这个水壶,直到这个水壶烧开,才去处理下一个水壶。线程在等待水壶烧开的时间段什么都没有做。
NIO(同步非阻塞 I/O)
还拿烧开水来说,NIO的做法是小菠萝一边玩着手机,每隔一段时间就看一看每个水壶的状态,看看是否有水壶的状态发生了改变,如果某个水壶烧开了,可以先处理那个水壶,然后继续玩手机,继续隔一段时间又看看每个水壶的状态。
AIO (异步非阻塞 I/O)
小菠萝觉得每隔一段时间就去看一看水壶太费劲了,于是购买了一批烧开水时可以哔哔响的水壶,于是开始烧水后,小菠萝就直接去客厅玩手机了,水烧开时,就发出“哔哔”的响声,通知小菠萝来关掉水壶。
1.IO介绍
a.全面认识IO
传统的IO大致可分为4种类型
- InputStream、OutputStream基于字节操作的IO
- Writer、Reader基于字符操作的操作
- File基于磁盘操作的IO
- Socket基于网络操作的IO
下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO ,因为网络通讯同样是 IO 行为。
下的类和接口很多,但大体都是 InputStream、OutputStream、Writer、Reader 的子集,所有掌握这4个类和File的使用,是用好 IO 的关键。
b.BIO、NIO、AIO的区别
- BIO:Block IO同步阻塞式IO,就是传统的IO,特点是模式简单使用方便,但并发处理能力低;(BIO 就是传统的 包,它是基于流模型实现的)
- NIO:New IO 同步非阻塞式IO,是传统IO的升级,客户端和服务器通过Channel(通道)通讯,实现了多路复用;(NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象)
- AIO:Asynchronous IO是NIO的升级,实现了异步非阻塞IO,异步IO的操作基于事件和回调机制。(AIO是Java1.7之后引入的包,是NIO的升级)
c.IO的使用
可参考之前的Java的IO流,主要为InputStream OutputStream Reader Writer
2.同步 异步 阻塞 非阻塞
a.同步与异步
同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。
同步和异步就是指发送方和接收方是否协调步调一致。
同步通信是指:发送方和接收方通过一定机制,实现收发步调协调。如:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式
异步通信是指:发送方的发送不管接收方的接收状态,如:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
b.阻塞与非阻塞
阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
阻塞就是这个事情阻到这儿了,不能继续往下干事了,非阻塞就是这个事情不会阻碍你继续干后面的事情。
总之:同步是两个对象之间的关系,而阻塞是一个对象的状态。
c.什么是同步阻塞BIO,同步非阻塞NIO,异步非则塞AIO?
同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。
同步非阻塞IO: 用户进程发起一个IO操作以后,可做其它事情,但用户进程需要经常询问IO操作是否完成,这样造成不必要的CPU资源浪费。
异步非阻塞IO: 用户进程发起一个IO操作然后,立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类比Future模式。
3.文件下的BIO与NIO
-
简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
-
- 面向流的I/O 系统一次一个字节地处理数据。
- 一个面向块(缓冲区)的I/O系统以块的形式处理数据。
NIO主要有三个核心部分组成:
- buffer缓冲区
- Channel管道
- Selector选择器
在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据。
简单理解一下:
- Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理!
-
要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
-
- Channel–>运输
- Buffer–>数据
相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)!
Java 7 之前文件的读取是这样的:
// 添加文件FileWriter fileWriter = new FileWriter(filePath, true);fileWriter.write(Content);fileWriter.close();// 读取文件FileReader fileReader = new FileReader(filePath);BufferedReader bufferedReader = new BufferedReader(fileReader);StringBuffer bf = new StringBuffer();String str;while ((str = bufferedReader.readLine()) != null) { bf.append(str + "\n");}bufferedReader.close();fileReader.close();System.out.println(bf.toString());
Java 7 引入了Files(java.nio包下)的,大大简化了文件的读写,如下:
// 写入文件(追加方式:StandardOpenOption.APPEND)Files.write(Paths.get(filePath), Content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);// 读取文件byte[] data = Files.readAllBytes(Paths.get(filePath));System.out.println(new String(data, StandardCharsets.UTF_8));
读写文件都是一行代码搞定,没错这就是最优雅的文件操作。
Files 下还有很多有用的方法,比如创建多层文件夹,写法上也简单了:
// 创建多(单)层目录(如果不存在创建,存在不会报错)new File("D://a//b").mkdirs();
4.Socket通信下对比BIO NIO AIO
a.传统的Socket实现
接下来我们将会实现一个简单的 Socket,服务器端只发给客户端信息,再由客户端打印出来的例子,代码如下:
int port = 4343; //端口号// Socket 服务器端(简单的发送信息)Thread sThread = new Thread(new Runnable() { @Override public void run() { try { ServerSocket serverSocket = new ServerSocket(port); while (true) { // 等待连接 Socket socket = serverSocket.accept(); Thread sHandlerThread = new Thread(new Runnable() { @Override public void run() { try (PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) { printWriter.println("hello world!"); printWriter.flush(); } catch (IOException e) { e.printStackTrace(); } } }); sHandlerThread.start(); } } catch (IOException e) { e.printStackTrace(); } }});sThread.start();// Socket 客户端(接收信息并打印)try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println("客户端:" + s));} catch (UnknownHostException e) { e.printStackTrace();} catch (IOException e) { e.printStackTrace();}
-
调用 accept 方法,阻塞等待客户端连接;
-
利用 Socket 模拟了一个简单的客户端,只进行连接、读取和打印;
在 Java 中,线程的实现是比较重量级的,所以线程的启动或者销毁是很消耗服务器的资源的,即使使用线程池来实现,使用上述传统的 Socket 方式,当连接数极具上升也会带来性能瓶颈,原因是线程的上线文切换开销会在高并发的时候体现的很明显,并且以上操作方式还是同步阻塞式的编程,性能问题在高并发的时候就会体现的尤为明显。
服务器通过一个Acceptor线程负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。典型的一请求一应答模式。
若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。后改良为用线程池的方式代替新增线程,被称为伪异步IO。服务器提供IP地址和监听的端口,客户端通过TCP的三次握手与服务器连接,连接成功后,双放才能通过套接字(Stock)通信。
b.NIO多路复用
NIO 是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。
客户端和服务器之间通过Channel通信。NIO可以在Channel进行读写操作。这些Channel都会被注册在Selector多路复用器上。
Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。
// NIO 多路复用ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());threadPool.execute(new Runnable() { @Override public void run() { try (Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) { serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); // 阻塞等待就绪的Channel Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) { channel.write(Charset.defaultCharset().encode("你好,世界")); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } }});// Socket 客户端(接收信息并打印)try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s));} catch (IOException e) { e.printStackTrace();}
-
首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色;
-
然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求;
-
为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常;
-
Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒;
c.AIO版
用户进程发起读取请求后立马返回,当数据完全拷贝到用户空间后通知用户直接使用数据。
Java 1.7 提供了 AIO 实现的 Socket 是这样的,如下代码:
// AIO线程复用版Thread sThread = new Thread(new Runnable() { @Override public void run() { AsynchronousChannelGroup group = null; try { group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4)); AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); server.accept(null, new CompletionHandler() { @Override public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) { server.accept(null, this); // 接收下一个请求 try { Future f = result.write(Charset.defaultCharset().encode("你好,世界")); f.get(); System.out.println("服务端发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); result.close(); } catch (InterruptedException | ExecutionException | IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) { } }); group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } catch (IOException | InterruptedException e) { e.printStackTrace(); } }});sThread.start();// Socket 客户端AsynchronousSocketChannel client = AsynchronousSocketChannel.open();Future future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));future.get();ByteBuffer buffer = ByteBuffer.allocate(100);client.read(buffer, null, new CompletionHandler () { @Override public void completed(Integer result, Void attachment) { System.out.println("客户端打印:" + new String(buffer.array())); } @Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); try { client.close(); } catch (IOException e) { e.printStackTrace(); } }});Thread.sleep(10 * 1000);
转载地址:https://codingchaozhang.blog.csdn.net/article/details/110295513 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!