☀️javanio?看这一篇就够了!!☀️(代码片段)

经理,天台风好大 经理,天台风好大     2022-12-31     705

关键词:


一、NIO 简介

1.1 NIO 概述

Java NIO(New IO)是从 Java 1.4 版本开始引入的一组新的 IO API(其核心构成有 Channels,Buffers,Selectors 三部分),目的主要是基于这组 API 改善 IO操作性能。

1.2 NIO&IO 分析

1.2.1 IO 操作流程

对于一个 network IO (这里我们以 read 举例),它会涉及到两个系统对象,一个是调用这个 IO 的 process (or thread),另一个就是系统内核(kernel)。当一个 read 操作发生时,该操作会经历两个阶段:

  1. 将数据拷贝到操作系统内核 Buffer。
  2. 将操作系统内核数据拷贝到用户进程。

1.2.2 面向流与面向缓冲区

Java NIO 和 IO 之间第一个最大的区别是:IO 是面向流的,NIO 是面向缓冲区的。例如:

  1. 面向流的操作(输入&输出流操作是单向的

  2. 面向缓冲区的操作:(操作是双向且可移动指针

    说明:
    面向缓冲区的操作时,是缓冲区中的读写模式进行操作,写模式用于向缓冲区写数据,读模式用于从缓冲区读。

1.2.3 阻塞与非阻塞

阻塞和非阻塞的概念描述的是用户线程调用内核 IO 操作的方式。

  1. 阻塞: 是指调用操作需要等待结果的完成,同时会影响后操作的执行。例如阻塞式 IO 操作,用户线程会在内核等待数据以及拷贝数据期间都处于阻塞状态。

    整个 IO 请求的过程中,用户线程是被阻塞的,这导致用户在发起 IO 请求时,不能做任何事情,对 CPU 的资源利用率不够。

    话外音:小李去火车站买票,排队两天买到一张票。

  2. 非阻塞: 是指 IO 操作被调用后立即返回给用户一个状态值,无需等到 IO 操作彻底完成。例如:

    虽然用户线程每次发起 IO 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源。

    话外音:小李去火车站买票,火车站没票,然后每隔 3 小时去火车站问有没有人退票,两天天后买到一张票

    为了避免同步非阻塞 IO 模型中轮询等待的问题,基于内核提供的多路分离函数 select(),可以实现 IO 的多路复用,例如:

    使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    话外音:小李去火车站买票,委托黄牛,然后每隔 1 小时电话黄牛询问,黄牛两天内买到票,然后老李去火车站交钱领票。

    这里的 select 函数是阻塞的,因此多路 IO 复用模型也被称为异步阻塞 IO模型。注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指socket。

1.2.4. 同步与异步

同步和异步的概念描述的是用户线程与内核的交互方式。

  1. 同步是指用户线程发起 IO 请求后需要等待或者轮询内核 IO 操作完成后才能继续执行。

    说明: 这种用户线程轮询的改进方式是 IO 多路复用的实现。

  2. 异步是指用户线程发起 IO 请求后仍继续执行,当内核 IO 操作完成后会通知用户线程,或者调用用户线程注册的回调函数,例如:

    说明:

    异步 IO 的实现需要操作系统的支持,目前系统对异步 IO 的支持并非特别完善,更多的是采用 IO 多路复用模型模拟异步 IO 的方式。

    话外音:小李去火车站买票,给售票员留下电话,有票后,售票员电话通知小李并快递送票上门

二、Buffer 基本应用

2.1 Buffer 概述

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。

Java NIO 里关键的 Buffer 实现:

  1. ByteBuffer
  2. CharBuffer
  3. DoubleBuffer
  4. FloatBuffer
  5. IntBuffer
  6. LongBuffer
  7. ShortBuffer

这些 Buffer 覆盖了你能通过 IO 发送的基本数据类型:byte,short,int,long,float,double 和 char。

说明: 实际的项目中的物理 I/O 操作相比与内存操作要慢数千倍,所以一般为了提高应用程序的性能通常会对数据进行缓存。我们的 JAVA 应用程序和物理磁盘间通常会有多级缓存,例如:

其中:

  1. Disk Drive Buffer(磁盘缓存): 位于磁盘驱动器中的 RAM,将磁盘数据移
    动到磁盘缓冲区是一件相当耗时的操作。
  2. OS Buffer(系统缓存): 操作系统自己缓存,可以在应用程序间共享数据
  3. Application Buffer(应用缓存): 应用程序的私有缓存(JVM进程)。

2.2 Buffer 基本应用

使用 Buffer 读写数据一般遵循以下四个步骤:

  1. 写入数据到 Buffer
  2. 调用 flip() 方法(切换模式)
  3. 从 Buffer 中读取数据
  4. 调用 clear() 方法或者 compact() 方法

当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip()方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear()compact() 方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

@Test
public void testBuffer01()
	//构建一个缓冲区对象(在JVM内存中分配一块区域)
	ByteBuffer buf=ByteBuffer.allocate(1024);
	System.out.println("===数据写入前===");
	doPrint(buf.position(),buf.limit(),buf.capacity());
	//向缓冲区写入数据
	byte []data="hello".getBytes();
	buf.put(data);//放入缓冲区
	System.out.println("===数据写入后===");
	doPrint(buf.position(),buf.limit(),buf.capacity());
	//切换模式(底层同时会移动指针,position位置会发生变换)
	buf.flip();
	System.out.println("===读数据之前===");
	doPrint(buf.position(),buf.limit(),buf.capacity());
	byte c1=buf.get();
	System.out.println((char)c1);
	System.out.println("===读数据之后===");
	doPrint(buf.position(),buf.limit(),buf.capacity());

private void doPrint(int pos,int limit,int cap)
	System.out.println("position:"+pos);
	System.out.println("limit:"+limit);
	System.out.println("capacity:"+cap);

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。

为了理解 Buffer 的工作原理,需要熟悉它的三个属性:

  1. capacity 容量
  2. position 位置
  3. limit 限制

position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管 Buffer处在什么模式,capacity 的含义总是一样的。

Capacity
作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型数据。一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据)才能继续往里写数据。

position
当你写数据到 Buffer 中时,position 表示当前的位置。初始的 position 值 为 0。当一个 byte、long 等数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1.
当读取数据时,也是从某个特定位置读。当将 Buffer 从写模式切换到读模式,position 会被重置为 0. 当从 Buffer 的 position 处读取数据时,position向前移动到下一个可读的位置。

limit
在写模式下,Buffer 的 limit 表示你最多能往 Buffer 里写多少数据。 写模式下,limit 等于 Buffer 的 capacity。
当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据。因此,当切换 Buffer 到读模式时,limit 会被设置成写模式下的 position 值。换句话说,你能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)

