当前位置: 代码迷 >> 综合 >> JavaIO-BIO与NIO超全知识点(二):基于BIO和NIO的网络编程,以及文件拷贝的实现方式和原理
  详细解决方案

JavaIO-BIO与NIO超全知识点(二):基于BIO和NIO的网络编程,以及文件拷贝的实现方式和原理

热度:5   发布时间:2023-12-16 04:12:28.0

文章目录

    • 网络IO(13)
      • IO/BIO
      • NIO
    • 文件拷贝(22)
      • BIO
      • NIO
      • Files.copy
      • 拷贝性能

网络IO(13)

IO/BIO

1、Socket简单实现客户端和服务端通信

1-服务端:建立ServerSocket,等待客户端连接,然后处理数据。

import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;public class DemoSocketServer extends Thread{
    private ServerSocket serverSocket;//获取端口号public int getPort(){
    return serverSocket.getLocalPort();}public void run(){
    try {
    //serverSocket相当于大门,绑定一个门牌号(端口)serverSocket=new ServerSocket(0);while (true){
    //创建一个socket,相当于在大门口迎接的服务员,//没有客户来,就一直等着(阻塞)//当有客户端来链接时,就为其服务System.out.println("等待连接....");Socket socket=serverSocket.accept();System.out.println("已连接");// 处理客户端(新建一个线程)RequestHandler requestHandler = new RequestHandler(socket);requestHandler.start();}}catch (IOException e){
    e.printStackTrace();}finally {
    //任何情况下都要关闭连接if(serverSocket!=null){
    try {
    serverSocket.close();}catch (IOException e){
    e.printStackTrace();}}}}public static class RequestHandler extends Thread{
    private Socket mSocket;RequestHandler(Socket socket){
    mSocket = socket;}@Overridepublic void run() {
    try {
    // 1、Socket的输出流来创建printWriterPrintWriter printWriter = new PrintWriter(mSocket.getOutputStream());// 2、写入数据printWriter.println("Hello World!");//清空输出流printWriter.flush();} catch (IOException e) {
    e.printStackTrace();}}}
}

2-客户端(简单的打印数据):借助try-with-resources,用Reader去读取数据。

public class client {
    public static void main(String[] args) throws IOException {
    DemoSocketServer server = new DemoSocketServer();server.start();// 1、Socket客户端,绑定Server端Host地址,和Server端的端口。(这边是本机)try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
    // 2、通过客户端的inpustream,创建ReaderBufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));// 3、从Reader中读取到数据,并且打印。String s=null;while ((s=bufferedReader.readLine())!=null)System.out.println(s);}}
}

执行结果:
在这里插入图片描述
2、线程池改进服务端

需要减少线程频繁创建和销毁的开销
// 线程池

private Executor mExecutor;
@Override
public void run() {
    try {
    serverSocket = new ServerSocket(0);// 1、创建线程池:只有核心线程数,没有非核心线程数。任务队列无限。空闲线程会立即停止mExecutor = Executors.newFixedThreadPool(8);while (true){
    Socket socket = serverSocket.accept();RequestHandler requestHandler = new RequestHandler(socket);// 2、线程池进行处理mExecutor.execute(requestHandler);}} catch (IOException e) {
    e.printStackTrace();}finally {
    // 任何情况下都要保障Socket资源关闭。if(serverSocket != null){
    try {
    serverSocket.close();} catch (IOException e) {
    e.printStackTrace();}}}
}

3、服务端采用线程池来提供服务的典型工作模式图
在这里插入图片描述

4、服务端采用线程池处理客户端连接的缺点?

  • 连接数几百时,这种模式没有问题。但是在高并发,客户端及其多的情况下,就会出现问题。
  • 线程上下文切换的开销会在高并发时非常明显。
  • 这就是同步阻塞方式的低扩展性的体现

NIO

5、NIO优化服务端连接问题的实例

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;public class NIOServer extends Thread {
    public void run(){
    try {
    //1、创建selector,调度员Selector selector=Selector.open();//2、ServerSocketChannel,相当于IO中的SocketServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 1. 绑定IP和端口serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));// 2. 非阻塞模式。因为阻塞模式下是不允许注册的。serverSocketChannel.configureBlocking(false);/**-------------------------* 3、向Selector进行注册。通过OP_ACCEPT,表明关注的是新的连接请求* 相当于IO中 serverSocket向Socket注册*---------------------------*/System.out.println("等待连接");serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while(true){
    // 4、Selector调度员,阻塞在select操作。当有Channel有接入请求时,会被唤醒selector.select();System.out.println("已连接");// 5、被唤醒,获取到事件已经被捕获的SelectionKey的集合Set<SelectionKey> selectionKeys = selector.selectedKeys();System.out.println("已连接"+selectionKeys.size()+"个客户端");Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()){
    SelectionKey selectionKey = iterator.next();// 6、从SelectionKey中获取到对应的ChannelhandleRequest((ServerSocketChannel) selectionKey.channel());iterator.remove();}}}catch (IOException e){
    e.printStackTrace();}}// 处理客户端的请求private void handleRequest(ServerSocketChannel socketChannel){
    // 1、获取到连接到该Channel Socket的连接try(SocketChannel client = socketChannel.accept()) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 2、从客户端读取数据client.read(byteBuffer);// 3、翻转,用于读取显示和发送给服务器byteBuffer.flip();System.out.println("receive msg from NIOClient: "+ Charset.defaultCharset().decode(byteBuffer.asReadOnlyBuffer()).toString());byteBuffer.clear();byteBuffer=Charset.defaultCharset().encode("I got it!");// 4、向客户端中写数据client.write(byteBuffer);} catch (IOException e) {
    e.printStackTrace();}}
}

测试

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;public class NIOClient {
    public static void main(String[] args)throws IOException {
    //开启服务器NIOServer server=new NIOServer();server.start();//1.创建客户端try (Socket client=new Socket(InetAddress.getLocalHost(),8888)){
    //2.写入数据BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));bufferedWriter.write("I'm Client!");bufferedWriter.flush();// 3.读取数据BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));System.out.println("receive msg from server: "+bufferedReader.readLine());}}
}

执行结果:
在这里插入图片描述
6、NIO在Socket编程上的优势

  • 使用非阻塞的IO方式
  • 支持IO多路复用
  • 这些特性改变了传统网络编程中一个线程只能管理一个链接的情况,现在可以采用一个线程管理多个链接。

7、NIO为什么比IO同步阻塞模式要更好?

  • 同步阻塞模式需要多线程来处理多任务。
  • NIO利用了单线程轮询事件的机制,高效定位就绪的Channel。
  • 仅仅是select阶段是阻塞的,可以避免大量客户端连接时,频繁切换线程带来的问题。

8、NIO实现网络通信的工作模式图
在这里插入图片描述

9、NIO能解决什么问题?

  • 服务端多线程并发处理任务,即使使用线程池,高并发处理依然会因为上下文切换,导致性能问题。
  • NIO是利用单线程轮询事件的机制,高效的去选择来请求连接的Channel仅提供服务。

10、NIO的请求接收和处理都是在一个线程处理,如果有多个请求的处理顺序是什么?

  • 多个请求会按照顺序处理
  • 如果一个处理具有耗时操作,会阻塞后续操作。

11、NIO是否应该在服务端开启多线程进行处理?

我觉得是可以的(转载作者说的)
我觉得因为CPU的时间片轮转机制,一个线程只有有限的时间来使用cpu,利用多线程可以更高效的榨取CPU性能(我说的)

12、NIO遇到大量耗时操作该怎么办?

如果有大量耗时操作,那么整个NIO模型就不适用于这种场景。??感觉可以开多线程。 过多的耗时操作,可以采用传统的IO方式。

13、selector在单线程下的处理监听任务会成为性能瓶颈?

  • 是的。单线程中需要依次处理监听。会导致性能问题。
  • 在并发数数万、数十万的情况下,会导致性能问题。
  • Doug Lea推荐使用多个selector,在多个线程中并发监听Socket事件

文件拷贝(22)

1、Java有几种文件拷贝方式?哪一种效率最高?

  1. 利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个FileOutputStream,完成写入工作。
public static void copyFileByStream(File source, File dest) throwsIOException {
    try (InputStream is = new FileInputStream(source);OutputStream os = new FileOutputStream(dest);){
    byte[] buffer = new byte[1024];int length;while ((length = is.read(buffer)) > 0) {
    os.write(buffer, 0, length);}}}
  1. 利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。
public static void copyFileByChannel(File source, File dest) throwsIOException {
    try (FileChannel sourceChannel = new FileInputStream(source).getChannel();FileChannel targetChannel = new FileOutputStream(dest).getChannel();){
    for (long count = sourceChannel.size() ;count>0 ;) {
    long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);            sourceChannel.position(sourceChannel.position() + transferred);count -= transferred;}}}

3.Java 标准类库本身已经提供了几种 Files.copy 的实现。

对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。

2、不同的文件复制方式,底层机制有什么不同?

  • 当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作也是类似,仅仅是步骤相反,你可以参考下面这张图。

