Commit 416890e8 by ClassmateWang

2021-10-31 17:33 更新\Netty\黑马\NIO.md

parent 6126aa86
# 2)实现服务器 # 1)连接到服务器
# 2)实现服务器 # 1)连接到服务器
## 一、服务器套接字 ## 一、使用telnet
每一个服务器程序,比如说一个HttpWeb 服务器都会不间断的执行下面这个循环 > telnet 是windows上基于网络编程的调试工具,windows 可以在控制面版->程序->打开/关闭Windows特性,然后选择Telnet 客户端来开启windows 上telnet
1. 通过输入数据流从客户端接收一个命令(“get me this information”) 可以通过两个基本的实验来认识telnet
2. <u>**解码**</u>这个客户端命令
3. 收集客户端所请求的信息
4. 通过输出数据流**<u>编码</u>**发送信息给客户端
协议在其中就是解码和编码的作用内容。 输入:
```java
public class EchoServer {
public static void main(String[] args) {
/*ServerSocket 用于创建服务端套接字,可主动发送数据的套接字*/
try(ServerSocket serverSocket = new ServerSocket(8189)) {
/*告诉程序不停的等待直到有客户端连接到这个端口*/
try(Socket incoming = serverSocket.accept()) {
/*获取输入输出流*/
/*对于server 来说输入是client 的输出*/
/*server 的输出是client 的输入*/
InputStream inStream = incoming.getInputStream();
OutputStream outStream = incoming.getOutputStream();
/*将输入输出流封装入扫描器和写入器*/
Scanner in = new Scanner(inStream,"utf-8");
PrintWriter out = new PrintWriter(
new OutputStreamWriter(outStream,"utf-8"),
true);
out.println("Hello!Enter BYE to exit!");
boolean flag = false;
while (!flag && in.hasNextLine()){
/*读取用户的输入*/
String line = in.nextLine();
/*输出用户输入的数据*/
System.out.println("用户的输入为:"+line);
out.println("Echo:"+line);
if (line.trim().toLowerCase().equals("bye")){
flag = true;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```shell
telnet time-a.nist.gov 13
``` ```
![image-20211013180100099](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013180100099.png) 得到:
## 二、为多个服务端服务 ![image-20211013155434367](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013155434367.png)
在正常的连接和使用当中,一个程序往往是供给给多个用户去使用的,就是说在互联网中存在:希望多个客户端同时连接到服务器上,而服务器不间断的为客户端提供服务的需求。**<u>实际的服务器如果无法完成这个需求就会出现串行处理用户请求,或者某一用户占用Server 资源过长的情况。</u>** 这里通过telnet 命令来连接远程服务器,这里连接的是国家标准和技术研究所所运维的,使用这个命令后会建立和服务13端口的会话,**<u>而对应的url 也会转换成ip地址129.6.15.28</u>**,随后talent 软件就会发送一个连接请求给地址,**<u>请求一个到端口13的连接</u>**,一旦建立连接,遍会发送回一行数据然后关闭这个连接,而一般的客户端和服务器之间往往会进行更多的会话。
解决办法:`多线程` 输入:
> 在程序建立一个新的套接字连接的时候,当调用accept 方法时候,启动一个新的线程来处理服务器和客户端之间的连接,而主程序返回并等待下一个连接。 ```shell
telnet horstmann.com 80
```
**<u>实现多线程调用:</u>** 返回:
```java 因为这里是访问外网的原因可能迟迟没有给我消息,但是其实我是可以发送一个HTTP 的报文,向这个服务器提交请求
while(true){
Socket incoming = s.accept();
Runnable r = new ThreadedEchoHandler(incoming);
Thread t = new Thread(r); ```http
t.start(); GET / HTTP/1.1
} Host:horstmann.com
blank line
``` ```
里用到的Runnable 接口的实现类,ThreadedEchoHandler,它的run方法中应当**包含与客户端循环通信**代码 样的GET请求服务器会返回我一个HTML 的页面代码
```java ![image-20211013160451201](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013160451201.png)
class ThreadEchoHandler implements Runnable{
....... ![image-20211013160512059](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013160512059.png)
public void run(){
try(InputStream inputStream = incoming.getInputStream();
OutputStream outputStream = incoming.getOutputStream();)
{
process input and send response
}catch(IOException e){
Handle Exception
}
}
}
```
示例: ## 二、Java连接服务器
```java ```java
package Socket; public class Test1 {
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: Socket
* @Author: Wang Haipeng
* @CreateTime: 2021-10-18 08:12
* @Description: Socket服务多个用户端
*/
public class ThreadEchoServer {
public static void main(String[] args) { public static void main(String[] args) {
try(ServerSocket s = new ServerSocket(8189)) { /*创建一个指定地址和端口的套接字,如果创建失败会抛出一个UnknownHostException*/
int i = 1; try (Socket s = new Socket("time-a.nist.gov",13);
Scanner in = new Scanner(s.getInputStream(),"utf-8")
while (true){ ){
/*接收套接字创建连接*/ while (in.hasNextLine()){
Socket incoming = s.accept(); String line = in.nextLine();
System.out.println("Spawning"+i); System.out.println(line);
/*通过Socket 对象创建对应的线程Runnable 对象*/
Runnable r = new ThreadedEchoHandler(incoming);
Thread t = new Thread(r);
t.start();;
i++;
} }
} catch (IOException e) { }catch (IOException e) {
/*如果存在host 不存在之外的异常会抛出一个IOException 异常
* UnknownHostException是这个异常的子异常
* */
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
class ThreadedEchoHandler implements Runnable{ ```
/** 套接字一旦创建成功就会建立与服务器对应的连接,java.net.Socket 类中的getInputStream 方法就会返回一个InputStream 对象,该对象可以像任何一个流对象一样去使用,相应的也可以把其中的信息输出在控制台。
* 通过构造器传递公共数据,持有一个Socket 对象
*/
private Socket incoming;
/** ![image-20211013161701001](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013161701001.png)
* Constructs a handler
* @param incoming Socket
*/
public ThreadedEchoHandler(Socket incoming) {
this.incoming = incoming;
}
@Override > Socket 类非常的易用,因为Java库隐藏了建立网络连接和通过连接发送数据的复杂过程,实际上,java.net 包提供的编程接口与操作文件时使用的接口基本相同。
public void run() { >
> java 支持传输层的TCP协议,也支持UDP协议。
try(InputStream inputStream = incoming.getInputStream(); ### 解决超时
OutputStream outputStream = incoming.getOutputStream()) {
Scanner in = new Scanner(inputStream,"utf-8"); 1、`阻塞读`第一种超时是连接建立成功之后,在有数据可供访问之前,读操作会一直阻塞下去
PrintWriter out = new PrintWriter(
new OutputStreamWriter(outputStream,"utf-8"),
true
);
out.println("Hello! Enter BYE to exit."); ```java
//echo client input Socket s = new Socket(...);
boolean done = false; s.setTimeout(10000);
while (!done && in.hasNextLine()){
String line = in.nextLine();
out.println("Echo" + line);
if (line.trim().toLowerCase().equals("byte")){
done = true;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
``` ```
![image-20211019143710416](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211019143710416.png) 通过socket.setTimeOut方法可以设置套接字的超时时间,如果超过这个时间就会抛出SocketTimeoutException 异常,如果此时读操作和写操作在没有完成前就超时,可以通过捕获这个超时异常做出反应。
2、`阻塞Socket建立`通过Socket 的构造器可以建立与服务器的连接,但是如果一直无法建立连接,就会一直阻塞下去,直到建立了连接。
> 这样通过线程实现多客户端服务的情况只能适用于小的测试,并不能满足高性能服务器的要求,可以使用Java.nio 中一些特性,使得服务器得到更多的吞吐量:因为这里网络的读写都是IO模型,这里用到的方式方法是BIO,也就是同步阻塞的线程模型。 可以通过:
## 三、半关闭 ```java
Socket s =new Socket();
s.connect(new InetSocketAddress(host,port),timeout)
```
> 套接字连接的一端可以终止其输出,同时仍旧可以接收来自另一端的数据 设置超时时间来处理一直无法连接上的问题
适用场景: ## 三、因特网地址
​ 对于写文件来说,写完文件后关闭即可,并不需要考虑响应的问题。但是如果向服务器传输数据,关闭了套接字,那么和服务器之间的连接就会断开,因此就无法读取服务器的响应了。 就是通过域名来获取IP地址,一般的IP的地址是4字节的,而IPV6是16字节的,这时就可以通过**<u>InetAddress类来实现</u>**
​ 这个时候可以通过半关闭的方法来解决上面的问题,通过关闭一个套接字的输出流来表示向服务器发送的数据已经借书,但是必须保持输入流开启的状态。 **<u>只要主机支持IPV6格式的因特网地址,java.net 包也会支持它</u>**
这种情况适用于一站式(one-shot)的服务,例如HTTP服务,再这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。 获取指定域名的IP地址:
```java ```java
try(Socket socket = new Socket(host,port)){ public class Test1 {
Scanner int = new Scanner(socket.getInputStream(),"utf-8"); public static void main(String[] args) throws UnknownHostException {
PrintWriter writer = new PrintWriter(socket.getOutputStream); String host = "www.baidu.com";
write.print(..); /*InetAddress address = InetAddress.getByAddress(host);*/
write.flush(..); /*
socket.shutdownOutput(); 一般的域名只会对应一个一个IP地址
while(in.haxNextLine() != null){ 而一些访问量比较大的域名会对应多个IP地址,从而实现负载均衡
String line = in.nextLine();.... 可以通过上面的方法获取一个IP地址
也可以通过下面的方法获取一个域名对应的多个IP地址
*/
/*InetAddress 的toString方法会会返回一个字符串类型,进而打印会输出对应的ip地址*/
InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses){
System.out.println(address);
}
} }
} }
``` ```
![image-20211013165714501](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013165714501.png)
![image-20211013165834153](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013165834153.png)
![image-20211013165848158](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013165848158.png)
如果想获取本地的IP地址可以通过:
```java
public static void main(String[] args) throws UnknownHostException {
InetAddress address = InetAddress.getLocalHost();
System.out.println(address);
}
```
![image-20211013170019949](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013170019949.png)
前面输出的是主机名而后面输出的是主机的IP地址
![image-20211013170056989](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/image-20211013170056989.png)
## 四、用到的API ## 四、用到的API
`java.net.ServerSocket` `java.netl.Sokcet 类`
- ServerSocket(int port) - Socket(String host,int port)
- 创建一个监听端口的服务器套接字 - 创建一个套接字,指定主机和端口
- Socket accept() - Socket
- 等待连接对象。该方法阻塞当前线程直到建立连接为止,该方法返回一个Socket 对象,程序可以通过这个对象与连接中的客户端进行通信 - 创建一个为连接的套接字
- void close() - InputStream getInputStream()
- 关闭服务器套接字 - OutputStream getOutputStream()
- 获取从套接字中读取数据的流
`java.net.Socket` - void connect(SocketAddress address)
- 将套接字连接到指定的地址
- void shutdownOutput() - void connect(SocketAddress address,int timeout)
- 关闭输出流 - 连接到指定地址,如果给定时间内没有响应,则返回
- void shutdownInput() - void setTimeout(int timeout)
- 关闭输入流 - 设置套接字上的**读请求的阻塞时间**,超出给定时间抛出InterruptedIOException
- boolean isOutputShutdown() - boolean isConnected()
- 输出流是否关闭 - 如果该套接字被连接,则返回true
- boolean isInputShutdown() - boolean isClosed()
- 输入流是否关闭 - 如果该套接字被关闭返回False
`java.net.InetAddress类`
- statci InetAddress getByName(String host)
- statci InetAddress[] getAllByName(String host)
- 给定主机名一个InetAddress 对象,或者一个数组
- static InetAddress getLocalHost()
- 为本机创建一个InetAddress 对象
- byte[] getAddress()
- 返回一个包含数字行地址的字节数组
- IP 地址的字节数组表示
- String getHostAddress()
- 返回值一个十进制组成的字符串,各数字用. 隔开
- 129.6.15.28
- String getHostName()
- 返回主机名
## 五、小结
这里的内容其实是简单的实现了一个网络的客户端,需要注意的是,在创建了和服务器的连接之后,往往服务器不会那么快就断开连接,而是等待再次请求然后才会断开连接,这个就涉及到HTTP协议的一些内容了。
# 4)获取web数 # 4)获取web数
...@@ -120,8 +120,6 @@ URL类和URI类可以获取一定的资源信息,但更多的web 信息就需 ...@@ -120,8 +120,6 @@ URL类和URI类可以获取一定的资源信息,但更多的web 信息就需
URLConnection connection = url.openConnection(); URLConnection connection = url.openConnection();
``` ```
2. 设置请求的消息属性:(这些字段只能在建立连接前进行修改) 2. 设置请求的消息属性:(这些字段只能在建立连接前进行修改)
- setDoInput - setDoInput
...@@ -131,7 +129,7 @@ URL类和URI类可以获取一定的资源信息,但更多的web 信息就需 ...@@ -131,7 +129,7 @@ URL类和URI类可以获取一定的资源信息,但更多的web 信息就需
- 该字段在URLConnection可用于输出时为true,否则为false。默认为false。 - 该字段在URLConnection可用于输出时为true,否则为false。默认为false。
- 生成向服务器发送数的输出流 - 生成向服务器发送数的输出流
- setIfModifiedSince - setIfModifiedSince
- 该字段指示了将放置If-Modified-Since首部字段中的日期(格林威治标准时间1970年1月1日子夜后的毫秒数) - 该字段指示了将放置If-Modified-Since首部字段中的日期(格林威治标准时间1970年1月1日子夜后的毫秒数)
- setUseCaches - setUseCaches
- 该字段指定了是否可以在缓存可用时使用缓存。默认为true,表示缓存将被使用;false表示缓存不被使用。 - 该字段指定了是否可以在缓存可用时使用缓存。默认为true,表示缓存将被使用;false表示缓存不被使用。
- 用于Applet - 用于Applet
......
# 1)NIO # 1)NIO
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
> non-blocking IO 非阻塞IO > non-blocking IO 非阻塞IO
[toc]
## 一、三大组件 ## 一、三大组件
### 1.1Channel & Buffer ### 1.1Channel & Buffer
...@@ -77,6 +79,11 @@ selector 的作用就是配合一个线程来管理多个channel,获取这些c ...@@ -77,6 +79,11 @@ selector 的作用就是配合一个线程来管理多个channel,获取这些c
## 二、ByteBuffer ## 二、ByteBuffer
> 1、减少实际物理读写次数
> 2、缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数
>
> 说白了就是提高读写速度,节省IO次数,因为重新开IO都需要进行系统调用,都是非常消耗资源的
### 2.1ByteBuffer ### 2.1ByteBuffer
**基本使用:** **基本使用:**
...@@ -171,17 +178,818 @@ ByteBuffer 有以下属性: ...@@ -171,17 +178,818 @@ ByteBuffer 有以下属性:
![image-20211025181315081](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025181315081.png) ![image-20211025181315081](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025181315081.png)
读完缓冲区中的数据之后,缓冲区的状态等价于初始状态,这个时候从逻辑上相当于数据已经被清空了 读完缓冲区中的数据之后,缓冲区的状态等价于初始状态,这个时候从逻辑上相当于数据已经被清空了,但是物理上其实并没有说都释放掉或者初始化掉
![image-20211025181348919](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025181348919.png) ![image-20211025181348919](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025181348919.png)
如果没有读完缓冲区的数据,这个时候如果clear 就会将buffer 指针状态初始化,如果不想丢失数据可以用buffer.compact 方法:同时也会将数据切换到写模式 如果没有读完缓冲区的数据,这个时候如果clear 就会将buffer 指针状态初始化,如果不想丢失数据可以用buffer.compact 方法:同时也会将缓冲区切换到写模式
![image-20211025182911428](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025182911428.png) ![image-20211025182911428](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025182911428.png)
### 2.3常见方法
#### 分配空间
```java
/*class java.nio.HeapByteBuffer*/
System.out.println(ByteBuffer buffer = ByteBuffer.allocate(16));
/*class java.nio.DirectByteBuffer*/
System.out.println(ByteBuffer buffer = ByteBuffer.allocateDirect(16));
```
两个方法分别为缓冲区分配空间:
- 分配堆内存作为缓冲区
- 堆内存读写效率较低
- 属于程序内存,会受到GC的影响
- 分配直接内存作为缓冲区
- 读写效率较高,少一次拷贝
- 属于系统内存,不会受到GC的影响
- 因为是系统内存,所以分配效率较低,如果使用不当可能会造成内存泄漏
#### 写数据
两种方法进行写入:
- 调用channel 的read方法
- 调用buffer 的自己put方法
```java
int readBytes = channel.read(buf); //从channel 中读就是写buffer
buffer.put((byte)127)
```
#### 读数据
get 方法会让position 读指针向后走,如果想重复读数据
- 可以调用 rewind 方法将position 置为0
- 或者调用get(int i) 获取索引I的内容,且它不会移动读指针
#### mark&reset
mark 方法记录当前ByteBuffer 的位置
reset 方法将bytebuffer 跳转到标志位
![image-20211025205050376](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025205050376.png)
#### ByteBuffer 与字符串互传
`字符串转ByteBuffer`
![image-20211025205311445](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025205311445.png)
将一个字节数据包装成ByteBuffer
![image-20211025205332461](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025205332461.png)
`ByteBuffer转字符串`
![image-20211025205506388](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025205506388.png)
#### ScatteringReads
`分散读取`
> 需求,现有文件data.txt 文件内容是 onetwothree 要求读到三个ByteBuffer 中
>
> 其中一种方法就是将数据全部读出来让将数据写入到三个不同的ByteBuffer 中
>
> 还有一种思想就是直接在读的时候将读到三个不同的ByteBuffer 中
```
try(FileChannel channel = new RandomAccessFile("word.txt","r").getChannerl()){
ByteBuffer b1 = ByteBuffer.allocate(3);
ByteBuffer b2 = ByteBuffer.allocate(3);
ByteBuffer b3 = ByteBuffer.allocate(3);
channel.read(new ByteBuffer[]{b1,b2,b3});
}
```
#### GatheringWrite
`集中写入`
```java
ByteBuffer b1 = StandardCharsets.UTF_8.encode("hello");
ByteBuffer b2 = StandardCharsets.UTF_8.encode("world");
ByteBuffer b3 = StandardCharsets.UTF_8.encode("你好");
try(FileChannel channel = new RandomAccessFile("word.txt","r").getChannerl()){
channel.write(new ByteBuffer[]{b1,b2,b3});
}catch(IOException e){
}
```
其实也是为了提高速度,可以减少数据在ByteBuffer 中的拷贝,进而提高效率,不管是缓冲区还是这两种方法都是为了提高IO效率
#### 黏包和半包
![image-20211025212221999](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211025212221999.png)
```java
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: netty.t1
* @Author: Wang Haipeng
* @CreateTime: 2021-10-25 21:37
* @Description: 黏包和半包处理
*/
public class Demo1 {
public static void main(String[] args) {
ByteBuffer source = ByteBuffer.allocate(32);
source.put("Hello.world\nim zhangsan\nho".getBytes(StandardCharsets.UTF_8));
split(source);
source.put("w are you\n".getBytes(StandardCharsets.UTF_8));
split(source);
}
private static void split(ByteBuffer source) {
source.flip();
for(int i = 0 ; i <source.limit() ; i++){
/*找到一条完整的消息*/
if (source.get(i) == '\n'){
/*这条完整消息的长度*/
int length = i + 1 - source.position();
ByteBuffer buffer = ByteBuffer.allocate(length);
for (int j = 0 ; j < length ; j++){
buffer.put(source.get());
}
debugAll(buffer);
}
}
source.compact();
}
}
```
#### Buffer转String
![image-20211031133337280](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211031133337280.png)
## 三、文件编程 ## 三、文件编程
> **<u>为什么要用Channel?</u>**
>
> Channel 相对于流的优势
>
> - 可读可写
> - 异步读写
> - 总是基于缓冲区Buffer 来读写
> - 效率比较高
>
> ![image-20211026131108374](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026131108374.png)
>
> ![image-20211026131156526](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026131156526.png)
>
> ![image-20211026131208024](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026131208024.png)
>
> ![image-20211026131217623](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026131217623.png)
### 3.1FileChannel
> 需要注意的是FileChannel 只能工作在阻塞模式下,也就是说并不能和Selector 一起进行使用
不能直接打开FileChannel ,必须通过FileInputStream 、FileOutputStream 或者是RandomAccessFile 来获取,他们都有FileChannel 方法
- 通过FileInputStream 获取的Channel 只能读
- 通过FileOutputStream 获取的channel 只能写
- 通过RandomAccessFile 能否读写根据RandomAccessFile 时的读写模式决定
#### 读取
从channel 中读取数据填充ByteBuffer,返回值表示读到了多少字节,-1表示到达了文件尾
```java
int readBytes = channel.read(buffer);
```
#### 写入
```java
ByteBuffer buffer= 。。;
buffer.put(...);
buffer.flip();
while(buffer.hasRemaining()){
channel.write(buffer);
}
```
#### 关闭
channel 必须关闭,不过调用了FileInputStream FileOutputStream 或者RandomAccessFile的close 方法会间接的调用channel 的 close 方法
#### 位置
获取当前的位置(读写指针的位置)
```java
long pos = channel.position();
```
设置当前位置
```java
long newPos = ...;
channel.position(newPos);
```
设置当前位置,如果设置为文件尾:
- 这时读会返回-1
- 这时写入会追加内容,但是要注意的是如果position 超过了文件尾,再写入新内容和源末尾之间会有空洞(00)
#### 大小
size 方法
#### 强制写入
操作系统出于性能的考虑,会将数据写入到缓存而不是立即写入到磁盘,可以调用force 方法将文件内容和元数据(文件权限等信息)立即写入磁盘
### 3.2两个channel 传输数据
![image-20211026132620970](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026132620970.png)
channel.transferTo 方法:
- param1
- 包含数据的channel的数据起始位置
- param2
- 传输的大小
- param3
- 要传给的channel 对象
效率更高(Java 凡是叫transferTo的方法底层都会调用操作系统的零拷贝进行优化)
<u>**这个方法有一个bug 就是最大的数据传输量是2G**</u>
而解决这个问题的方法也是通用来说就是进行多次传输:
```java
long size = fromChannel.size();
/*left 变量代表传输中还剩余多少字节*/
for(long left = size; left>0;){
/*这个方法的返回值是每次传输的数据量的字节数*/
left -= fromChannel.transferTo((size-left),left,toChannel);
}
```
### 3.3Path
jdk7 引入了Path 和Paths 类
- Path 用来表示文件路径
- Paths 是工具类,用来获取Path 实例
- 获取Path 对象
- ![image-20211026133727571](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026133727571.png)
- `.` 代表当前路径
- `..`代表上一级路径
- ![image-20211026133859479](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026133859479.png)
- normalize 会将相对路径的表示形式表示为绝对路径
### 3.4Files
> 也就是JDK1,7中新增的API
#### 检查文件是否存在
```java
Path path = Paths.get("hello/data.txt");
System.out.println(Files.exists(path));
```
#### 创建一级目录
```java
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
```
- 如果目录已存在会抛出FileAlreadyExistsException
- 不能一次创建多级目录,否则会抛出NoSuchFileException
#### 创建多级目录
```java
Path path = Paths.get("helloworld/d1/d2");
Files.createDirectories(path);
```
#### 拷贝文件
![image-20211026134652475](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026134652475.png)
#### 移动文件
![image-20211026134722003](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026134722003.png)
#### 删除文件
![image-20211026134744961](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026134744961.png)
#### 删除目录
不能进行级联删除,只能删除一个空目录
![image-20211026134806718](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026134806718.png)
#### 遍历文件目录
```java
package netty.t1;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: netty.t1
* @Author: Wang Haipeng
* @CreateTime: 2021-10-26 13:57
* @Description: 遍历文件树
*/
public class TestFilesWalkFileTree {
public static void main(String[] args) throws IOException {
/*
因为匿名类本身在编译时会编程成一个单独的class
所以在匿名类中访问局部变量相当于变量为常量,并不能修改所以这里必须使用AtomicInteger
因为这个类是非阻塞算法实现值修改的,而普通数据类型是通过阻塞算法实现修改的
*/
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
/**
* 这里遍历文件夹是使用的是设计模式中的访问者模式
*/
Files.walkFileTree(Paths.get("C:\\user"),new SimpleFileVisitor<>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("===>"+dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
System.out.println("dir count "+ dirCount);
System.out.println("file count "+ fileCount);
}
}
```
#### 删除多级目录
需要注意的是通过程序代码去删除文件或者文件夹会直接从磁盘上直接删除,并不会放入回收站,所以要注意尽量不要通过这种方式去删除文件
```java
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: netty.t1
* @Author: Wang Haipeng
* @CreateTime: 2021-10-26 13:57
* @Description: 级联删除多级目录
*/
public class TestFilesWalkFileTree {
public static void main(String[] args) throws IOException {
/**
* 这里遍历文件夹是使用的是设计模式中的访问者模式
*/
Files.walkFileTree(Paths.get("C:\\user"),new SimpleFileVisitor<>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("进入文件夹"+dir);
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("删除文件"+file);
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("退出文件夹并删除文件夹"+dir);
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
}
```
#### 拷贝多级目录
![image-20211026143633485](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026143633485.png)
## 四、网络编程 ## 四、网络编程
[(73 封私信 / 81 条消息) 如何学习Java的NIO? - 知乎 (zhihu.com)](https://www.zhihu.com/question/29005375)
### 4.1 非阻塞VS阻塞
大概明白了一些,但是又感觉还是不是很明白:
**<u>IO模型一般分为四种:同步阻塞,同步非阻塞,异步阻塞,和异步非阻塞</u>**
> (1)同步和异步
>
> ​ 同步和异步描述的是一种**消息通知的机制**,<u>**主动等待消息返回还是被动接受消息**</u>。同步io指的是调用方通过主动等待获取调用返回的结果来获取消息通知,而异步io指的是被调用方通过某种方式(如,回调函数)来通知调用方获取消息。
>
> (2)阻塞非阻塞
>
> ​ 阻塞和非阻塞描述的是**调用方在获取消息过程中的状态**,<u>**阻塞等待还是立刻返回**</u>。阻塞io指的是调用方在获取消息的过程中会挂起阻塞,知道获取到消息,而非阻塞io指的是调用方在获取io的过程中会立刻返回而不进行挂起。
**<u>这两种消息通知模式和两种获取消息状态组合起来就是四种IO模型:</u>**
> (1)同步阻塞 IO :
>
> 在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当真正完成了 IO 操作以后,用户进程才能运行。 JAVA传统的 IO 模型属于此种方式!
>
> (2)同步非阻塞 IO:
>
> 在此种方式下,用户进程发起一个 IO 操作以后 边可 返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。其中目前 JAVA 的 NIO 就属于同步非阻塞 IO 。
>
> (3)异步阻塞 IO :
>
> **此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,**那么为什么说是阻塞的呢?因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,**而采用 select 函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!**
>
> (4)异步非阻塞 IO:
>
> 在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为 真正的 IO读取或者写入操作已经由 内核完成了。目前 Java 中还没有支持此种 IO 模型。
>
>
对于NIO来说,select 本身还是同步的,因为需要主动的另起一个线程去监听系统内存中有没有数据,并不会被动的等到IO操作完成的通知
核心要点:
> 1. IO操作分为准备数据和复制数据从系统空间到用户空间两步,阻塞与非阻塞主要是在第一步请求准备数据上是不是会阻塞用户进程
> 2. IO操作主要是点是操作系统在系统空间中完成的
#### 阻塞模式
`Server`
```java
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: netty.t2
* @Author: Wang Haipeng
* @CreateTime: 2021-10-26 14:45
* @Description: 使用NIO理解阻塞模式
*/
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(16);
/*1、创建服务器*/
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
/*2、绑定端口*/
serverSocketChannel.bind(new InetSocketAddress(8080));
List<SocketChannel> list = new ArrayList<>();
while (true){
/*3、建立与客户端的连接,SocketChannel 用来与客户端通信*/
log.debug("connecting.....");
/*accept 是阻塞方法意味着此时线程停止运行*/
SocketChannel accept = serverSocketChannel.accept();
log.debug("connected...{}",accept);
list.add(accept);
/*4、接收客户端发生的数据*/
for (SocketChannel channel : list){
/*read 也是阻塞方法,也会让线程停止下来,read 这里会一直阻塞直到客户端发送数据*/
log.debug("before read....{}",channel);
/*将客户端发来的数据写入到buffer*/
channel.read(buffer);
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read....{}",channel);
}
}
}
}
```
`Client`
```java
package netty.t2;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: netty.t2
* @Author: Wang Haipeng
* @CreateTime: 2021-10-26 15:00
* @Description: 客户端
*/
public class Client {
public static void main(String[] args) throws IOException {
//1、创建客户端连接通道
SocketChannel socketChannel = SocketChannel.open();
//2、客户端建立连接
socketChannel.connect(new InetSocketAddress("localhost",8080));
/*向服务器发送数据*/
socketChannel.write(Charset.defaultCharset().encode("Hello"));
//3、保证连接不断开
System.out.println("waiting....");
}
}
```
阻塞模式如果Socket 建立被阻塞掉,则程序是无法继续向下执行的,这样一个的阻塞整个线程都会被阻塞掉。
其次单线程每次只能处理一个Socket连接,这个Sokcet 连接read 被阻塞,意味着之后的所有连接都会被阻塞掉
单线程在处理方法上需要串行运行,在处理连接上也需要串行运行
#### 非阻塞模式
将服务器的状态修改为非阻塞模式:
这个时候ServerSocketChannel 在创建连接的时候虽然没有建立连接,但是线程仍然会继续向下运行,此时SocketChannel 对象返回值为null(非阻塞模式会抛出异常,返回原线程继续向下执行),这样就是解决了在建立Socket 时阻塞的问题
```java
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(16);
/*1、创建服务器*/
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
/*将当前模式调整为非阻塞模式*/
serverSocketChannel.configureBlocking(false);
/*2、绑定端口*/
serverSocketChannel.bind(new InetSocketAddress(8080));
List<SocketChannel> list = new ArrayList<>();
while (true){
/*3、建立与客户端的连接,SocketChannel 用来与客户端通信*/
log.debug("connecting.....");
/*在非阻塞模式下,没有建立连接线程并不会停止运行,这时accept == null*/
SocketChannel accept = serverSocketChannel.accept();
log.debug("connected...{}",accept);
}
}
}
```
![image-20211027092802374](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211027092802374.png)
将SocketChannel 修改为非阻塞的:
```java
/*在非阻塞模式下,没有建立连接线程并不会停止运行,这时accept == null*/
SocketChannel accept = serverSocketChannel.accept();
if (accept != null){
log.debug("connected...{}",accept);
/*将SocketChannel 每个连接对象设置为非阻塞*/
accept.configureBlocking(false);
list.add(accept);
}
```
SokcetChannel 设置为非阻塞后在read 方法处将不再阻塞继续向下运行,这时如果read 没有读到数据会返回0
> ServerSocket 线程阻塞是指,当前如果没有accept 到连接就会一直阻塞下去,直到连接建立成功
```java
/*4、接收客户端发生的数据,这里做的是不停的轮询channel是否有数据*/
for (SocketChannel channel : list){
/*read 也是阻塞方法,也会让线程停止下来,read 这里会一直阻塞直到客户端发送数据*/
/*将客户端发来的数据写入到buffer*/
int read = channel.read(buffer);
if (read > 0){
buffer.flip();
debugRead(buffer);
buffer.clear();
log.debug("after read....{}",channel);
}
```
非阻塞模式下因为不会因为读不到数据而阻塞线程,所以单线程还是能处理,这部分不仅是在处理答应输出读到的数据,这里不停的循环也是单线程不停的轮询各个socket,看看是否有数据。
> 阻塞模式下的单线程没有建立连接就不能向下运行,此时再一个客户端要建立连接也是不行的,非阻塞模式下建立连接和读数据都可以继续向下进行,但是响应的将这些socket 放进来的时候要记录这几个socket,不停的去轮询是否有新的Socket建立了连接,或者是否有新的数据写到了channel,没有能继续运行才是客户端能不停被处理的原因。
这时其实阻塞和非阻塞之间的优缺点能看的很清楚,其实如果没有建立连接,我的线程其实就不用继续向下运行的,多个socket 之间没有写入数据的socket 没有必要每次都被访问,那么这个时候如果能将遍历时间和发生事件分开,socket就是将发生了需要处理事件的时候可以通知线程去处理他,但是如果没有发生就可以让线程去处理别人。
### 4.2多路复用器selector
> 多路复用器的核心功能在于选择已经就绪的任务
Selector 不断的轮询注册在其上面的channel ,如果某个channel 上有新的TCP连接接入,读或者写的事件,那么这个channel 就会处于`就绪状态`,会被selector 轮询出来,然后通过SelectionKet 获取就绪Channel 的集合然后进行后续的IO操作。
实际上一个selector 能够处理多个Channel,因为JDK使用epoll 代替传统的select实现,所以selector没有最大连接的限制,理论上一个线程的selector 可以接入成千上万的客户端。
> epoll 代替select:?
>
> //todo: 什么是poll?什么是epoll?
**<u>事件的类型:</u>**
- accept
- 服务器有连接请求的时候触发
- connect
- 客户端 与服务器建立好连接则出发
- read
- 可读事件
- write
- 可写事件
对于ServerSocketChannel 其实关注accept 请求即可,而SocketChannel 其实关注read 和 write 事件即可
#### accept事件处理
```java
/**
* @BelongsProject: JavaLearnAll
* @BelongsPackage: netty.t2
* @Author: Wang Haipeng
* @CreateTime: 2021-10-26 14:45
* @Description: 通过ServerSocketChannel 理解Selector.accept 事件
*/
@Slf4j
public class Server {
public static void main(String[] args) throws IOException {
/*1、创建Selector管理多个Channel*/
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
/*设置服务器为非阻塞模式*/
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
/*2、建立Selector 和 Channel 的联系
* SelectionKey 就是标识Channel 并且能获取发生的事件以及对应channel 的信息
* */
SelectionKey sscKey = serverSocketChannel.register(selector,0,null);
log.info("register key:{}",sscKey);
/*表示ServerSocketChannel 只关注accept 事件*/
sscKey.interestOps(SelectionKey.OP_ACCEPT);
while (true){
/*3、select 方法,表示没有事件发生的时候会将线程进行阻塞*/
selector.select();
/*4、处理事件,内部包含了所有发生的事件*/
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
log.info("key:{}",key);
/*拿到触发事件的channel,其实这里就是上面的ServerSocketChannel*/
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
/*建立连接*/
channel.accept();
}
}
}
}
```
ServerSocketChannel 建立之后被注册到了selector 中,从而获取到了SelectorKey
注册时设置属性当发生accept事件时才会触发
迭代selector的selectorkey,只有发生了事件的key 会放到返回值的set中
<u>**在事件未处理时是不会被阻塞的,只要拿到这个事件,但是并没有对这个事件做任何的操作,那么之后selector 在工作的时候就会将数据不停的返还给你,但是如果出于某种原因不想处理这个事件,就可以通过selectionKey.cancel()方法消除事件,也就是说事件发生后要么处理要么取消不能不去处理**</u>
![image-20211030161727692](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211030161727692.png)
#### read 事件的处理
```java
while (true){
/*3、select 方法,表示没有事件发生的时候会将线程进行阻塞*/
selector.select();
/*4、处理事件,内部包含了所有发生的事件*/
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iteratot.rem
/*5、区分事件类型*/
if (key.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
SelectionKey scKey = socketChannel.register(selector,0,null);
scKey.interestOps(SelectionKey.OP_READ);
}else if (key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
socketChannel.read(buffer);
buffer.flip();
debugRead(buffer);
buffer.clear();
}
}
}
```
需要去区分不同的事件类型进行不同的处理,当接收到accept 事件的时候显然是需要建立连接的channel 并将channel 注册selector,但是如果是read 事件的时候,说明触发的肯定是SocketChannel,所以响应的要将数据读进来
#### remove key
![image-20211030165115935](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211030165115935.png)
当事件发生时会将事件对应的key放入到set 集合中,但是这个set 并不会主动的去删除处理或者没有处理过的key,所以当处理完事件之后要去主动的删除这个集合中的key
所以这样的话在第二次循环迭代set 的时候因为这个事件已经被处理过了,并没有新的事件发生,但是索引key 还留在这里就会出错
```java
while (true){
/*3、select 方法,表示没有事件发生的时候会将线程进行阻塞*/
selector.select();
/*4、处理事件,内部包含了所有发生的事件*/
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
/*处理key时要从selectKeys中删除,否则下次处理就会有问题*/
/**
* 从底层集合中移除此迭代器返回的最后一个元素(可选操作)。 每次调用next只能调用此方法一次
*/
iterator.remove();
/*5、区分事件类型*/
if (key.isAcceptable()){
```
#### 客户端断开的处理
![image-20211031131811062](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211031131811062.png)
在运行的过程中如果关闭客户端就会造成服务器无法再从客户端中读取数据,这样就会导致一个服务器一场最终导致关闭服务器,而这就是我们要处理的,但客户端强行的关闭连接时,服务器面对连接的断开应该如何去处理?
```java
try {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
socketChannel.read(buffer);
buffer.flip();
debugRead(buffer);
buffer.clear();
}catch (IOException e){
e.printStackTrace();
/*因为客户端主动断开,所以要将key取消(从selector 集合中真正的删除,而不是只在事件中删除)掉*/
key.cancel();
}
```
**<u>如果客户端不是强制断开是自然断开,serverSocket.close()</u>**
情况是**不管是正常断开还是强制断开都会触发一个读事件**,但是强制断开会进入到catch 块中,但是正常断开并不会进入到catch 块中,而是将read 方法的返回值设为-1
所以也应该针对这种情况进行处理,将key 取消掉:
![image-20211031132931829](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211031132931829.png)
> key.cancel 是从注册的集合中进行删除而不仅仅是从事件set 中删除
#### 处理消息边界
### 4.3NIO的server
![image-20211030151924162](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211030151924162.png)
## 五、NIO和BIO ## 五、NIO和BIO
# 小结
阅读:
[(73 封私信 / 81 条消息) 如何学习Java的NIO? - 知乎 (zhihu.com)](https://www.zhihu.com/question/29005375)
JDK 1.4中的`java.nio.*包`中引入新的Java I/O库,其目的是**提高速度**。实际上,“旧”的I/O包已经使用NIO**重新实现过,即使我们不显式的使用NIO编程,也能从中受益**
使用过NIO重新实现过的**传统IO在传输文件的时候根本不虚**,在大文件下效果还比NIO要好(当然了,个人几次的测试,或许不是很准)
IO操作往往在**两个场景**下会用到:
- 文件IO
- 网络IO
![image-20211026150611966](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026150611966.png)
- 可简单认为:**IO是面向流的处理,NIO是面向块(缓冲区)的处理**
- - 面向流的I/O 系统**一次一个字节地处理数据**
- 一个面向块(缓冲区)的I/O系统**以块的形式处理数据**
- NIO在处理文件的时候仍是阻塞的
在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道**配合使用**来处理数据。
简单理解一下:
- Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
而我们的NIO就是**通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理**
- 要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
- - **Channel-->运输**
- **Buffer-->数据**
相对于传统IO而言,**流是单向的**。对于NIO而言,有了Channel管道这个概念,我们的**读写都是双向**的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)!
Channel通道**只负责传输数据、不直接操作数据的**。操作数据都是通过Buffer缓冲区来进行操作!
![image-20211026150800451](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026150800451.png)
### 2.1.4直接与非直接缓冲区
- 非直接缓冲区是**需要**经过一个:copy的阶段的(从内核空间copy到用户空间)
- 直接缓冲区**不需要**经过copy阶段,也可以理解成--->**内存映射文件**,(上面的图片也有过例子)。
![img](https://pic2.zhimg.com/50/v2-b75abc108506e1f48abbba0f91b80fdc_720w.jpg?source=1940ef5c)
![img](https://pica.zhimg.com/50/v2-51ec191305139e98a3b43d82c8ba17b1_720w.jpg?source=1940ef5c)
使用直接缓冲区有两种方式:
- 缓冲区创建的时候分配的是直接缓冲区
- 在FileChannel上调用`map()`方法,将文件直接映射到内存中创建
![img](https://pic1.zhimg.com/50/v2-d0a5dff46ec4a682125d210be9e945a7_720w.jpg?source=1940ef5c)![img](http
根据UNIX网络编程对I/O模型的分类,**在UNIX可以归纳成5种I/O模型**
- **阻塞I/O**
- **非阻塞I/O**
- **I/O多路复用**
- 信号驱动I/O
- 异步I/O
Linux 的内核将所有外部设备**都看做一个文件来操作**,对一个文件的读写操作会**调用内核提供的系统命令(api)**,返回一个`file descriptor`(fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为`socket fd`(socket文件描述符),描述符就是一个数字,**指向内核中的一个结构体**(文件路径,数据区等一些属性
- 所以说:在Linux下对文件的操作是**利用文件描述符(file descriptor)来实现的**
IO在系统中的运行是怎么样的(我们**以read为例**)
![image-20211026152024092](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026152024092.png)
当应用程序调用read方法时,是需要**等待**的--->从内核空间中找数据,再将内核空间的数据拷贝到用户空间的。
## 3.1阻塞I/O模型
在进程(用户)空间中调用`recvfrom`,其系统调用直到数据包到达且**被复制到应用进程的缓冲区中或者发生错误时才返回**,在此期间**一直等待**
![image-20211026152125674](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026152125674.png)
## 3.2非阻塞I/O模型
`recvfrom`从应用层到内核的时候,如果没有数据就**直接返回**一个EWOULDBLOCK错误,一般都对非阻塞I/O模型**进行轮询检查这个状态**,看内核是不是有数据到来。
![img](https://pic2.zhimg.com/50/v2-d1ea36da880455688519e6f023c5ec57_720w.jpg?source=1940ef5c)
## 3.3I/O复用模型
前面也已经说了:在Linux下对文件的操作是**利用文件描述符(file descriptor)来实现的**
在Linux下它是这样子实现I/O复用模型的:
- 调用`select/poll/epoll/pselect`其中一个函数,**传入多个文件描述符**,如果有一个文件描述符**就绪,则返回**,否则阻塞直到超时。
比如`poll()`函数是这样子的:`int poll(struct pollfd *fds,nfds_t nfds, int timeout);`
其中 `pollfd` 结构定义如下:
```c
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
};
```
![img](https://pic1.zhimg.com/50/v2-f2c7c69788d159dbb7141f48c6589ff9_720w.jpg?source=1940ef5c)
![img](https://pic3.zhimg.com/50/v2-501d87cb78ad37dc75da447e191619ce_720w.jpg?source=1940ef5c)
- (1)当用户进程调用了select,那么整个进程会被block;
- (2)而同时,kernel会“监视”所有select负责的socket;
- (3)当任何一个socket中的数据准备好了,select就会返回;
- (4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程(空间)。
- 所以,I/O 多路复用的特点是**通过一种机制一个进程能同时等待多个文件描述符**,而这些文件描述符**其中的任意一个进入读就绪状态**,select()函数**就可以返回**
select/epoll的优势并不是对于单个连接能处理得更快,而是**在于能处理更多的连接**
## 3.4I/O模型总结
NIO被叫为 `no-blocking io`,其实是在**网络这个层次中理解的**,对于**FileChannel来说一样是阻塞**
![image-20211026152419299](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026152419299.png)
**通常**使用NIO是在网络中使用的,网上大部分讨论NIO都是在**网络通信的基础之上**的!说NIO是非阻塞的NIO也是**网络中体现**的!
从上面的图我们可以发现还有一个`Selector`选择器这么一个东东。从一开始我们就说过了,nio的**核心要素**有:
- Buffer缓冲区
- Channel通道
- Selector选择器
我们在网络中使用NIO往往是I/O模型的**多路复用模型**
![image-20211026152451512](https://wangnotes.oss-cn-beijing.aliyuncs.com/notesimage/image-20211026152451512.png)
- 将Socket通道注册到Selector中,监听感兴趣的事件
- 当感兴趣的时间就绪时,则会进去我们处理的方法进行处理
- 每处理完一次就绪事件,删除该选择键(因为我们已经处理完了)
NIO是指同步非阻塞,是对BIO(同步阻塞)的改进,它的代码在java.nio及其子包下。
## **IO**
IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<-->内核空间、内核空间<-->设备空间(磁盘、网络等)。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。
对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
所以,对于一个网络输入操作通常包括两个不同阶段:
- 等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
- 从内核缓冲区复制数据到进程空间。
## **2、5种IO模型**
《UNIX网络编程》说得很清楚,5种IO模型分别是阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型、异步IO模型;前4种为同步IO操作,只有异步IO模型是异步IO操作。
下面这样些图,是它里面给出的例子:接收网络UDP数据的流程在IO模型下的分析,在它的基础上再加以简单描述,以区分这些IO模型。
### **2-1、阻塞IO模型**
> 我等到你好了我再继续
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。
**1、典型应用:阻塞socket、Java BIO;**
**2、特点:**
- 进程阻塞挂起不消耗CPU资源,及时响应每个操作;
- 实现难度低、开发应用较容易;
- 适用并发量小的网络应用开发;
不适用并发量大的应用:因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大。
### **2-2、非阻塞IO模型**
> 我不等你好没好,但是我会不停的问你
进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
**<u>由进程不断的轮询去问是否准备好了数据</u>**
对于上面的阻塞IO模型来说,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
1、典型应用:socket是非阻塞的方式(设置为NONBLOCK)
2、特点:
- 进程轮询(重复)调用,消耗CPU的资源;
- 实现难度低、开发应用相对阻塞IO模式较难;
- 适用并发量较小、且不需要及时响应的网络应用开发;
### **2-3、IO复用模型**
> 我和几个人借助一个东西来告诉我们好没好
多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;
**<u>由select 决定,数据准备好了select 就调用对应的进程</u>**
如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;
而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。
可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。
1、典型应用:select、poll、epoll三种方案,nginx都可以选择使用这三个方案;Java NIO;
2、特点:
- 专一进程解决多个进程IO的阻塞问题,性能好;Reactor模式;
- 实现、开发应用难度较大;
- 适用高并发服务应用开发:一个进程(线程)响应多个请求;
3、select、poll、epoll
- Linux中IO复用的实现方式主要有select、poll和epoll:
- Select:注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_SIZE;
- Poll:原理和Select相似,没有数量限制,但IO数量大扫描线性性能下降;
- Epoll :事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持;
### **2-4、信号驱动IO模型**
> 你好了你就给我发个消息
**<u>通过互相发送信号进行确定的模式,需要系统主动发信号</u>**
当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
特点:回调机制,实现、开发应用难度大;
### **2-5、异步IO模型**
当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。
1、典型应用:JAVA7 AIO、高性能服务器应用
2、特点:
- 不阻塞,数据一步到位;Proactor模式;
- 需要操作系统的底层支持,LINUX 2.5 版本内核首现,2.6 版本产品的内核标准特性;
- 实现、开发应用难度大;
- 非常适合高性能高并发应用;
## **3、IO模型比较**
### **异步IO不清楚!**
### **3-1、阻塞IO调用和非阻塞IO调用、阻塞IO模型和非阻塞IO模型**
注意这里的阻塞IO调用和非阻塞IO调用不是指阻塞IO模型和非阻塞IO模型:
- 阻塞IO调用 :在用户进程(线程)中调用执行的时候,进程会等待该IO操作,而使得其他操作无法执行。
- 非阻塞IO调用:**<u>在用户进程中调用执行的时候,无论成功与否,该IO操作会立即返回</u>**,之后进程可以进行其他操作(当然如果是读取到数据,一般就接着进行数据处理)。
这个直接理解就好,进程(线程)IO调用会不会阻塞进程自己。所以这里两个概念是相对调用进程本身状态来讲的。
从上面对比图片来说,阻塞IO模型是一个阻塞IO调用,而非阻塞IO模型是多个非阻塞IO调用+一个阻塞IO调用,因为多个IO检查会立即返回错误,不会阻塞进程。
而上面也说过了,非阻塞IO模型对于阻塞IO模型来说区别就是,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
### **3-2、同步IO和异步IO**
- 同步IO:导致请求进程阻塞,直到I/O操作完成。
- 异步IO:不导致请求进程阻塞。
上面两个定义是《UNIX网络编程 卷1:套接字联网API》给出的。这不是很好理解,我们来扩展一下,先说说同步和异步,同步和异步关注的是双方的消息通信机制:
- 同步:双方的动作是经过双方协调的,步调一致的。
- 异步:双方并不需要协调,都可以随意进行各自的操作。
这里我们的双方是指,用户进程和IO设备;明确同步和异步之后,我们在上面网络输入操作例子的基础上,进行扩展定义:
- 同步IO:用户进程发出IO调用,去获取IO设备数据,双方的数据要经过内核缓冲区同步,完全准备好后,再复制返回到用户进程。而复制返回到用户进程会导致请求进程阻塞,直到I/O操作完成。
- 异步IO:用户进程发出IO调用,去获取IO设备数据,并不需要同步,内核直接复制到进程,整个过程不导致请求进程阻塞。
所以, 阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型者为同步IO模型,只有异步IO模型是异步IO。
# 小结:
> 大概明白了一些,但是又感觉还是不是很明白
>
> **<u>首先不明白的点就是同步IO和异步IO的区别</u>**
>
> 其次就是在同步IO里面其实是分为四种的:
>
> 阻塞IO
>
> 非阻塞IO
>
> IO复用
>
> 信号驱动
>
> NIO其实是IO复用这种模型,但是为什么说NIO是同步非阻塞模型呢?
>
> IO复用是一种技术,一般的说的IO模型是四种
>
> select 本身还是同步的,因为需要另起一个线程去监听系统内存中有没有数据
>
> 但是异步的则是又系统进程完成将数据复制到用户空间后通知用户线程,这样两个之间的关系是毫不相干的。
>
> 核心要点:
>
> 1. IO操作分为准备数据和复制数据从系统空间到用户空间两步,阻塞与非阻塞主要是在第一步请求准备数据上是不是会阻塞用户进程
> 2. IO操作是操作系统完成的
# 概述
​ java nio可以被称为java new io,因为其提供了一种有别于传统java io的io工作方式。同时,由于java nio是同步非阻塞的,其也被称为non-blocking io(非阻塞io)。但是,为什么java nio是同步非阻塞的?本文将对这个问题进行深入的解析。
# 概念解析
​ 在对本文的问题进行分析之前,我们先看下同步异步,阻塞非则塞者两对概念。
(1)同步和异步
​ 同步和异步描述的是一种消息通知的机制,主动等待消息返回还是被动接受消息。同步io指的是调用方通过主动等待获取调用返回的结果来获取消息通知,而异步io指的是被调用方通过某种方式(如,回调函数)来通知调用方获取消息。
(2)阻塞非阻塞
​ 阻塞和非阻塞描述的是调用方在获取消息过程中的状态,阻塞等待还是立刻返回。阻塞io指的是调用方在获取消息的过程中会挂起阻塞,知道获取到消息,而非阻塞io指的是调用方在获取io的过程中会立刻返回而不进行挂起。
# 为什么java nio是同步非阻塞的?
​ 我们知道java nio是基于io多路复用模型,也就是我们经常提到的select,poll,epoll。io 多路复用本质是同步io,其需要调用方在读写事件就绪时主动去进行读写。在java nio中,通过selector来获取就绪的事件,当selector上监听的channel中没有就绪的读写时间时,其可以直接返回,或者设置一段超时后返回。可以看出java nio可以实现非则塞,而不像传统io里必须则塞当前线程直到可读或可写。所以,java nio可以实现非阻塞。我们简单看下java nio处理连接和java socket 处理连接的方式:
```java
//java nio
while(true) {
......
selector.select(1);
Set<SelectionKey> selectionKeySet= selector.selectedKeys();
......
//处理selectionKeySet中事件,线程没有阻塞
}
//java socket处理连接,线程会阻塞
while(true) {
......
Socket socket = serverSocket.accept();
InputStream in = socket.getInputStream();
......
//处理in中内容
}
```
## 一般来说 I/O 模型可以分为:**同步阻塞,同步非阻塞,异步阻塞,异步非阻塞 四种IO模型**
**同步阻塞 IO :**
在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当真正完成了 IO 操作以后,用户进程才能运行。 JAVA传统的 IO 模型属于此种方式!
**同步非阻塞 IO:**
在此种方式下,用户进程发起一个 IO 操作以后 边可 返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。其中目前 JAVA 的 NIO 就属于同步非阻塞 IO 。
**异步阻塞 IO :**
**此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,**那么为什么说是阻塞的呢?因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,**而采用 select 函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!**
**异步非阻塞 IO:**
在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为 真正的 IO读取或者写入操作已经由 内核完成了。目前 Java 中还没有支持此种 IO 模型。
JAVA NIO是同步非阻塞io。同步和异步说的是消息的通知机制,阻塞非阻塞说的是线程的状态 。
下面说说我的理解,client和服务器建立了socket连接:
1、同步阻塞io:client在调用read()方法时,stream里没有数据可读,线程停止向下执行,直至stream有数据。
> 阻塞:体现在这个线程不能干别的了,只能在这里等着
> 同步:是体现在消息通知机制上的,即stream有没有数据是需要我自己来判断的。
2、同步非阻塞io:调用read方法后,如果stream没有数据,方法就返回,然后这个线程就就干别的去了。
> 非阻塞:体现在,这个线程可以去干别的,不需要一直在这等着
> 同步:体现在消息通知机制,这个线程仍然要定时的读取stream,判断数据有没有准备好,client采用循环的方式去读取,可以看出CPU大部分被浪费了
3、异步非阻塞io:服务端调用read()方法,若stream中无数据则返回,程序继续向下执行。当stream中有数据时,操作系统会负责把数据拷贝到用户空间,然后通知这个线程,这里的消息通知机制就是异步!**而不是像NIO那样,自己起一个线程去监控stream里面有没有数据!**
二、
Selector不是异步的。因为它对IO的读写还是同步阻塞的。只是通过线程复用,将IO的准备时间分离出来。真的进行IO时,还是需要等待的。
异步网络 jdk 7已经有支持。你可以参考 AsynchronousServerSocketChannel。这个才是真正的异步IO
JAVA NIO其内部的IO实现是同步的,采用基于selector实现的事件驱动机制。
这是内部原理。
当外界发起多个线程同时进行IO操作时,所感受到的就是“我使用了多个线程,每个线程的响应不是实时的(由于是事件驱动)”,这样外界可以认为这种方式是异步的。
这是外界认知。
两种描述之所以不同是因为描述的角度不一样。打个比方,单核CPU执行多任务系统,其实是依赖于调度器的,其内部同一时间时间只能执行一个任务,但是外界认为他是“多任务系统”。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment