终于明白了 IO NIO Buffer Socket—— 零基础笔记,基于尚硅谷2021新版教程整理

前言

本文其实就是一个大杂烩,它综合了尚硅谷的IO/NIO、马士兵教育、以及各路网络学习资料(均标注了出处,且非盈利)、一些计算机网络的前置知识、以及一些自己的理解。这个简单的笔记最终目的是旨在理解什么是IO和NIO,奥利给!

基础差的人学习这个需要不少的前置知识,否则看都看不懂。本文的特色之一就是尽可能地介绍了这些前置知识,方便了 非科班/转行人士/大学学渣 理解。

本文于Gitee上建立有配套的代码仓库,方便理解https://gitee.com/da-ji/java-se-demos

一、前置知识

理论基础是下面三篇文章 (全部来源于公众号:码农的荒岛求生) 从底层到上层,讲解清楚IO/NIO :
这是一个系列,从头开始看文章:

前置知识1:进程和线程

1、看完这篇还不懂高并发中的线程与线程池你来打我(内含20张图

对上文链接的读后感:CPU执行只管从寄存器(程序计数器)中拿指令在内存中的地址。从我们写的高级语言程序到CPU的执行过程如下图:
在这里插入图片描述
main函数是一个线程,也是一个进程。是主程序的入口,在程序启动期间就被创建完毕。 当然你也可以创建多个线程。在main线程(也是主进程)启动后,会在程序运行期间创建。因此当线程开始运行的时候这块地址空间就已经存在了,线程可以直接使用。这就是为什么各种教材上提的创建线程要比创建进程快的原因(当然还有其它原因)。

如下图所示,虽然每个线程都共享主进程的地址空间,但是每个线程都有自己各自的栈(可以理解为JVM的栈帧,栈帧里面有函数在被执行的时产生的数据包括函数参数、局部变量、返回地址等信息)

在这里插入图片描述

因此,每个线程创建时,都是借助操作系统完成,操作系统会给线程分配栈等等空间,所以线程的创建和销毁是存在一定的开销的。为了解决这种开销,我们引入了线程池的概念:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前置知识2:IO时底层发生了什么

2、读取文件时,程序经历了什么?

文章读后感:

首先明确,不止是磁盘和内存间的交互(读写文件)叫做IO,计算机网络收发报文也叫做IO。IO的本质是将一侧的数据copy到另一侧(文件将数据copy到内存、A计算机通过网络将数据包copy到B计算机)

任何IO的速度都是奇慢无比的(比起CPU),如何解决这问题?有几种方案,一种是另外开线程,另一种是NIO,另一种是buffer(缓冲区)。

buffer(缓冲区)概念:

在这里插入图片描述

所有IO的本质就是对Buffer的处理,我们把数据放入Buffer供系统写入外部数据,或者从系统Buffer中读取从外部系统中读取的数据。

零拷贝的概念:
在这里插入图片描述

3、终于明白了,一文彻底理解I/O多路复用

二、IO流

https://zhuanlan.zhihu.com/p/25418336

装饰器模式:

IO流的一大堆API其实是装饰器模式的体现:

装饰器模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。
下面代码是为 FileReader 增加缓冲区而采用的装饰类 BufferedReader 的例子:

BufferedReader in = new BufferedReader(new FileReader("filename.txt"));
String s = in.readLine();

在这里插入图片描述

那什么是装饰器模式?

https://mp.weixin.qq.com/s/hLLWmC61FwvtV4VZC57X5g

三、IO(阻塞IO)存在的问题,以及为何引入NIO(非阻塞IO)

在这里插入图片描述
在这里插入图片描述

四、什么是Buffer(概念)?

buffer(缓冲区)概念:

在这里插入图片描述

所有IO的本质就是对Buffer的处理,我们把数据放入Buffer供系统写入外部数据,或者从系统Buffer中读取从外部系统中读取的数据。

后文还会更详细地介绍java.nio.Buffer类,这里介绍的是统一的Buffer概念。

IO的buffer可以使用BufferedReader,也可以使用Byte数组:byte[] buffer = new byte[1024]。NIO的buffer见本文第八章。

五、NIO的通道(Channel)和IO的流

IO面向流(Stream),NIO中的Channel面向缓冲区(buffer)

两者可以说作用是一样的,只是实现上有区别。

通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络 socket。 在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

注意,这里说的是字节,而不是字符。也就是说这个channel可以传任何文件。

补充知识:字节和字符

字节和字符区别,可以去查:字节流可以处理一切文件(网络传输),而字符流只能处理纯文本文件。

补充:
字节(Byte)是计量单位,表示数据量多少,是计算机信息技术用于计量存储容量的一种计量单位,通常情况下一字节等于八位。
字符(Character)计算机中使用的字母、数字、字和符号,比如’A’、‘B’、’$’、’&'等。
UTF-8 编码中,一个英文字为一个字节,一个中文为三个字节。
Unicode 编码中,一个英文为一个字节,一个中文为两个字节。

Java中最基本的两个字节流类是InputStream和OutputStream,而字符流是Writer、Reader这两个类

Channel是全双工的,它既能读,又能写。 以FileChannel为例,有以下两个应用场景:
A、通过FileChannel读取文件中数据到Buffer中
B、通过Buffer向FileChannel写入数据,然后FileChannel将该数据回写给文件

Stream有很多种,涵盖了从文件到网络(上文说过,它们是装饰器模式的体现):
在这里插入图片描述
Channel也有很多种,涵盖了从文件到网络:
在这里插入图片描述

最后,不管是Channel 还是 Stream,本质都是逐层调用了操作系统给我们提供的方法。以文件IO为例,不管是FileInputStream还是FileChannel,最终都是调用了native方法:ReadFIle。

代码实现Gitee(代码位置在文章前言):JavaSE Demos的下图位置

在这里插入图片描述

六、什么是 Socket?

Socket就是套接字。直接说来,就是封装了TCP的三次握手建立连接,四次挥手释放连接的过程。

两台计算机想通信,其中一台负责读数据的,先创建一个socket。然后就只管从那个socket里疯狂的读;至于如何创建三次握手,建立连接,四次挥手释放连接,监听端口号等等操作,统统不用管,都可以在socket中简单配置,socket帮我们做了。

马士兵老师说过:socket帮我们建立了一个四元组:[源IP+port] 和 [目标IP+port] 可以确定绝对唯一的一个连接。如何建立如何释放都不用管,统统交给socket!

Socket是应用层与TCP/IP协议族通信的中间软件抽象层(连接了应用层和传输层),它是一组接口。 在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

普通Socket 和 NIO Socket

代码实现Gitee(代码位置在文章前言):JavaSE Demos的下图位置

下文引自:www.cnblogs.com/blogtech/p/10142212.html
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

七、Channel:Socket通道(其实就是Socket Channel)

注意,socket通道和上文讲的socket不同。它是Channel的一种。
在这里插入图片描述

a.ServerSocketChannel

它是一个基于Channel的socket监听器,它增加了通道语义,因此能在非阻塞模式下运行

代码实现Gitee(代码位置在文章前言):JavaSE Demos的下图位置

在这里插入图片描述

b.SocketChannel

它常配合ServerSocketChannel使用。上面那个ServerSocketChannel只是个监听器,而这个Channel就是操作TCP网络套接字(Socket)的了,它是一个连接到TCP网络套接字的通道。

代码实现Gitee(代码位置在文章前言):JavaSE Demos的下图位置

在这里插入图片描述

在这里插入图片描述

上面说过,Channel的读写是面向缓冲区的,SocketChannel也不例外,必须配合buffer

c.DatagramChannel

上面的SocketChannel是面向Socket的TCP,而DatagramChannel是面向UDP。所以DatagramChannel是无连接的。

DatagramChannel 是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的 socket 不同DatagramChannel 可以发送单独的数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)

代码实现Gitee(代码位置在文章前言):JavaSE Demos的下图位置
在这里插入图片描述

八、Java NIO Buffer详细讲解

代码实现Gitee(代码位置在文章前言):JavaSE Demos的下图位置
在这里插入图片描述

Java NIO Buffer简介

这里的Buffer指的是java.nio.Buffer而不是前面所说的计算机科学领域的Buffer概念。当然NIO的buffer也是计算机科学的Buffer的一种实现罢了。
在这里插入图片描述
在这里插入图片描述

这么多Buffer的儿子,最常用的就是ByteBuffer

Buffer的基本用法

在这里插入图片描述

Buffer的capacity、position、limit三个重要属性

在这里插入图片描述
在这里插入图片描述
关于capacity、position、limit三个重要属性的操作方法(rewind、clear、compact、mark、reset):
在这里插入图片描述
在这里插入图片描述

Buffer的分配,读写数据,flip(读写切换)

在这里插入图片描述
在这里插入图片描述

缓冲区操作

a.缓冲区分片

在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用 slice()方法可以创建一个子缓冲区。

代码实现Gitee(代码位置在文章前言):BufferDemo2 —— b01()

b.只读缓冲区

只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的 asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化.

如果尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException 异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

代码实现Gitee(代码位置在文章前言):BufferDemo2 —— b02()