三、Channel 基本应用

3.1 Channel 概述

NIO 是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。如图所示:

NIO 中 Channel 的一些具体实现类有:

  1. FileChannel :从文件中读写数据。
  2. DatagramChannel :能通过 UDP 读写网络中的数据。
  3. SocketChannel :能通过 TCP 读写网络中的数据。
  4. ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。

正如你所看到的,这些通道涵盖了 UDP 和 TCP 网络 IO,以及文件 IO。

3.2 FileChannel 基本应用

借助 Channel 对象(FileChannel 类型),从文件读取数据。代码示例:

案例1:

@Test
public void testFileChannel()throws Exception
	//构建一个Buffer对象(缓冲区):JVM内存
	ByteBuffer buf=ByteBuffer.allocate(1024);
	//构建一个文件通道对象(可以读写数据)
	FileChannel fChannel=FileChannel.open(Paths.get("data.txt"),StandardOpenOption.READ);//读模式
	//将文件内容读到缓冲区(ByteBuffer)
	fChannel.read(buf);
	System.out.println("切换buf模式,开始从buf读数据");
	System.out.println(buf.position());
	//从Buffer中取数据
	buf.flip();
	System.out.println(buf.position());
	System.out.println(new String(buf.array()));
	buf.clear();//不是是清除,而将数据标记为脏数据(无用数据)
	//释放资源
	fChannel.close();

案例2:

@Test
public void testFileChannel()throws Exception
	//构建一个Buffer对象(缓冲区):JVM内存
	ByteBuffer buf=ByteBuffer.allocate(2);
	//构建一个文件通道对象(可以读写数据)
	FileChannel fChannel=FileChannel.open(Paths.get("data.txt"),StandardOpenOption.READ);//读模式
	//将文件内容读到缓冲区(ByteBuffer)
	int len=-1;
	do
		len=fChannel.read(buf);
		System.out.println("切换buf模式,开始从buf读数据");
		buf.flip();
		//判定缓冲区中是否有剩余数据
		while(buf.hasRemaining())
			System.out.print((char)buf.get());//每次都1个字节
		
		System.out.println();
		buf.flip();
		buf.clear();//每次读数据应将原数据设置为无效。
	while(len!=-1);
	//释放资源
	fChannel.close();

3.3 SocketChanel 基本应用

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络另一端的通道。可以通过以下 2 种方式创建 SocketChannel:

  1. 客户端打开一个 SocketChannel 并连接到互联网上的某台服务器。
  2. 服务端一个新连接到达 ServerSocketChannel 时,会创建一个SocketChannel。

基于 channel 实现通讯

代码示例:

package com.java.nio;

import org.junit.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * @author archibald
 * @date 2021/9/6
 * @apiNote
 */
public class TestSocketChannel01 

    /**
     * 创建服务
     */
    @Test
    public void testServerSocket() throws Exception 
        //1.创建ServerSocketChannel对象
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //2. 让对象在指定端口上进行监听
        ssc.socket().bind(new InetSocketAddress(8090));
        //3.构建一个buffer对象,用于存储读取的数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //4.等待客户端的连接
        while (true) 
            SocketChannel socketChannel = ssc.accept();
            System.out.println("有客户端来了~~~");
            socketChannel.read(buffer);
            buffer.flip();
            System.out.println("Server: " + new String(buffer.array()));
            socketChannel.close();
        
    

    /**
     * 创建客户端
     */
    @Test
    public void testClientSocket() throws IOException 

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(8090));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put("hello server".getBytes());

        buffer.flip();
        socketChannel.write(buffer);
        socketChannel.close();
    

四、Selector 基本应用

4.1 Selector 概述

Selector 是 Java NIO 中实现多路复用技术的关键,多路复用技术又是提高通讯性能的一个重要因素。项目中可以基于 selector 对象实现了一个线程管理多个 channel 对象,多个网络链接的目的。

例如:在一个单线程中使用一个Selector 处理 3 个 Channel,如图所示:

为什么使用 Selector?

仅用单个线程来处理多个 Channel 的好处是:只用一个线程处理所有的通道,可以有效避免线程之间上下文切换带来的开销,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。

4.2 Selector 基本应用

Selector 的创建

通过调用 Selector.open()方法创建一个 Selector,如下:

Selector selector = Selector.open();

向 Selector 注册通道

为了将 Channel 和 Selector 配合使用,必须将 channel 注册到 selector 上。
通过 SelectableChannel.register()方法来实现,如下:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

channel 与 Selector 一起使用时,Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式。而套接字通道都可以。

注意 register() 方法的第二个参数。这是一个“interest 集合”,意思是在通过 Selector 监听 Channel 时对什么事件感兴趣。可以监听四种不同类型的事件:

  1. Connect
  2. Accept
  3. Read
  4. Write

通道触发了一个事件意思是该事件已经就绪。所以,某个 channel 成功连接到另一个服务器称为“连接就绪”。一个 server socket channel 准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。

这四种事件用 SelectionKey 的四个常量来表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

代码示例如下:

服务端实现:

//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0)
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext())
	//8. 获取准备“就绪”的是事件
	SelectionKey sk = it.next();
	//9. 判断具体是什么事件准备就绪
	if(sk.isAcceptable())
		//10. 若“接收就绪”,获取客户端连接
		SocketChannel sChannel = ssChannel.accept();
		//11. 切换非阻塞模式
		sChannel.configureBlocking(false);
		//12. 将该通道注册到选择器上
		sChannel.register(selector, SelectionKey.OP_READ);
	else if(sk.isReadable())
		//13. 获取当前选择器上“读就绪”状态的通道
		SocketChannel sChannel = (SocketChannel) sk.channel();
		//14. 读取数据
		ByteBuffer buf = ByteBuffer.allocate(1024);
		int len = 0;
		while((len = sChannel.read(buf)) > 0 )
			buf.flip();
			System.out.println(new String(buf.array(), 0, len));
			buf.clear();
		
	
	//15. 取消选择键 SelectionKey
	it.remove();
	

客户端实现:

//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext())
	String str = scan.next();
	buf.put((new Date().toString() + "\\n" + str).getBytes());
	buf.flip();
	sChannel.write(buf);
	buf.clear();

//5. 关闭通道
sChannel.close();

程序运行分析如下:

五、Tomcat 中的 NIO 应用

5.1 Tomcat 核心架构

Tomcat 是一个 apache 推出的一个 web 应用服务器,核心功能就是解析 Http 协议,处理网络 IO 操作,执行 Servlet 对象,其简易架构如下:

其中:

  1. Server:代表整个容器,它可能包含一个或多个 Service 和全局的对象资源;
  2. Service:包含一个或多个 Connector 和一个 Engine,这些 Connector 和 Engine 相关联;
  3. Connector:处理与客户端的通信,网络 I/O 操作;
  4. Engine:表示请求处理流水线(pipeline),它接收所有连接器的请求,并将响应交给适当的连接器返回给客户端;
  5. Host:网络名称(域名)与 Tomcat 服务器的关联,默认主机名 localhost,一个 Engine 可包含多个 Host;
  6. Context:表示一个 Web 应用程序,一个 Host 包含多个上下文。

5.2 Tomcat 中的 NIO 应用配置

Tomcat 中的 NIO 应用要从 Connector 说起,Connector 是请求接收环节与请求处理环节的连接器。具体点说,就是 Connector 将接收到的请求传递给 Tomcat 引擎(Engine)进行处理,引擎处理完成以后会交给 Connector 将其响应到客户端。但是 Connector 本身并不会读写网络中的数据,读写网络中的数据还是要基于网络 IO 进行实现。但使用哪种 IO 模型需要由 Connector 对象进
行指定。

例如:可在 tomcat 的 server.xml 进行配置,其默认配置如下:

<Connector connectionTimeout="20000"
						port="8080"
						protocol查看详情  

redux从入门到进阶,看这一篇就够了!(代码片段)

Redux,带你从入门到进阶🌂序言☂️一、基础知识1、Redux概念简述2、Redux的工作流程🎃二、使用Antd实现TodoList页面布局1、在项目中使用Antd2、使用Antd实现TodoList的基本布局3、创建redux中的store(1)创建storeÿ... 查看详情

redux从入门到进阶,看这一篇就够了!(代码片段)

Redux,带你从入门到进阶🌂序言☂️一、基础知识1、Redux概念简述2、Redux的工作流程🎃二、使用Antd实现TodoList页面布局1、在项目中使用Antd2、使用Antd实现TodoList的基本布局3、创建redux中的store(1)创建storeÿ... 查看详情

❤️学习虚幻引擎这一篇就够了⚡⚡⚡❤️

如果大家有什么资料网站欢迎私信我补充,需要去外网的暂时不加虚幻引擎学习前言¥虚幻引擎官方提供资料¥一、模型方面二、场景制作三,b站上学习前言暂时所有有关虚幻引擎需要的网站都在这里了,随... 查看详情

elasticsearch入门,看这一篇就够了(代码片段)

Elasticsearch入门,看这一篇就够了前言可视化工具kibanakibana的安装kibana配置kibana的启动Elasticsearch入门操作操作index创建index索引别名有什么用删除索引查询索引exist索引操作document插入document查询document删除document更新document使用... 查看详情

