主要介绍计算机网络传输一些基础的概念与原理知识,帮助更好的对java网络编程有一个清楚的认知和学习。本博文将介绍一下有关于计算机网络相关知识和NIO相关知识。
NIO通信模型基础概念 阻塞(Block)与非阻塞(Non-Block)阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。
阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。
非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。
阻塞 IO :
非阻塞 IO :
同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。比如
同步:是应用程序要直接参与 IO 读写的操作。
异步:所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。
同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。所以异步相比较于同步带来的直接好处就是在我们处理IO数据的时候,异步的方式我们可以把这部分等待所消耗的资源用于处理其他事务,提升我们服务自身的性能。
同步 IO :
异步 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模型BIONIO通信面向流面向缓冲处理阻塞 IO非阻塞 IO触发无选择器 NIO 的 Server 通信的简单模型:NIO的特点:
- 一个线程可以处理多个通道,减少线程创建数量;
- 读写非阻塞,节约资源:没有可读/可写数据时,不会发生阻塞导致线程资源的浪费
Netty 简介:Netty 是一个 NIO 客户端服务器框架,可快速轻松地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,例如 TCP 和 UDP 套接字服务器。
堆栈内存指的是堆内存和栈内存:堆内存是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操纵的性能。少去将数据从堆内内存拷贝到堆外内存的步骤。
- 内核态: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);
transferTo()方法将数据从一个channel传输到另一个可写的channel上,其内部实现依赖于操作系统对zero copy技术的支持。在unix操作系统和各种linux的发型版本中,这种功能最终是通过sendfile()系统调用实现。下边就是这个方法的定义:
#include
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
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();
}
}
博文参考
Netty 实现原理与源码解析系统 —— 精品合集 | 芋道源码 —— 纯源码解析博客
膜拜!终于拿到了阿里大佬分享的Netty源码剖析与应用PDF - osc_wyap8yov的个人空间 - OSCHINA - 中文开源技术交流社区