download:Kubernetes 零碎精讲 Go 语言实战 K8S 集群可视化
摘要
次要介绍计算机网络传输一些基础的概念与原理学识,帮助更好的对 java 网络编程有一个明显的认知和学习。本博文将介绍一下有对于计算机网络相干学识和 NIO 相干学识。
NIO 通信模型基础概念
阻塞(Block)与非阻塞(Non-Block)
阻塞和非阻塞是过程在拜访数据的时候,数据是否准备就绪的一种处理形式,当数据没有筹备的时候。
阻塞:经常需要等待缓冲区中的数据筹备好过后才处理其余的事件,否则一直等待在那里。
非阻塞: 当咱们的过程拜访咱们的数据缓冲区的时候,如果数据没有筹备好则间接返回,不会等待。如果数据已经筹备好,也间接返回。
阻塞 IO:
非阻塞 IO:
同步(Synchronous)与异步(Asynchronous)
同步和异步都是基于应用程序和操作系统处理 IO 事件所采纳的形式。比如
同步:是应用程序要直接参与 IO 读写的操作。
异步:所有的 IO 读写交给操作系统去处理,应用程序只需要等待告诉。
同步形式在处理 IO 事件的时候,必须阻塞在某个方法下面等待咱们的 IO 事件实现(阻塞 IO 事件或者通过轮询 IO 事件的形式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,咱们可能去做其余的事件,并不需要去实现真正的 IO 操作,当操作实现 IO 后,会给咱们的应用程序一个告诉。所以异步相比较于同步带来的间接好处就是在咱们处理 IO 数据的时候,异步的形式咱们可能把这部分等待所消耗的资源用于处理其余事务,晋升咱们服务自身的性能。
同步 IO:
异步 IO:
BIO(传统 IO):
BIO 是一个同步并阻塞的 IO 模式,传统的 java.io 包,它基于流模型实现,提供了咱们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的形式,也就是说,在读取输出流或者写入输入流时,在读、写动作实现之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性次序。
NIO(Non-blocking/New I/O)
NIO 是一种同步非阻塞的 I/O 模型,于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可能理解为 Non-blocking,不单纯是 New。它反对面向缓冲的,基于通道的 I/O 操作方法。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 绝对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现, 两种通道都反对阻塞和非阻塞两种模式。对于高负载、高并发的(网络)利用,应使用 NIO 的非阻塞模式来开发
BIO 与 NIO 的对比
IO 模型
BIO
NIO
通信
面向流
面向缓冲
处理
阻塞 IO
非阻塞 IO
触发
无
选择器
NIO 的 Server 通信的简略模型:
BIO 的 Server 通信的简略模型:
NIO 的个性:
一个线程可能处理多个通道,缩小线程创建数量;
读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费
Reactor 模型原理
单线程的 Reactor 模型
多线程的 Reactor 模型
多线程主从 Reactor 模型
Netty 架构原理
Netty 简介:Netty 是一个 NIO 客户端服务器框架,可疾速轻松地开发网络应用程序,例如协定服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。
Netty 执行流程
Netty 的底层原理
堆栈内存
堆栈内存指的是堆内存和栈内存:堆内存是 GC 治理的内存,栈内存是线程内存。堆内存结构:
还有一个更粗疏的结构图(包含 MetaSpace 还有 code cache):注意在 Java8 当前 PermGen 被 MetaSpace 代替,运行时可主动扩容,并且默认是无限大。
咱们看上面一段代码来简略理解下堆栈的关系:
public static void main(String[] args) {
Object o = new Object();
}
其中 new Object()是在堆下面调配,而 Object o 这个变量,是在 main 这个线程栈下面。
应用程序所有的部分都使用堆内存,而后栈内存通过一个线程运行来使用。
不论对象什么时候创建,他都会存储在堆内存中,栈内存蕴含它的引用。栈内存只蕴含原始值变量好和堆中对象变量的引用。
存储在堆中的对象是全局可能被拜访的,然而栈内存不能被其余线程所拜访。
通过 JVM 参数 -Xmx 咱们可能指定最大堆内存大小,通过 -Xss 咱们可能指定每个线程线程栈占用内存大小
.
堆外内存
狭义的堆外内存:除了堆栈内存,剩下的就都是堆外内存了,包含了 jvm 本身在运行过程中调配的内存,codecache,jni 里调配的内存,DirectByteBuffer 调配的内存等等
广义的堆外内存 – DirectByteBuffer:而作为 java 开发者,咱们常说的堆外内存溢出了,其实是广义的堆外内存,这个次要是指 java.nio.DirectByteBuffer 在创建的时候分配内存,咱们这篇文章里也次要是讲广义的堆外内存,因为它和咱们平时碰到的问题比较密切。
为啥要使用堆外内存。通常因为:
在过程间可能共享,缩小虚拟机间的复制。
对垃圾回收进展的改善:如果利用某些长期存活并大量存在的对象,常常会出发 YGC 或者 FullGC,可能考虑把这些对象放到堆外。过大的堆会影响 Java 利用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统治理 (而不是虚拟机)。这样做的后果就是能保持一个较小的堆内内存,以缩小垃圾收集对利用的影响。
在某些场景下可能晋升程序 I / O 摆布的性能。少去将数据从堆内内存拷贝到堆外内存的步骤。
JNI 调用与内核态及用户态
内核态:cpu 可能拜访内存的所有数据,包含外围设备,例如硬盘,网卡,cpu 也可能将自己从一个程序切换到另一个程序。
用户态:只能受限的拜访内存,且不容许拜访外围设备,占用 cpu 的能力被剥夺,cpu 资源可能被其余程序获取。
零碎调用:为了使下层利用能够拜访到这些资源,内核为下层利用提供拜访的接口。
Java 调用原生方法即 JNI 就是零碎调用的一种。:咱们举个例子,文件读取;Java 本身并不能读取文件,因为用户态没有权限拜访外围设备。需要通过零碎调用切换内核态进行读取。
目前,JAVA 的 IO 形式有基于流的传统 IO 还有基于块的 NIO 形式(诚然文件读取其实不是严格意义上的 NIO,哈哈)。面向流意味着从流中一次可能读取一个或多个字节,拿到读取的这些做什么你说了算,这里没有任何缓存(这里指的是使用流没有任何缓存,接收或者发送的数据是缓存到操作系统中的,流就像一根水管从操作系统的缓存中读取数据)而且只能次序从流中读取数据,如果需要跳过一些字节或者再读取已经读过的字节,你必须将从流中读取的数据先缓存起来。面向块的处理形式有些不同,数据是先被 读 / 写到 buffer 中的,根据需要你可能管制读取什么地位的数据。这在处理的过程中给用户多了一些灵敏性,然而,你需要额定做的工作是查看你需要的数据是否已经全副到了 buffer 中,你还需要保障当有更多的数据进入 buffer 中时,buffer 中未处理的数据不会被覆盖。咱们这里只分析基于块的 NIO 形式,在 JAVA 中这个块就是 ByteBuffer。
零拷贝原理
大部分 web 服务器都要处理大量的动态内容,而其中大部分都是从磁盘文件中读取数据而后写到 socket 中。咱们以这个过程为例子,来看下不同模式下 Linux 工作流程
一般 Read/Write 模式
// 从文件中读取,存入 tmp_buf
read(file, tmp_buf, len);
// 将 tmp_buf 写入 socket
write(socket, tmp_buf, len);
当调用 read 零碎调用时,通过 DMA(Direct Memory Access)将数据 copy 到内核模式。
而后由 CPU 管制将内核模式数据 copy 到用户模式下的 buffer 中。
read 调用实现后,write 调用首先将用户模式下 buffer 中的数据 copy 到内核模式下的 socket buffer 中。
最初通过 DMA copy 将内核模式下的 socket buffer 中的数据 copy 到网卡设施中传送。
从下面的过程可能看出,数据白白从内核模式到用户模式走了一圈,浪费了两次 copy(第一次,从 kernel 模式拷贝到 user 模式;第二次从 user 模式再拷贝回 kernel 模式,即下面 4 次过程的第 2 和 3 步骤。),而这两次 copy 都是 CPU copy,即占用 CPU 资源。
NIO 下的 IO 模式
Zero-Copy 技术省去了将操作系统的 read buffer 拷贝到程序的 buffer,以及从程序 buffer 拷贝到 socket buffer 的步骤,间接将 read buffer 拷贝到 socket buffer. Java NIO 中的 FileChannal.transferTo() 方法就是这样的实现:
public void transferTo(long position,long count,WritableByteChannel target);
1.
transferTo()方法将数据从一个 channel 传输到另一个可写的 channel 上,其外部实现依赖于操作系统对 zero copy 技术的反对。在 unix 操作系统和各种 linux 的发型版本中,这种功能最终是通过 sendfile()零碎调用实现。下边就是这个方法的定义:
include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
BIO 下的代码实现
BIOserver
package com.zhuangxiaoyan.nio.zerocopy;
import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
- @Classname NIOClient
- @Description TODO
- @Date 2021/11/1 7:25
- @Created by xjl
*/
public class BIOServer {
public static void main(String[] args) throws Exception {ServerSocket serverSocket = new ServerSocket(7001);
while (true) {Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {byte[] byteArray = new byte[4096];
while (true) {int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
if (-1 == readCount) {break;}
}
} catch (Exception ex) {ex.printStackTrace();
}
}
}
}
NIOClient
package com.zhuangxiaoyan.nio.zerocopy;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;
/**
- @Classname NIOClient
- @Description TODO
- @Date 2021/11/1 7:25
- @Created by xjl
*/
public class BIOClient {
public static void main(String[] args) throws Exception {Socket socket = new Socket("localhost", 7001);
String fileName = "D:\\softwaresavfile\\Github\\JAVA_NIO\\NIO\\src\\main\\resources\\VSCodeUserSetup-x64-1.61.2.exe";
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}
System.out.println("发送总字节数:" + total + ", 耗时:" + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
inputStream.close();}
}
NIO 下的代码实现
NIOServer
package com.zhuangxiaoyan.nio.zerocopy;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
- @Classname NIOServer
- @Description TODO
- @Date 2021/11/6 11:16
- @Created by xjl
*/
public class NIOServer {
public static void main(String[] args) throws IOException {InetSocketAddress inetSocketAddress = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(inetSocketAddress);
// 创建一个 buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {SocketChannel socketChannel = serverSocketChannel.accept();
int readcount = 0;
while (-1 != readcount) {
try {readcount = socketChannel.read(byteBuffer);
} catch (Exception e) {e.printStackTrace();
}
// 倒带 position = 0 mark 作废
byteBuffer.rewind();}
}
}
}
NIOClient
登录后复制
package com.zhuangxiaoyan.nio.zerocopy;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
- @Classname NIOClient
- @Description TODO
- @Date 2021/11/6 11:22
- @Created by xjl
*/
public class NIOClient {
public static void main(String[] args) throws IOException {SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String fileName = "D:\\softwaresavfile\\Github\\JAVA_NIO\\NIO\\src\\main\\resources\\VSCodeUserSetup-x64-1.61.2.exe";
// 失去一个文件 channel
File file;
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
// 筹备发送
long startTime = System.currentTimeMillis();
// 在 liunx 在使用 transferto 形式可能实现传输 在 window 下调用时候只能发送 8M 如果大于时候就是需要分段进行传输文件
long transfercount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总的字节数 =" + transfercount + "耗时:" + (System.currentTimeMillis() - startTime));
fileChannel.close();}
}