handler看这一篇就够了(代码片段)

Handler使用首先来熟悉一下Handler的四种使用方式,如果比较熟悉可以直接跳过:通过sendMessage消息机制来发送sendEmptyMessage(int);//发送一个空的消息sendMessage(Message);//发送消息,消息中可以携带参数sendMessageAtTime(Message,long... 查看详情

最全排序算法及优化,看这一篇就够了(代码片段)

最全排序算法总结看这一篇就够了没有经过总结的知识是沉重的,无用的瞧一瞧~博健的LeetCode题解:Gitbook版本传送门博健的LeetCode题解:CSDN传送门有趣的CSS:Gitbook传送门前端进阶笔记:Gitbook传送门目录... 查看详情

[转]关于深度学习,看这一篇就够了

关于深度学习,看这一篇就够了原文地址:http://www.dlworld.cn/XueXiSuanFa/13.html[日期:2016-04-26]来源:36氪 作者:[字体:大 中 小]    编者按:本文作者王川,投资人,中科大少年班校友,现居加州硅谷,个人微信号... 查看详情

@override看这一篇就够了

一、是什么一句话:它是表示重写的注解@Override注解是伪代码,用于表示被标注的方法是一个重写方法。不写也完全可以,但强烈建议写上! 二、为什么用既然不写@Override也可以重写父类的方法,那为什么非要“多此一... 查看详情

css渐变背景看这一篇就够了(代码片段)

CSS渐变背景看这一篇就够了在我们自己设计网页的时候,为了好看美观,颜色可谓是最让人头疼的一部分。尤其是在配色上又找不到一些好看的网站。今天我就来记录一些好看的渐变式背景,和一些常用的颜色网站。... 查看详情

json入门看这一篇就够了

什么是JSONJSON:JavaScriptObjectNotation【JavaScript对象表示法】JSON是存储和交换文本信息的语法。类似XML。JSON采用完全独立于任何程序语言的文本格式,使JSON成为理想的数据交换语言S为什么需要JSON提到JSON,我们就应该和XML来进行对... 查看详情

java注解看这一篇就够了

注解1.概念注解:说明程序的。给计算机看的注释:用文字描述程序的。给程序员看的注解的定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次... 查看详情

想要弄懂groupby看这一篇就够了(代码片段)

一、前言groupby关键字,不管是工作中还是面试都会经常被用到,所以弄懂它是非常有必要的。要弄懂groupby那我们就得联合着:聚合函数、groupby、having一块讲解。讲之前我们先准备一张表:二、聚合函数为了讲好groupby我们必须... 查看详情

逆转单向链表看这一篇就够了java(代码片段)

逆转单向链表逆转前:1->2->3->4->5->null逆转后:5->4->3->2->1->null个人博客地址:逆转单向链表方法一、循环迭代publicNodereverse(Nodehead)if(head==null||head.next==null)returnhead;//取前面节点Nodepre=head;//取后面 查看详情

json格式对象该怎么传?看这一篇就够了!

一、List<Map>一般会使用在SQL语句的返回结果上代码如下:@Query(value="selectuser_name,user_agefromsys_userwheredept_id=?,nativeQuery=true)List<Map>findUserInfoById(LongdeptId);根据部门的ID查 查看详情

java中的多线程你只要看这一篇就够了

 https://www.cnblogs.com/wxd0108/p/5479442.html  查看详情

jetpackworkmanager看这一篇就够了~

前言最近有读者反馈,在我的新书​​《AndroidJetpack开发:原理解析与应用实战》​​中并没有提及到WorkManager,这是因为目前这个东西在国内并不是很好用。最近因为工作需要正好研究了下,也作为补充章节分享给读者。什么... 查看详情

系统性的学会pandas看这一篇就够了(代码片段)

作者:MaSizhou https://blog.csdn.net/weixin_45901519/article/details/1129808221、Pandas数据结构2008年WesMcKinney开发出的库专门用于数据挖掘的开源python库以Numpy为基础,借力Numpy模块在计算方面性能高的优势基于matplotlib,能够简便的... 查看详情