nio-总结列表
java IO体系图
IO流的操作规律总结:
1,明确体系:
数据源:InputStream ,Reader
数据汇:OutputStream,Writer
2,明确数据:因为数据分两种:字节,字符。
数据源:是否是纯文本数据呢?
是:Reader 字符输入流
否:InputStream 字节输入流
数据汇:是否是纯文本数据呢?
是:Writer 字符输出流
否:OutputStream 字节输出流、
到这里就可以明确具体要使用哪一个体系了,剩下的就是要明确使用这个体系中的哪个对象。
3.明确设备:
数据源:
键盘:System.in 键盘输入
硬盘:FileXXX 硬盘上面
内存:数组。
网络:socket socket.getInputStream();
数据汇:
控制台:System.out
硬盘:FileXXX
内存:数组
网络:socket socket.getOutputStream();
4,明确额外功能:
1,需要转换?是,使用转换流。InputStreamReader OutputStreamWriter
2,需要高效?是,使用缓冲区。Buffered
3,需要其他?
-------------------------------------------------------------------------------------------------------------------------------------
1,复制一个文本文件。
1,明确体系:
源:InputStream ,Reader
目的:OutputStream ,Writer
2,明确数据:
源:是纯文本吗?是 Reader
目的;是纯文本吗?是 Writer
3,明确设备:
源:硬盘上的一个文件。 FileReader
目的:硬盘上的一个文件。FileWriter
FileReader fr = new FileReader("a.txt");
FileWriter fw = new FileWriter("b.txt");
4,需要额外功能吗?
需要,高效,使用buffer
BufferedReader bufr = new BufferedReader(new FileReader("a.txt"));
BufferedWriter bufw = new BufferedWriter(new FileWriter("b.txt"));
2,读取键盘录入,将数据存储到一个文件中。
1,明确体系:
源:InputStream ,Reader
目的:OutputStream ,Writer
2,明确数据:
源:是纯文本吗?是 Reader
目的;是纯文本吗?是 Writer
3,明确设备:
源:键盘,System.in
目的:硬盘,FileWriter
InputStream in = System.in;
FileWriter fw = new FileWriter("a.txt");
4,需要额外功能吗?
需要,因为源明确的体系时Reader。可是源的设备是System.in。
所以为了方便于操作文本数据,将源转成字符流。需要转换流。InputStreamReader
InputStreamReader isr = new InputStreamReader(System.in);
FileWriter fw = new FileWriter("a.txt");
需要高效不?需要。Buffer
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bufw = new BufferedWriter(new FileWriter("a.txt"));
3,读取一个文本文件,将数据展现在控制台上。
1,明确体系:
源:InputStream ,Reader
目的:OutputStream ,Writer
2,明确数据:
源:是纯文本吗?是 Reader
目的;是纯文本吗?是 Writer
3,明确设备:
源:硬盘文件,FileReader。
目的:控制台:System.out。
FileReader fr = new FileReader("a.txt");
OutputStream out = System.out;
4,需要额外功能?
因为源是文本数据,确定是Writer体系。所以为了方便操作字符数据,
需要使用字符流,但是目的又是一个字节输出流。
需要一个转换流,OutputStreamWriter:字符向字节转化
FileReader fr = new FileReader("a.txt");
OutputStreamWriter osw = new OutputStreamWriter(System.out);
需要高效吗?需要。
BufferedReader bufr = new BufferedReader(new FileReader("a.txt"));
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
4,读取键盘录入,将数据展现在控制台上。
1,明确体系:
源:InputStream ,Reader
目的:OutputStream ,Writer
2,明确数据:
源:是纯文本吗?是 Reader
目的;是纯文本吗?是 Writer
3,明确设备:
源:键盘:System.in
目的:控制台:System.out
InputStream in = System.in;
OutputStream out = System.out;
4,需要额外功能吗?
因为处理的数据是文本数据,同时确定是字符流体系。
为方便操作字符数据的可以将源和目的都转成字符流。使用转换流。
为了提高效率,使用Buffer
BufferedReader bufr =new BufferedReader(new InputStreamReader(Systme.in));
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
5,读取一个文本文件,将文件按照指定的编码表UTF-8进行存储,保存到另一个文件中。
1,明确体系:
源:InputStream ,Reader
目的:OutputStream ,Writer
2,明确数据:
源:是纯文本吗?是 Reader
目的;是纯文本吗?是 Writer
3,明确设备:
源:硬盘:FileReader.
目的:硬盘:FileWriter
FileReader fr = new FileReader("a.txt");
FileWriter fw = new FileWriter("b.txt");
4,额外功能:
注意:目的中虽然是一个文件,但是需要指定编码表。
而直接操作文本文件的FileWriter本身内置的是本地默认码表。无法明确具体指定码表。
这时就需要转换功能。OutputStreamWriter,而这个转换流需要接受一个字节输出流,而且
对应的目的是一个文件。这时就使用字节输出流中的操作文件的流对象。FileOutputStream.
FileReader fr = new FileReader("a.txt");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("b.txt"),"UTF-8");
需要高效吗?
BufferedReader bufr = new BufferedReader(new FileReader("a.txt"));
BufferedWriter bufw =
new BufferedWriter(new OutputStreamWriter(new FileOutputStream("b.txt"),"UTF-8"));
目前为止,10个流对象重点掌握。
字符流:
FileReader
FileWriter
BufferedReader
BufferedWriter
InputStreamReader
OutputStreamWrier
字节流:
FileInputStream
FileOutputStream
BufferedInputStream
BufferedOutputStream
----------------------------------------------
file类
构造方法:File f = new File("file.txt");//file.txt 相对路径
* File f1 = new File("c:"+File.separator+"abc"+File.separator+"lp", "b.txt");//c:\abc\lp\b.txt 绝对路径
File f2 = new File(File d, child);
常用方法:
新建文件:
boolean createNewFile() 如果文件存在,则返回false,不会被覆盖,这和输出流不一样,IO输出流每次都会覆盖
public static File createTempFile(String prefix, String suffix) throws IOException
在默认的情况下创建一个临时空文件,给定前缀(abc)和后缀(.tmp)
public static File createTempFile(String prefix, String suffix, File directory) throws IOException
指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称
directory如果为null,使用默认的路径, 在windows上为C盘存放Tmp的目录下面,UNIX下面通常是"/tmp"或"/var/temp
创建目录:
boolean mkdirs() 可以递归的创建不存在的目录,创建多级目录
boolean mkdir() 创建目录,不能递归,如果存在不能创建,
删除文件: boolean delete() 删除File对象所对应的文件和路径
public void deleteOnExit() 在虚拟机终止时,请求删除此抽象路径名表示的文件或目录
判断:
boolean exists() 测试此抽象路径名表示的文件或目录是否存在。
boolean isDirectory() 测试此抽象路径名表示的文件是否是一个目录。
boolean isFile() 测试此抽象路径名表示的文件是否是一个标准文件。
获取:
String getName()
String getParent()
String getPath()
String getAbsolutePath() 返回此抽象路径名的绝对路径名字符串。
File getAbsoluteFile() 返回此抽象路径名的绝对路径名所对应的File对象
long lastModified() 返回此抽象路径名表示的文件最后一次被修改的时间。
long getFreeSpace() 获取指定分区(例如D盘)可用的空间,或者剩余的空间
long getUsableSpace() 获取分区(例如D盘的可用于虚拟机的空间
long getTotalSpace()获取分区(例如D盘)总容量
重命名:boolean renameTo(File dest) :把某个路径下的文件重命名,从一个盘重命名后剪切到另一个盘
列出指定目录下面的文件:
String[] list() 返回文件或者目录的名字,是字符串类型
File[] listFiles() 返回文件或者目录对象,通过getName获取文件或者路径名称
文件过滤:
String[] list(FilenameFilter filter)
FilenameFilter-->>boolean accept(File dir, String name)
可以使用此方法列出指定目录下面的带有某些后缀的文件
----------------------------------------------
/** * 深度遍历目录下面的内容,递归,包含子目录的内容 * @param dir */ public static void showDir(File dir){ File[] files = dir.listFiles(); for(int i=0;i<files.length;i++){ if(dir.exists()){ if(files[i].isDirectory()){//如果是文件夹,继续列出指定子文件夹的内容 showDir(files[i]);//使用递归 }else{ System.out.println(files[i].getName());//是文件 } } } }
----------------------------------------------
RandomAccessFile是Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。与普通的输入/输出流不同的是,RandomAccessFile支持"随机访问"的方式,程序可以直接跳转到文件的任意地方来读写数据。
RandomAccessFile支持对文件的读取和写入随机访问(其他的输入流或者输出流只能进行一种操作,要么输入,要么输出)。RandomAccessFile把随机访问的文件对象看作存储在文件系统中的一个大型 byte 数组,然后通过指向该 byte 数组的光标或索引(即:文件指针 FilePointer)在该数组任意位置读取或写入任意数据。输入操作从文件指针开始读取字节(以字节为单位进行读取),并随着对字节的读取而前移此文件指针。如果RandomAccessFile访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。
RandomAccessFile可以自由访问文件的任意位置。
RandomAccessFile允许自由定位文件记录指针。
RandomAccessFile只能读写文件而不是流。
RandomAccessFile类中包含一个游标(文件指针:隐含数组的索引),用以标识当前读写处的位置(文件指针开始位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节。),RandomAccessFile可以自由移动该游标。
RandomAccessFile包含了如下两个方法来操作文件记录指针。
➢ long getFilePointer():返回文件记录指针的当前位置。(native方法)
➢ void seek(long pos):将文件记录指针定位到pos位置。(调用本地方法seek0)
RandomAccessFile类有两个构造器:
RandomAccessFile(String name, String mode):
RandomAccessFile(File file, String mode)
创建一个随机访问文件的流:
(1)构造器1中name会转换为构造器2中的file,RandomAccessFile(String name, String mode)等价于RandomAccessFile(new File(name), String mode)
(2)mode – 访问模式
➢ "r":以只读方式打开指定文件。如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常。
➢ "rw":以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
➢ "rws":以读、写方式打开指定文件。相对于"rw"模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
➢ "rwd":以读、写方式打开指定文件。相对于"rw"模式,还要求对文件内容的每个更新都同步写入到底层存储设备。
"rws"和"rwd"模式的工作方式与FileChannel类的force(boolean)方法非常相似,传递true和false两种参数来实现,但它们始终适用于每个 I/O 操作,因此通常更有效。 如果文件驻留在本地存储设备上,则当此类方法的调用返回时,可以保证该调用对文件所做的所有更改都将写入该设备。 这对于确保在系统崩溃时不会丢失关键信息非常有用。 如果文件不在本地设备上,则不提供此类保证。
“rwd”模式可用于减少执行的 I/O 操作的数量。 使用“rwd”只需要更新要写入存储的文件内容; 使用“rws”需要更新文件内容及其要写入的元数据,这通常至少需要再进行一次低级 I/O 操作。
如果有安全管理器,则调用其checkRead方法,并以file参数的路径名作为其参数,以查看是否允许对文件进行读访问。 如果模式允许写入,还会使用路径参数调用安全管理器的checkWrite方法,以查看是否允许对文件进行写访问。
----------------------------------------------
InputStreamReader:是字节流通向字符流的桥梁;
OutputStreamWriter 是字符流通向字节流的桥梁;
----------------------------------------------
select、poll、epoll 区别总结:
1、支持一个进程所能打开的最大连接数
select
单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
poll
poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
epoll
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
2、FD剧增后带来的IO效率问题
select
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll
同上
epoll
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
3、 消息传递方式
select
内核需要将消息传递到用户空间,都需要内核拷贝动作
poll
同上
epoll
epoll通过内核和用户空间共享一块内存来实现的。
总结:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
----------------------------------------------
AIO、BIO、NIO的区别
IO模型主要分类:
- 同步(synchronous) IO和异步(asynchronous) IO
- 阻塞(blocking) IO和非阻塞(non-blocking)IO
- 同步阻塞(blocking-IO)简称BIO
- 同步非阻塞(non-blocking-IO)简称NIO
- 异步非阻塞(synchronous-non-blocking-IO)简称AIO
1.BIO (同步阻塞I/O模式)
数据的读取写入必须阻塞在一个线程内等待其完成。
这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
2.NIO(同步非阻塞)
同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
3.AIO (异步非阻塞I/O模型)
异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
4.IO与NIO区别:

5.同步与异步的区别:
同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生。
异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。
6.阻塞和非阻塞
阻塞:传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或者write()方法时,该线程将被阻塞,直到有一些数据读读取或者被写入,在此期间,该线程不能执行其他任何任务。在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。
非阻塞:Java
NIO是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此NIO可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
7.BIO、NIO、AIO适用场景
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
NIO的3个核心概念
NIO重点是把Channel(通道),Buffer(缓冲区),Selector(选择器)三个类之间的关系弄清楚。
1.缓冲区Buffer
Buffer是一个对象。它包含一些要写入或者读出的数据。在面向流的I/O中,可以将数据写入或者将数据直接读到Stream对象中。
在NIO中,所有的数据都是用缓冲区处理。这也就本文上面谈到的IO是面向流的,NIO是面向缓冲区的。
缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。
最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:
- ByteBuffer:字节缓冲区
- CharBuffer:字符缓冲区
- ShortBuffer:短整型缓冲区
- IntBuffer:整型缓冲区
- LongBuffer:长整型缓冲区
- FloatBuffer:浮点型缓冲区
- DoubleBuffer:双精度浮点型缓冲区
缓冲区的四个核心属性
capacity:容量,表示缓冲区中最大存储数据的容量。一旦申明不能改变。
limit:界限,表示缓冲区中可以操作数据的大小。(limit后面的数据不能读写)
position:位置,表示缓冲区中正在操作数据的位置。
mark:标记,表示记录当前position的位置。可以通过rest()恢复到mark的位置
0=<mark<=position <= limit <= capacity
直接缓冲区与非直接缓冲区
非直接缓冲区:通过allocate()方法分配非直接缓冲区,这个缓存是建立在JVM的内容中的。
直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。
2.通道Channel
Channel是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过Channel读取和写入。
通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。
因为Channel是全双工的,所以它比流更好地映射底层操作系统的API,特别是在UNIX网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。
Channel有四种实现:
- FileChannel:是从文件中读取数据。
- DatagramChannel:从UDP网络中读取或者写入数据。
- SocketChannel:从TCP网络中读取或者写入数据。
- ServerSocketChannel:允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。
获取通道的方法
<1>下面几个类可以通过getChannel()获得Channel
本地IO:
- FileInputStream/FileOutputStream
- RandomAccessFile
网络IO:
- Socket
- ServerSocket
- DatagramSocket
<2>通道的静态方法open()
<3>File工具类的newByteChannel()
3.多路复用器Selector
Selector选择器可以监听多个Channel通道感兴趣的事情(read、write、accept(服务端接收)、connect,实现一个线程管理多个Channel,节省线程切换上下文的资源消耗。Selector只能管理非阻塞的通道,FileChannel是阻塞的,无法管理。
关键对象
- Selector:选择器对象,通道注册、通道监听对象和Selector相关。
- SelectorKey:通道监听关键字,通过它来监听通道状态。
监听注册
监听注册在Selector
socketChannel.register(selector, SelectionKey.OP_READ);
监听的事件有
- OP_ACCEPT: 接收就绪,serviceSocketChannel使用的
- OP_READ: 读取就绪,socketChannel使用
- OP_WRITE: 写入就绪,socketChannel使用
- OP_CONNECT: 连接就绪,socketChannel使用
selector常用的方法
方 法 | 描 述 |
---|---|
Set keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的 |
SelectionKey | 集合中,该方法返回这些 Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
(3)SelectionKey常用的方法
方 法 | 描 述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
NIO的应用和框架
1.NIO的应用
Java NIO成功的应用在了各种分布式、即时通信和中间件Java系统中,充分的证明了基于NIO构建的通信基础,是一种高效,且扩展性很强的通信架构。
例如:Dubbo(服务框架),就默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
Jetty、Mina、Netty、Dubbo、ZooKeeper等都是基于NIO方式实现。
- Mina出身于开源界的大牛Apache组织
- Netty出身于商业开源大亨Jboss
- Dubbo阿里分布式服务框架
2.NIO框架
特别是Netty是目前最流行的一个Java开源框架NIO框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
相比JDK原生NIO,Netty提供了相对十分简单易用的API,非常适合网络编程。
Mina和Netty这两个NIO框架的创作者是同一个人Trustin Lee 。Netty从某种程度上讲是Mina的延伸和扩展,解决了一些Mina上的设计缺陷,也优化了一下Mina上面的设计理念。
另一方面Netty相比较Mina的优势:
- 更容易学习
- API更简单
- 详细的范例源码和API文档
- 更活跃的论坛和社区
- 更高的代码更新维护速度
Netty无疑是NIO框架的首选,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。
----------------------------------------------
零拷贝
一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。针对操作系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各种零拷贝技术极大地提升了特定应用程序的性能,并且使得这些应用程序可以更加有效地利用系统资源。这种性能的提升就是通过在数据拷贝进行的同时,允许 CPU 执行其他的任务来实现的。
2.数据拷贝
2.1. 传统方式下的数据拷贝原理
①一个read系统调用后,DMA执行了一次数据拷贝,从磁盘到内核空间
②read结束后,发生第二次数据拷贝,由cpu将数据从内核空间拷贝至用户空间
③send系统调用,cpu发生第三次数据拷贝,由cpu将数据从用户空间拷贝至内核空间(socket缓冲区)
④send系统调用结束后,DMA执行第四次数据拷贝,将数据从内核拷贝至协议引擎
2.1. 基于NIO的数据零拷贝(sendfile)
----------------------------------------------
netty
----------------------------------------------
mina
----------------------------------------------
----------------------------------------------
----------------------------------------------
----------------------------------------------
----------------------------------------------
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
2021-12-24 Java架构师需要掌握的技术栈
2021-12-24 Java技术栈总结-提升
2021-12-24 Java技术栈总结-基础
2021-12-24 高性能的RTC服务器OpenFire
2021-12-24 MySQL存储引擎,锁,优化简述
2021-12-24 java实现常见查找算法
2021-12-24 Java的几种常见排序算法