在这里插入图片描述
所以,这种方式会带来一定的额外开销,可能会降低 IO 效率。

  • 而基于 NIO transferTo 的实现方式,在 Linux 和 Unix
    上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo
    不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket
    发送,同样可以享受这种机制带来的性能和扩展性提高。transferTo 的传输过程是:

在这里插入图片描述

2,3问题回答来自极客时间

3、用户态空间是什么?

用户态空间-User Space 用户态空间是普通应用和服务所使用的

4、内核态空间是什么?

  • 内核态空间-Kernel Space
  • 操作系统内核、硬件驱动等都运行在内核态空间

5、读写操作时的上下文切换是什么?

  • 就是内核态和用户态之间的切换。
  • 使用输入流、输出流进行读写操作时,就在进行用户态和内核态之间的上下文切换。
  • 当读取数据时,会切换至内核态将数据从磁盘读取到内核缓存。然后再切换到用户态,将数据从内核缓存读取到用户缓存。
  • 写入操作类似, 仅仅是方向相反。

6、读写操作时的性能问题是什么?如何去解决?

  • 读写操作时,进行用户态和内核态的上下文切换,会带来额外的开销,从而降低IO效率。
  • 基于NIO transferTo的实现方式,在Linux和Unix上,会用到零拷贝技术。
  • 数据传输不再需要用户态参与,节省了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。

7、为什么零拷贝(zero-copy)可能有性能优势?

  • 不再需要用户态和内核态的切换
  • 减少了从内核缓存拷贝到用户缓存的这些不必要的内存拷贝。
  • 原来是4次拷贝: 磁盘->内核缓存,内核缓存->用户缓存,用户缓存->内核缓存,内核缓存->磁盘
  • 零拷贝只有2次拷贝: 磁盘->内核缓存,内核缓存->磁盘

8、NIO transferTo的实现方式

  • 会采用零拷贝技术
  • transferTo不仅仅用在文件拷贝中,也能用于读取磁盘文件、然后Socket进行发送。同样性能有很大优势。

9、为什么copy要设计成需要进行上下文切换的方式?为什么不和nio的transfer一样设计为不需要用户态切换的开销?

  • 大部分工作需要用户态。
  • transfer是特定场景而不是通用场景

BIO

10、利用java.io的InputStream和OutputStream进行文件拷贝

1-实现文件拷贝

/**========================================* java.io: 实现文件复制* @param src 源文件* @param dst 目标文件*=======================================*/
public static void copyFileByIO(File src, File dst){
    try(InputStream inputStream = new FileInputStream(src);OutputStream outputStream = new FileOutputStream(dst)){
    byte[] buffer = new byte[1024];int length;// 读取数据到byte数组中,然后输出到OutStream中while((length = inputStream.read(buffer)) > 0){
    outputStream.write(buffer, 0, length);}} catch (FileNotFoundException e) {
    e.printStackTrace();} catch (IOException e) {
    e.printStackTrace();}
}

2-测试程序

File src = new File("D:\\src.txt");
File dst = new File("D:\\dst.txt");
copyFileByIO(src, dst);

NIO

11、利用NIO实现文件的拷贝

/**===================================* java.nio: 实现文件复制* @param src 源文件* @param dst 目标文件*===============================*/
public static void copyFileByChannel(File src, File dst){
    // 1、获取到源文件和目标文件的FileChanneltry(FileChannel srcFileChannel  = new FileInputStream(src).getChannel();FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
    // 2、当前FileChannel的大小long count = srcFileChannel.size();while(count > 0){
    /**=============================================================* 3、从源文件的FileChannel中将bytes写入到目标FileChannel中* 1. srcFileChannel.position(): 源文件中开始的位置,不能为负* 2. count: 转移的最大字节数,不能为负* 3. dstFileChannel: 目标文件*==============================================================*/long transferred = srcFileChannel.transferTo(srcFileChannel.position(),count, dstFileChannel);// 4、传输完成后,更改源文件的position到新位置srcFileChannel.position(srcFileChannel.position() + transferred);// 5、计算出剩下多少byte需要传输count -= transferred;}} catch (FileNotFoundException e) {
    e.printStackTrace();} catch (IOException e) {
    e.printStackTrace();}
}

测试

File src = new File("D:\\src.txt");
File dst = new File("D:\\dst.txt");
// nio拷贝
copyFileByChannel(src, dst);

12、Nio的transferTo一定会快于BIO吗?

Files.copy

13、利用Files.copy()进行文件拷贝