c.直接缓冲区

直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK 文档中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数据。要分配直接缓冲区,需要调用 allocateDirect()方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别。

代码实现Gitee(代码位置在文章前言):BufferDemo2 —— b03()

d.内存映射文件IO

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多。内存映射文件 I/O 是通过使文件中的数据出现为内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。

代码实现Gitee(代码位置在文章前言):BufferDemo2 —— b04()

九、NIO Seletor(多路复用器)

代码实现Gitee(代码位置在文章前言):如下图所示
在这里插入图片描述

Selector概念简述:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

多路复用器的基本思想:

见第6章:普通Socket和NIO Socket。

简单来说,使用Selector通信,服务端(ServerSocketChannel)和客户端的通道(SocketChannel),可以根据某一个Key,注册到Selector上。
然后Selector可以使用一个while循环进行轮询(类似于事件监听),当有真正的读写操作事件到来时,根据不同的Key,进行不同的处理逻辑。

NIO 编程步骤总结:

第一步:创建 Selector 选择器
第二步:创建 ServerSocketChannel 通道,并绑定监听端口
第三步:设置 Channel 通道是非阻塞模式
第四步:把 Channel 注册到 Socketor 选择器上,监听连接事件
第五步:调用 Selector 的 select 方法(循环调用),监测通道的就绪状况
第六步:调用 selectKeys 方法获取就绪 channel 集合
第七步:遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
第八步:根据业务,决定是否需要再次注册监听事件,重复执行第三步操作

【总结】十:NIO BIO AIO

BIO(同步阻塞)

BIO 其实就是传统的IO , BIO (Blocking I/O) 的B代表Blocking,同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

BIO的特点就是,服务端监听到某一个客户端发起的请求后,就开始处理和响应。这里的处理包括业务逻辑处理,建立Socket,三次握手四次挥手等等,重点是:只要处理不完这个请求,其它的请求都进不来,只能等待该请求处理结束。所以被称作同步阻塞式IO。

我们可以使用多线程、线程池的方法来改善BIO,每次处理一个请求就开一个线程即可。但是就算是使用这种方法,底层仍旧是阻塞IO,只不过加入了并行处理而已。

NIO(同步非阻塞)

详见本文第三章:IO(阻塞IO存在的问题),以及为何引入NIO

NIO是一种同步非阻塞的I/O模型,上面提到了可以用线程池改善BIO,但是线程池终究是数量有限的。比如线程池大小是100,第101个请求到来,线程池仍旧无法处理该请求,该请求也必须阻塞等待。

NIO实现这一机制的核心就是NIO Selector(多路复用器)。IO调用不会被阻塞,反而是在Selector上面注册监听事件,只有实际的IO操作到来,且buffer和channel到位,满足IO条件的时候,才会真正进行IO操作。 从而可以实现一个Selector线程轮询处理多个请求的目的。当然,在Java里面,多个请求被看作是Channel,即相对少线程的Selector可以处理一大堆Channel。

IO面向的是流,NIO面向的是缓冲区(buffer)。

为什么说NIO是非阻塞的:

由于多路复用机制的存在,如果没有侦测到监听事件(读写事件)发生,就会继续轮询。只有监听到真正的IO操作后,才会进行阻塞调用,这在一定程度上防止了阻塞的发生。
在这里插入图片描述

所以NIO不像IO那样会被阻塞。将数据放到缓冲区后,线程可以去做其它的事情。直到另一端将缓冲区取空。

AIO(异步非阻塞)

A 代表 Async , 是异步的意思。这里涉及到知识点同步和异步,以及CallBack回调机制。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会执行回调通知相应的线程进行后续的操作。

猜你喜欢

转载自blog.csdn.net/weixin_44757863/article/details/121306876