当前位置: 首页 > news >正文

Java 输入与输出之 NIO【非阻塞式IO】【NIO核心原理】探索之【一】

Java标准的输入/输出(Input/Output,简称I/O)是Java程序与外部世界进行交互的重要机制,它允许程序读取和写入数据到各种类型的源,如文件、网络套接字、管道、内存缓冲区等。Java I/O API主要位于java.io包中,提供了丰富的类和接口来处理不同类型的输入输出操作。
Java 的 I/O 类库位于 java.io 包中,JDK 1.0 最初的Java IO只支持字节流(InputStream、OutputStream)和字符流(Reader、Writer)两种,属于阻塞式IO(BIO)模型。
Java标准的输入/输出(I/O)体系一些实现类的层次关系:
在这里插入图片描述

以下是一个简单的 Java I/O 示例,它展示了如何使用 FileInputStream 和 FileOutputStream 读取和写入文件:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class IOExample {public static void main(String[] args) {try {// 创建输入流以读取文件FileInputStream fis = new FileInputStream("input.txt");// 创建输出流以写入文件FileOutputStream fos = new FileOutputStream("output.txt");int content;// 读取并写入文件直到文件末尾while ((content = fis.read()) != -1) {fos.write(content);}// 关闭流fis.close();fos.close();} catch (IOException e) {e.printStackTrace();}}
}

一、Java I/O发展史
JDK 1.0 最初的Java IO只支持字节流(InputStream、OutputStream)和字符流(Reader、Writer)两种,属于阻塞式IO(BIO)模型。
JDK 1.4 引入了一套全新的IO处理机制,引入了缓存区(Buffer)、通道(Channel)等概念,与之前的标准IO(BIO)相比,NIO具有更高的可扩展性和灵活性,特别是在网络编程和高并发场景下,表现得更为出色。这是非阻塞式IO(NIO)模式。提供了更强大的文件处理功能和更高效的IO操作,如内存映射文件等的功能。Java NIO(New I/O)是一种高性能的I/O处理机制,它提供了对标准Java I/O API的替代方案,以支持更高效的文件和网络数据传输。
在JDK 1.7 版本中对NIO进行了完善,推出了NIO.2,也称为AIO(异步IO),在处理大量并发请求时具有优势,特别是在网络编程和高并发场景下,表现得更为出色。
在这里插入图片描述
BIO、NIO和AIO比较
Java的输入输出(I/O)模型,可以分为以下三种:
BIO(Blocking IO)是最传统的一种IO模型,BIO在读/写数据如果没有可以读取/写入时会发生阻塞。BIO 读写是面向流(Stream)的,一次性只能从流(Stream)中读取一个或者多个字节,并且读完之后流(Stream)无法再读取,需要我们自己将数据缓存起来,BIO位于java.io包中。
NIO(New IO)采用非阻塞模式,它是基于selector模式,IO调用不会被阻塞,它是NIO位于java.nio包中。NIO则是面向缓冲区(Buffer)的,它将会使用缓存去管理数据,使得读写操作更加快速和灵活。
AIO 也就是 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞 的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是说 AIO 模式不需要selector 操作,而是是事件驱动形式,也就是当客户端发送数据之后,会主动通知服务器,接着服务器再进行读写操作。AIO位于java.nio包中。

特别是在网络编程和高并发场景下,Java NIO和AIO表现得更为出色。Java BIO在进行网络通信时,每个客户端连接都需要创建一个线程来进行处理,这样会导致系统资源的浪费。Java NIO则只需要一个线程就可以完成对多个客户端连接的处理,大大减少系统资源的占用。
在这里插入图片描述

二、NIO核心原理
主要包括:缓冲区(Buffer)、通道(Channel)和选择器(Selector)、字符集(Charset);首先获取用于连接IO设备的通道channel以及用于容纳数据的缓冲区,利用选择器Selector监控多个Channel的IO状况(多路复用),然后操作缓冲区,对数据进行处理。 NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道,即一个单独的线程现在可以管理多个输入和输出通道。

  1. 缓冲区Buffer
    缓冲区是Java NIO中一个非常重要的概念,所有数据都是通过缓冲区对象进行传输的。缓冲区是一段连续的内存块,用于保存读写的数据。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。
    缓冲区可以在内存中创建,并可以通过通道(Channel)进行读写操作,也可以作为参数传递给其他方法。缓冲区在java NIO中负责数据的存取,底层缓冲区其实就是数组,用于存储不同数据类型的数据,根据不同的数据类型(Boolean除外),提供了相应类型的缓冲区:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。

缓冲区的四个核心属性:

(1). position属性
表示缓冲区中正在操作数据的位置。它表示当前位置是要读取或写入的下一个元素的索引。在buffer进行读写模式改变时,position的值会进行相应调整
写模式
在刚进入写模式时,position的值为0,表示当前写入位置为从头开始,每当一个数据写入到缓冲区之后,position会向后移动到下一个可写位置,当position的值达到limit时,缓冲区就已经无空间可写了。
读模式
当缓冲区刚进入读模式时,position会被重置为0,每读一个数据,position会向后移动到下一个可读为止,当position的值达到limit时,缓冲区就已经无数据可读了。

(2). limit属性
limit:界限,缓冲区中可以操作数据的大小,表示可以写入或者读取的数据最大上限。
写模式
limit属性值的含义为可以写入数据的最大上限位置,在刚进入写模式时,limit的值会别设置成Buffer的capacity值,表示一直可以将缓冲区内容写满
读模式
limit值的含义为最多能从buffer读取数据的最大上限,flip方法将缓冲区切换到读模式时,limit的值会被设置为上次写模式的position。

(3). mark属性
mark:标记,在缓冲区操作过程中,可以将当前position的值临时存入mark属性中,需要的时候可以通过调用reset()把mark的位置恢复到position属性中

(4). capacity属性
capacity是缓冲区的容量,表示缓冲区的最大容量,声明后不能改变。
一旦写入数据达到了capacity,缓冲区就满了,不能再写入。

Buffer在读模式与写模式下4个属性的关系如下图所示:
在这里插入图片描述

缓冲区的读写操作都会修改position和limit属性,例如在从缓冲区中读取数据时,position属性会自动向后移动,而limit属性则不会更改,因此读取操作只能读取到limit位置之前的数据。

四者的关系:0<mark<=position<=limit<=capacity

缓冲区相关的一些方法:
put():存数据到缓存区,写数据模式。
flip():切换操作模式。当将缓冲区从写模式“切换”到读数据模式,重置 position 为 0(position和limit改变,capacity不变)
get():从缓冲区中读取数据。
rewind():将 position 设置为 0,限制保持不变,可以重新读取缓冲区中的数据。
compact():其功能有点像磁盘碎片整理的作用。compact()方法将所有未读的数据拷贝到Buffer起始处,将 position 设置为 0。
clear():清空缓冲区,重置 position 和 limit 为初始位置。缓冲区中原有数据不会被清除,但处于“已遗忘”状态。

缓冲区的创建
ByteBuffer是一个抽象类,所以无法直接使用new关键字来创建对象。创建ByteBuffer对象主要使用ByteBuffer类中的两个静态方法来创建:用ByteBuffer.allocate(int capacity)方法可创建非直接缓冲区;用ByteBuffer.allocateDirect(int capacity)方法创建的则是直接缓冲区。

直接缓冲区和非直接缓冲区:
直接缓冲区可以通过调用工厂方法 allocateDirect()来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区还可以通过 FileChannel 的 map()方法将文件区域直接映射到操作系统的系统内存中来创建。该方法返回MappedByteBuffer 。
非直接缓冲区,是在JVM内存中创建的,缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复杂的操作。
缓冲区创建样例代码:

import java.nio.ByteBuffer; //导入包
public class ByteBufferTest {public static void main(String ... args) {//非直接缓冲区:使用allocate创建缓冲区,//其内存分配由jvm负责,受到jvm堆内存机制的影响。ByteBuffer buffer1 = ByteBuffer.allocate(10);//直接缓冲区:使用allocateDirect方法创建//是在操作系统的本地内存中进行的,不受 Java 堆内存管理机制的影响ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);}
}
  1. 通道Channel
    通道(Channel)是Java NIO的核心概念,是网络或文件IO操作的抽象,它表示一个数据通讯的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序。在java NIO中Channel本身不负责存储数据,通道可以和缓冲区一起使用,让数据直接在缓冲区之间进行传输。

在这里插入图片描述

通道(Channel)可以对应到BIO中的流(Stream)。Channel与Stream的区别是:Stream是单向的阻塞式的,Channel是双向的,可以使用Selector选择器实现非阻塞IO操作。在使用流(Stream)的时候有InputStream,OutputStream等都只支持单向操作;而Channel是双向的,它即可用来进行读操作,又可以进行写操作。

通道的主要实现类:
FileChannel:用于文件读写操作;
DatagramChannel:用于UDP协议的网络通信;
SocketChannel:用于TCP协议的网络通信;
ServerSocketChannel:用于监听TCP连接请求。

通道的获取方式
java针对支持通道的类提供了getChannel()方法。
支持通道的类如下:
(一)提供本地文件IO的Channel类有:
FileInputStream
FileOutputStream
RandomAccessFile
(二)提供网络套接字IO的Channel类:
DatagramSocket
Socket
ServerSocket
(三)获取通道的其他方式:
在JDK7.0中的AIO针对各个通道提供静态方法open()可打开并返回指定通道;
在JDK7.0中的AIO的Files类可使用Files类的静态方法newByteChannel()获取字节通道。

在使用NIO进行网络编程时,我们常常使用SocketChannel和ServerSocketChannel以TCP/IP协议来实现客户端与服务器之间的通信。当使用DatagramChannel来实现客户端与服务器之间的通信时,可以发送和接收UDP协议的数据包。使用FileChannel可以完成对本地文件的读写操作。

我们来看一个从文件读数据的例程,其中方法fileReadBIO()是用BIO的输入流方式读文件信息;方法fileReadNIO()使用NIO的FileChannel和字节缓冲区ByteBuffer来读文件信息。

package nio;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileReadTest {public static void fileReadBIO(){InputStream in = null;System.out.println("BIO模式读文件测试***");try{String path = "D:/temp/TestBIO.txt";in = new BufferedInputStream(new FileInputStream(path));byte [] buf = new byte[1024];int bytesRead = in.read(buf);while(bytesRead != -1){for(int i=0;i<bytesRead;i++)System.out.print((char)buf[i]);bytesRead = in.read(buf);}}catch (IOException e){e.printStackTrace();}finally{try{if(in != null){in.close();}}catch (IOException e){e.printStackTrace();}}}public static void fileReadNIO(){System.out.println("NIO模式读文件测试***");RandomAccessFile aFile = null;try{aFile = new RandomAccessFile("D:/temp/TestNIO.txt","rw");FileChannel fileChannel = aFile.getChannel();ByteBuffer buf = ByteBuffer.allocate(1024);int bytesRead = fileChannel.read(buf);System.out.println(bytesRead);while(bytesRead != -1){buf.flip();while(buf.hasRemaining()){System.out.print((char)buf.get());}buf.compact();bytesRead = fileChannel.read(buf);}}catch (IOException e){e.printStackTrace();}finally{try{if(aFile != null) aFile.close();}catch (IOException e){e.printStackTrace();}}}public static void main(String[] args) {fileReadBIO();System.out.println();fileReadNIO();}}
  1. 选择器Selector和选择键SelectionKey
    选择器(Selector)和选择键(SelectionKey)是Java NIO提供的另外两个核心组件。选择器(Selector)用于检测通道的状态,并且可以根据通道状态进行非阻塞选择操作。而选择键(SelectionKey)则是一种将通道和选择器(Selector)进行关联的机制。

Selector是SelectableChannel的选择器,又称为多路复用器,一般用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。

使用选择器(Selector)可以实现单线程管理多个通道的方式,以此实现高并发IO操作。在选择器的模型中,每个通道都会注册到一个选择器上,并且每个通道都有一个唯一的选择键对象来代表这个通道。选择键对象包含几个标志位,表示通道的当前状态等信息。
在这里插入图片描述

选择器(Selector)可以监听多个通道的事件,例如连接就绪、读取数据就绪、写入数据就绪等等。当有一个或多个通道的事件就绪时,选择器就会自动返回这些通道的选择键,我们可以通过选择键获取到对应的通道,然后进行相应的操作。

选择器(Selector)是Java NIO中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。

  1. 字符集Charset(编码解码)
    编码(按指定Charset编码方案编码)
    字符串转成字节数组
    解码(按指定Charset编码方案解码)
    字节数组转成字符串

请看一个编码和解码的演示例程:

package nio;import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Map;
import java.util.Set;/*** 字符集(Charset)* 编码:字符串-->字节数组* 解码:字节数组-->字符串*/public class CharsetDemo {static String infStr = "绿水青山就是金山银山!";public static void charSetEncoderAndDecoder() throws CharacterCodingException {Charset charset=Charset.forName("UTF-8");//1.获取编码器CharsetEncoder charsetEncoder=charset.newEncoder();//2.获取解码器CharsetDecoder charsetDecoder=charset.newDecoder();//3.获取需要解码编码的数据CharBuffer charBuffer=CharBuffer.allocate(1024);charBuffer.put(infStr);charBuffer.flip();//4.编码ByteBuffer byteBuffer=charsetEncoder.encode(charBuffer);System.out.println("编码后:");for (int i=0;i<byteBuffer.limit();i++) {System.out.println(byteBuffer.get());}//5.解码byteBuffer.flip();CharBuffer charBuffer1=charsetDecoder.decode(byteBuffer);System.out.println("\n解码后:");System.out.println(charBuffer1.toString());System.out.println("\n使用不正确的编码格式解码,解码结果:");Charset charset1=Charset.forName("GBK");byteBuffer.flip();CharBuffer charBuffer2 =charset1.decode(byteBuffer);System.out.println(charBuffer2.toString());}/***查询系统可用的字符编码***/public static void getAvailableCharsets() {//6.获取Charset所支持的字符编码System.out.println("\n系统可用的字符编码:");Map<String ,Charset> map= Charset.availableCharsets();Set<Map.Entry<String,Charset>>  set=map.entrySet();for (Map.Entry<String,Charset> entry: set) {System.out.println(entry.getKey()+"="+entry.getValue().toString());}}public static void main(String[] args) throws IOException {/****字符集编码和解码演示****/charSetEncoderAndDecoder();/***查询系统可用的字符编码***/getAvailableCharsets();}
}

下面我们对通道的主要实现类来进行一下介绍:

  1. FileChannel:用于文件读写操作;

文件通道FileChannel是用于读取,写入,文件的通道。FileChannel只能被InputStream、OutputStream、RandomAccessFile所创建。
使用fileChannel.transferTo()可极大提高文件的复制效率,为进行大容量文件的读和写,直接把读通道和写通道建立了连接,还能有效避免因文件过大而导致内存溢出。

FileChannel的常用方法:
int read(ByteBuffer dst) 从Channel当中读取数据至ByteBuffer
long read(ByteBuffer[] dsts)将channel当中的数据“分散”至ByteBuffer[]
int write(Bytesuffer src)将ByteBuffer当中的数据写入到Channel
long write(ByteBuffer[] srcs)将Bytesuffer[]当中的数据“聚集”到Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void force(boolean metaData)强制将所有对此通道的文件更新写入到存储设备中

这里我们提供一个文件读、写和拷贝复制的例程:

package nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {public static void readFile(){ //读文件例程try {//1.定义一个文件字节输入流与源文件接通FileInputStream fos = new FileInputStream(new File("D:/temp/test01.txt"));//2.获取文件字节输入流的文件通道FileChannel channel = fos.getChannel();//3.定义一个缓存区ByteBuffer buf = ByteBuffer.allocate(1024);int bytesRead = channel.read(buf); //4.读取数据到缓存区String str = null;while(bytesRead != -1) {//5、切换buf.flip();//6.读取缓存区中的数据并输出即可str = new String(buf.array(), 0, buf.remaining());System.out.println("读取内容..." + str);bytesRead = channel.read(buf); //4.读取数据到缓存区}channel.close();    } catch (Exception e) {throw new RuntimeException(e);}}public static void writeFile(){ //写文件例程try {//1.字节输出流通向目标文件FileOutputStream fos = new FileOutputStream(new File("D:/temp/test01.txt"));//2.得到字节输出流对应的通道ChannelFileChannel channel = fos.getChannel();//3.分配缓存区ByteBuffer bf = ByteBuffer.allocate(1024);bf.put("最近公司有个需求,就是上传产品详情图。" .getBytes());//4.把缓存区切换为写模式bf.flip();//5.输出数据到文件channel.write(bf);channel.close();System.out.println("完成数据写入....");} catch (Exception e) {throw new RuntimeException(e);}}public static void copyFile(){ //复制文件例程try {long starTime = System.currentTimeMillis();//1、创建输入文件流FileInputStream fis = new FileInputStream(new File("D:/temp/test01.txt"));//2、得到输入channelFileChannel fisChannel = fis.getChannel();//3、创建输出文件流FileOutputStream fos = new FileOutputStream(new File("D:/temp/test02.txt"));//4、得到输出channelFileChannel fosChannel = fos.getChannel();//5、使用输入channel将文件转到fosChannelfisChannel.transferTo(0, fisChannel.size(), fosChannel);fis.close();fos.close();fisChannel.close();fosChannel.close();long endTime = System.currentTimeMillis();System.out.println("耗时=" + (endTime - starTime) + "ms");} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {writeFile(); System.out.println("从文件读信息:");readFile();}
}

例程说明:
方法readFile()演示如何从文件读入数据信息;
方法writeFile()演示如何把数据信息写入磁盘文件中。
方法copyFile()则是一个文件复制演示程序。
由于例程中没有显式指定数据信息的字符集编码方案,如果读入一个其他文本编辑器编辑的文本文件,显示出来也可能是乱码的。
因此,本例程中最后测试时,先调用writeFile(); 然后再调用readFile();,这样可确保读写测试都使用默认的字符集编码方案。由于写入的文本信息太少,第一次无法测试到循环读信息的效果。可在第一次测试后,再用文本编辑器随意增加足够文本信息,再进行测试,才可测试到循环读信息。

缓冲区Buffer的使用说明:
Buffer顾名思义:缓冲区,实际上是一个容器,一个连续数组。通道Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须在缓冲区Buffer进行缓存。
可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态:capacity, position, limit, mark:

在这里插入图片描述
我们以“写文件例程”方法writeFile()为例来进行说明。我们来分析这几行源代码:

            //3.分配缓存区ByteBuffer bf = ByteBuffer.allocate(1024);bf.put("最近公司有个需求,就是上传产品详情图。" .getBytes());//4.把缓存区切换为写模式bf.flip();//5.输出数据到文件channel.write(bf);channel.close();

//3.分配缓存区
ByteBuffer bf = ByteBuffer.allocate(1024);
当执行完上面这行代码后,程序分配了缓冲区Buffer,此时Buffer的初始状态如下图,position的位置为0,capacity和limit默认都是数组长度。
在这里插入图片描述
bf.put(“最近公司有个需求,就是上传产品详情图。” .getBytes());
执行完上面这行代码,缓冲区Buffer写入了数据后,position的位置移到写入数据的后面,缓冲区的状态如下:
在这里插入图片描述
//4.把缓冲区切换为写模式
bf.flip();
执行完上面这行代码flip()后,缓冲区的position的位置移到0,limit则移到了原来position的位置。
在这里插入图片描述
在下一次再往Buffer写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。
调用clear()方法:position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”。
如果Buffer中仍有未读的数据,且后续还需要这些数据,那么可使用compact()方法,其功能有点像磁盘碎片整理的作用。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定的position,之后可以通过调用Buffer.reset()方法恢复到这个position。
Buffer.rewind()方法将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。

关于NIO网络通讯编程的应用请参见博客:
Java 输入与输出之 NIO【非阻塞式IO】【NIO网络编程】探索之【二】

NIO 的应用场景:

网络通信:NIO 可以用于开发高并发的网络应用,例如 Web 服务器、游戏服务器等。
文件操作:NIO 可以用于开发高性能的文件操作应用,例如文件传输、文件压缩等。
进程间通信:NIO 可以用于实现进程间通信,例如共享内存、管道等。
数据库操作:NIO 可以用于提高数据库操作的性能,例如批量插入、批量查询等。

NIO 的优缺点
1、NIO 的优势
NIO 相对于传统 IO 具有以下优势:
提高并发性:NIO 可以使用多路复用器来监听多个通道的事件,提高并发性。
提高性能:NIO 支持非阻塞 IO,可以提高性能。
简化编程:NIO 的 API 更加简洁,易于理解和使用。

2、NIO 的缺点
NIO 相对于传统 IO 具有以下缺点:
学习成本较高:NIO 的概念和 API 与传统 IO 不同,学习成本较高。
不兼容性:NIO 与传统 IO 存在不兼容性,需要注意兼容性问题。

BIO性能优势局限:
Java NIO 在高并发和高吞吐量的场景下可以提供性能优势,但对于许多常规应用程序而言,传统的阻塞式I/O 已经足够了。只有需要处理大量并发连接或需要高度定制化的网络通信时,Java NIO 才会显得更有价值。

参考文献&博客:

  1. 参考文献之一
    攻破JAVA NIO技术壁垒
  2. 参考文献之二
    Java NIO全面详解(看这篇就够了)
  3. 参考文献之三
    Java NIO详解
  4. 参考文献之四
    Java FileChannel文件的读写实例

http://www.mrgr.cn/news/13776.html

相关文章:

  • 数据链路层(Mac帧,报头字段,局域网通信原理),MTU,MSS,ip报文的分包与组装(ip报头字段介绍,组装过程,判断是否被分片/收到全部分片)
  • 手机游玩植物大战僵尸杂交版V2.3.7最新版教程(文章末尾免费直接下载链接)
  • 跨境电商避坑指南:如何在亚马逊和速卖通安全进行测评补单
  • Linux—信号量
  • sql实现按照自定义顺序 排序
  • vue3实现excel文件预览和打印
  • 利用移动语义优化 C++ 程序性能的实用指南
  • easyExcel 单元格合并
  • Image Stride(内存图像行跨度)
  • 初识Vue.js:从零开始构建你的第一个Vue项目
  • 在Linux中杀死占用某个端口的进程
  • pymysql cursor使用教程
  • DARKTIMES集成到Sui,带来中世纪格斗大逃杀游戏体验
  • Java使用Tesseract进行OCR图片文字识别
  • CannotCreateTransactionException产生原因及解决方案
  • 【C++二分查找】2271. 毯子覆盖的最多白色砖块数
  • c语言每日学习8.24
  • 视频监控汇聚智能分析安全帽佩戴检测算法工作原理未戴安全帽算法源码分享
  • 分布式中间件
  • MariaDB基本知识汇总