Path srcPath = Paths.get("D:\\src.txt");
Path dstPath = Paths.get("D:\\dst.txt");
try {
    // 进行拷贝,CopyOption参数可以没有Files.copy(srcPath, dstPath);
} catch (IOException e) {
    e.printStackTrace();
}

14、Files.copy()有哪4种方法?

1-文件间进行copy

public static Path copy(Path source, Path target, CopyOption... options);

2-从输入流中copy到文件中

public static long copy(InputStream in, Path target, CopyOption... options);

3-从文件中copy到输出流

public static long copy(Path source, OutputStream out);

4-从输入流copy到输出流中

private static long copy(InputStream source, OutputStream sink);

15、Path copy(Path, Path, CopyOption…)源码分析

public static Path copy(Path source, Path target, CopyOption... options)
{
    FileSystemProvider provider = provider(source);if (provider(target) == provider) {
    // same provider-同种文件系统拷贝provider.copy(source, target, options);} else {
    // different providers-不同文件系统拷贝CopyMoveHelper.copyToForeignTarget(source, target, options);}return target;
}

从文件系统中最终会定位到UnixCopyFile.c 根据内部实现的说明,这只是简单的用户态空间拷贝。 因此这个copy(path,path)并不是利用transferTo,而是本地技术实现的用户态拷贝

16、FileSystemProvider是什么?如何提供文件系统的?

  • 文件系统的服务的提供者
  • 是一个抽象类
  • 内部通过ServiceLoader机制加载一系列文件系统,然后提供服务。
  • 文件系统的实际逻辑是在JDK内部实现的,可以在JDK源码中搜索FileSystemProvider、nio,可以定位到sun/nio/fs,因为NIO底层和操作系统紧密相连,所以每个平台都有自己部分特有的文件系统。
  • Unix平台上会定位到UnixFileSystemProvider->UnixCopyFile.Transfer->UnixCopyFile.c
// FileSystemProvider.java
private static List<FileSystemProvider> loadInstalledProviders() {
    //加载所有已经安装的文件系统服务的提供者List<FileSystemProvider> list = new ArrayList<FileSystemProvider>();ServiceLoader<FileSystemProvider> sl = ServiceLoader.load(FileSystemProvider.class, ClassLoader.getSystemClassLoader());//xxx
}

17、平台特有的文件系统服务提供者: FileSystemProvider
在这里插入图片描述

18、copy(InputStream, OutputStream)源码分析

直接进行inoputstream和outputstream的read和write操作,本质和一般IO操作是一样的。
也就是用户态的读写

private static long copy(InputStream source, OutputStream sink)
{
    long nread = 0L;byte[] buf = new byte[BUFFER_SIZE];int n;while ((n = source.read(buf)) > 0) {
    sink.write(buf, 0, n);nread += n;}return nread;
}

19、copy(InputStream, Path, CopyOption…)源码分析

本质调用的copy(InputStream, OutputStream)方法

public static long copy(InputStream in, Path target, CopyOption... options)
{
    // xx省略代码xxObjects.requireNonNull(in);OutputStream ostream;// 1、通过target的Path获取到OutputStreamostream = newOutputStream(target, StandardOpenOption.CREATE_NEW,StandardOpenOption.WRITE);// 2、然后还是直接调用copy(InputStream, OutpuStream)的方法进行数据拷贝。try (OutputStream out = ostream) {
    return copy(in, out);}
}

20、copy(Path, OutputStream)源码分析

本质调用的copy(InputStream, OutputStream)方法

public static long copy(Path source, OutputStream out) throws IOException {
    //1、输出流不为NullObjects.requireNonNull(out);//2、本质调用的copy(InputStream, OutputStream)方法try (InputStream in = newInputStream(source)) {
    return copy(in, out);}
}

21、JDK10中Files.copy()实现的轻微改变:InputStream.transferTo

  1. copy(Path, Path, CopyOption…)内部机制没有变化
  2. 剩余copy()方法,将输入流、输出流的读写封装到了方法中:InputStream.transferTo(),也就是处于用户态的读写
public long transferTo(OutputStream out) throws IOException {
    Objects.requireNonNull(out, "out");long transferred = 0;byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];int read;//IO读写操作while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
    out.write(buffer, 0, read);transferred += read;}return transferred;
}

拷贝性能

22、如何提升类似拷贝等IO操作的性能?

  • 合理使用缓存等机制,合理减少IO次数
  • 使用transferTo等机制,减少上下文切换和额外的IO操作
  • 减少不必要的转换过程, 如:
    1. 编解码
    2. 对象序列化和反序列化
    3. 操作文本文件或者网络通信,如果不是要使用文本信息,可以直接传输二进制信息。而不是传输字符串。