Java I/O
- Java IO核心就是流。流只能是单向的,要么输入要么输出。只能选其一。
- java.io中最为核心的一个概念是流(stream),面向流的编程。Java中,一个流要么是输入流,要么是输出流,不可能同时既是输入流又是输出流。
java NIO
NIO主要内容
- java.nio中拥有3个核心概念:Selector,Channel与Buffer。在java.nio中,我们是面向块(block)或是缓冲区(buffer)编程的。Buffer本身就是一块内存,底层实现上,它实际上是个数组。数据的读、写都是通过Buffer来实现的。
- 除了数组之外,Buffer还提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。
- Java中的7种原生数据类型都有各自对应的Buffer类型,如IntBuffer,LongBuffer,ByteBuffer及CharBuffer等等。没有BooleanBuffer。
- Channel指的是可以向其写入数据或是从中读取数据的对象,它类似于java.io中的stream,所有数据的读写都是通过Buffer来进行的,永远不会出现直接向channel写入数据的情况,或是直接从channel读取数据的情况。
- 与stream不同的是,Channel是双向的,一个流只可能是InputStream或是OutputStream,Channel打开后则可以进行读取、写入或是读写。
- 由于Channel是双向的,即全双工通信,因此它能更好地反映出底层操作系统的真实情况;在Linux系统中,底层操作系统的通道就是双向的。
- 在Java NIO(New Input/Output)中,Channel(通道)是一个抽象概念,代表一个与I/O源或目标之间的连接。它可以是文件、套接字、管道等。Channel提供了一种高效的、非阻塞的I/O操作方式,与传统的Java I/O(也称为IO流)相比,它更适用于处理大量的并发连接。
- 可以把Channel类比成网线,然后程序可以从网线里面拿数据,也可以往网线里面传数据。
代码样例
- 输入
public class NIOInTest {
public static void main(String[] args) throws Exception {
File file = new File("C:/Users/25852/Desktop/HTTP请求.jmx");
FileInputStream inputStream = new FileInputStream(file);
FileChannel fileChannel = inputStream.getChannel();// 流变channel
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
fileChannel.read(byteBuffer);//从channel中读数据给buffer
byteBuffer.flip();//翻转 从写变成读模式
while(byteBuffer.hasRemaining()){//buffer 中是否还有元素
byte b = byteBuffer.get();
System.out.print((char) b);
}
inputStream.close();
}
}
- 输出
public class NIOOutTest {
public static void main(String[] args) throws Exception {
File file = new File("test.txt");
FileOutputStream outputStream = new FileOutputStream(file);
FileChannel fileChannel = outputStream.getChannel();// 流变 channel
byte[] message = "hello world ,It's me".getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(message.length);
byteBuffer.put(message);// 数据都要装入buffer
byteBuffer.flip(); //Buffer 默认是读模式,需要反转
fileChannel.write(byteBuffer);// 将buffer写入channel
System.out.println("输出成功");
outputStream.close();
}
}
- 可以发现不管输入还是输出都用的是FileChannel,不像流还要指定In还是Out,我Channel都可以。所以说是双向的。
- 读要将数据从channel读入buffer,再将数据从buffer中读出来。
- 写要将数据写入buffer,再将buffer中的数据写入channel。
关于NIO Buffer 三个属性: position、limit、capacity。
直接读Buffer的JDK源码就可知其含义
- 英文版
- 译文
- 特定原始类型数据的容器。除了Boolean
- 缓冲区是特定原始类型元素的线性有限序列。 除了内容之外,缓冲区的基本属性是它的capacity(容量), limit(限制), and position(位置)。
- 缓冲区的capacity是它包含的元素数。 缓冲区的capacity永远不会为负且永远不会改变。
- 缓冲区的limit是不应读取或写入的第一个元素的索引。 缓冲区的limit永远不会为负,也永远不会大于其capacity。
- 缓冲区的position是要读取或写入的下一个元素的索引。 缓冲区的位置永远不会为负,也永远不会大于其limit。
- 每个非布尔基本类型都有这个类的一个子类。
- 传输数据
- 该类的每个子类都定义了两类get和put操作:
- 相对操作从当前位置开始读取或写入一个或多个元素,然后将position增加传输的元素数量。 如果请求的传输超过限制,则相对获取操作会抛出BufferUnderflowException而相对放置操作会抛出BufferOverflowException ; 在任何一种情况下,都不会传输数据。
- 绝对操作采用显式元素索引并且不影响position。 如果 index 参数超出限制,则绝对get和put操作会抛出IndexOutOfBoundsException 。
- 当然,数据也可以通过适当channel的 I/O 操作传入或传出缓冲区,这些操作总是相对于当前position。
- 该类的每个子类都定义了两类get和put操作:
- 标记和重置
- 缓冲区的mark是在调用reset方法时将其position重置到的索引。 标记并不总是被定义,但当它被定义时,它永远不会是负数,也永远不会大于position。 如果定义了标记,则在将位置或限制调整为小于标记的值时将丢弃该标记。 如果未定义标记,则调用reset方法会导致抛出InvalidMarkException 。
mark 方法要和reset方法要搭配使用。可以使position回到mark的地方。
- 不变量
- 以下不变量适用于mark、position、limit和capacity值:
- 0 <=mark<=position<=limit<=capacity
- 新创建的缓冲区始终具有零位置和未定义的标记。 初始限制可能为零,也可能是某个其他值,具体取决于缓冲区的类型及其构造方式。 新分配的缓冲区的每个元素都初始化为零。
- 以下不变量适用于mark、position、limit和capacity值:
- 清除、翻转和倒带
- 除了用于访问position、limit和capacity值以及用于标记和重置的方法之外,该类还定义了以下对缓冲区的操作:
- clear 使缓冲区为新的通道读取或相对放置操作序列做好准备:它将limit置为capacity和position设置为零。
- flip 使缓冲区为新的通道写入或相对获取操作序列做好准备:它将limit设置为当前position,然后将position设置为零。
- rewind 使缓冲区准备好重新读取它已经包含的数据:它保持limit不变并将position设置为零。
- 除了用于访问position、limit和capacity值以及用于标记和重置的方法之外,该类还定义了以下对缓冲区的操作:
- 只读缓冲区
- 每个缓冲区都是可读的,但并非每个缓冲区都是可写的。 每个缓冲区类的变异方法被指定为可选操作,当在只读缓冲区上调用时将抛出ReadOnlyBufferException 。 只读缓冲区不允许更改其内容,但其标记、位置和限制值是可变的。 缓冲区是否为只读可以通过调用它的isReadOnly方法来确定。
- 线程安全
- 多个并发线程使用缓冲区是不安全的。 如果一个缓冲区被多个线程使用,那么对缓冲区的访问应该有适当的同步控制。
- 链式调用
- 此类中没有返回值的方法被指定为返回调用它们的缓冲区。 这允许链接方法调用; 例如,语句序列
b.flip(); b.position(23); b.limit(42);
- 可以用单一的、更紧凑的语句代替
b.flip().position(23).limit(42);
- 此类中没有返回值的方法被指定为返回调用它们的缓冲区。 这允许链接方法调用; 例如,语句序列
调用clear并没有清除数据,只是写的时候会覆盖原来的数据,让三变量回到刚创建buffer时的状态。
- 复制文件的代码
public class NIOCopyTest {
public static void main(String[] args) throws Exception{
File file = new File("test.txt");
FileInputStream fileInputStream = new FileInputStream(file);//输流
FileOutputStream fileOutputStream = new FileOutputStream("out.txt");//目的地
FileChannel channel = fileInputStream.getChannel();//获取输入channel
ByteBuffer buffer = ByteBuffer.allocate(10);
while (channel.read(buffer)!=-1){//会继续读输入channel中的内容
//channel.read(buffer);
buffer.flip();
channel = fileOutputStream.getChannel();//输出channel
System.out.println("before write position :"+buffer.position());
for (int i = 0; i < buffer.limit(); i++) {//输出一下buffer的内容
System.out.print((char) buffer.array()[i]);
}
System.out.println();
channel.write(buffer);
System.out.println("after write position :"+buffer.position());//write 之后position也会变化
buffer.clear();
channel = fileInputStream.getChannel();//输入channel
}
fileInputStream.close();
fileOutputStream.close();
channel.close();
// channel.read(buffer);
// buffer.flip();
// channel = fileOutputStream.getChannel();
// channel.write(buffer);
}
}
绝对方法与相对方法的含义:
- 相对方法:limit值与position值会在操作时被考虑到。
- 就是会操作limit和position,比如clear(),flip()
- 绝对方法:完全忽略掉limit值与position值。
- 不改变limit和position,比如get()、put(),由jdk底层实现,考虑是IOUtil,没有开源
其它的方法
- slice()分割的buffer和原buffer共享的。
- buffer还有只读模式。我们可以随时将一个普通Buffer调用asReadOnlyBuffer方法返回一个只读Buffer,但不能将一个只读Buffer转换为读写Buffer,只读buffer的put方法直接抛异常。
DirectBuffer 和 HeapBuffer
- DirectBuffer通过Unsafe类中的方法,创建内存区域,native方法,c语言的malloc(int c)。
- Buffer中的long address;存直接内存地址。堆中引用指向堆外内存。
HeapBuffer为什么要从Heap拷贝一份buffer到直接内存,操作系统直接访问不行吗?
- IO都要和硬件关联,操作系统可以直接访问堆中的内容,但是,IO较慢,在堆中buffer会出现变故,比如,GC,有一个标记-整理算法,会压缩空间,就有可能导致buffer内存位置改变,就导致IO设备错乱,导致数据不准确。
- 让buffer对象固定不动,也不现实,不GC肯定也不行。
- 还是把堆中的buffer拷贝一份到直接内,稳妥,内存地址不容易变,在内存中拷贝也不慢(相对IO操作来说简直小巫见大巫),而且也不会被JVM GC。
但是JVM 在拷贝buffer时,JVM已经支持不GC了,稳的很。
- long address会随着DirectBuffer对象的销毁而销毁,操作系统发现没有引用了就会回收直接内存中的buffer,即不会发生内存泄漏。稳的很。
unsafe.freeMemory(address); address = 0;
要点
- Java的Buffer有两种类型:Heap Buffer和Direct Buffer。
- Heap Buffer是在JVM堆上分配的字节数组,Direct Buffer是在JVM外部分配的直接内存。如果将Heap Buffer中的数据写入磁盘或者网卡,JVM会先在JVM外部申请一个内存,将要写入的数据拷贝到JVM外部申请的内存中,然后再写入到磁盘或者网卡中。这样就会有一次额外的内存拷贝,会降低性能。
- 而Direct Buffer可以避免这种内存拷贝,因为它本身就是在JVM外部分配的直接内存。
- Heap Buffer和Direct Buffer的优缺点:
| 类型 | 优点 | 缺点 | 适用场景 |
| --- | --- | --- | --- |
| Heap Buffer | 分配和回收速度快,方便操作,受JVM管理 | 进行I/O操作时需要额外的数据拷贝,影响性能 | 主要用于存储和处理数据,不涉及I/O操作 |
| Direct Buffer | 进行I/O操作时无需额外的数据拷贝,提高效率,避免内存碎片 | 分配和回收速度慢,不方便操作,不受JVM管理 | 主要用于网络或文件的I/O操作,数据量大或频繁 |
- 如果你的缓冲区既要用于存储和处理数据,又要用于网络或文件的I/O操作,那么你可以根据实际情况进行权衡,比如可以使用Heap Buffer来存储和处理数据,然后在需要进行I/O操作时,使用wrap或者duplicate方法来创建一个对应的Direct Buffer,这样可以减少Direct Buffer的创建和销毁开销,也可以减少数据拷贝开销。
- Direct Buffer的缺点有以下几点:
- Direct Buffer的创建和销毁比Heap Buffer要耗费更多的资源,因为它需要调用操作系统的malloc和free函数,而不是由JVM管理。
- Direct Buffer不受JVM的垃圾回收机制的管辖,它需要依靠一个Cleaner对象来释放内存,这个过程可能会延迟或者失败。
- Direct Buffer的大小受到操作系统的限制,如果申请的内存超过了操作系统允许的最大值,就会抛出OutOfMemoryError异常。
其他一些样例
- 修改文件内容,文件映射,将文件映射到内存,改了内存中的也就改了硬盘中的
public class FileMapTest {//修改文件特定位置的内容
public static void main(String[] args) throws Exception{
File file = new File("test.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");//有文件指针
FileChannel fileChannel = randomAccessFile.getChannel();
FileChannel.MapMode mode;
MappedByteBuffer mappedByteBuffer = fileChannel.map(MapMode.READ_WRITE, 0, file.length());
mappedByteBuffer.put(0,"H".getBytes()[0]);//修该某文件对应索引的位置。
mappedByteBuffer.put(2,"H".getBytes()[0]);
mappedByteBuffer.put(3,"H".getBytes()[0]);
mappedByteBuffer.put(4,"H".getBytes()[0]);
System.out.println("修改成功");
fileChannel.close();
}
}
- 文件锁,可以锁住文件的部分内容,还可以设置是否为共享锁
public class FileLockTest {
public static void main(String[] args) throws Exception{
RandomAccessFile randomAccessFile = new RandomAccessFile("test.txt", "rw");
FileChannel fileChannel = randomAccessFile.getChannel();
FileLock fileLock = fileChannel.lock(2,9,true);//锁住索引2 到 9的内容,且是共享锁
System.out.println("valid :" +fileLock.isValid());
System.out.println("isShared :"+fileLock.isShared());
fileLock.release();//释放锁
fileChannel.close();
}
}
- 内存映射和普通的文件读写操作有什么不同?
- 回答:内存映射的方式和普通的文件读写操作的区别在于,内存映射可以将部分或全部文件映射到进程的虚拟地址空间中,使得进程可以直接通过访问虚拟地址来访问文件内容,而不是通过系统调用进行文件读写操作。这种方式可以提高文件读写的效率,并且减少了应用程序和操作系统之间的数据传输次数,从而提高了系统的整体性能。此外,内存映射还可以实现多个进程之间的共享文件,从而方便协同工作和数据共享。
- 那它还需要将硬盘中的文件加载进内存吗?
- 回答:是的,内存映射仍然需要将硬盘中的文件加载进内存。在内存映射的过程中,进程首先需要打开文件,并且通过系统调用将文件内容加入到操作系统的内核缓存中。然后,操作系统会将文件内容加载到进程的虚拟地址空间中,使得进程可以直接访问文件内容。因此,内存映射仍然需要将文件加载到内存中来进行访问,但是与普通的文件读写操作相比,内存映射可以通过减少数据传输次数来提高文件读写的效率。
- 普通的文件读写进程每次访问都会产生系统调用,而内存映射不需要频繁的系统调用。
关于Buffer的Scattering(散开)与Gathering(聚集)
Scattering
- 读的时候,将一个channel中的内容放置到不同buffer中,要将每个buffer填满,再填下一个buffer。是按顺序的。一个channel对应多个buffer。
Gathering
- 写的时候,按buffer的循序,填入channel。多个buffer对应一个channel。
一般有buffer数组。
使用场景
- 在定义网络协议时,就可以将协议的不同组成Scattering成不同的buffer:
- 因为协议的各部分内容大小是固定的。
- 将协议名称放一个buffer,请求头放一个buffer,请求体放一个buffer。
- 解析时就很方便。
样例
- 样例
- win10 可以用Telnet当客户端, Telnet 127.0.0.1 8899
public class BufferArrayTest {//win10 可以用Telnet Telnet 127.0.0.1 8899
public static void main(String[] args) throws Exception{
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8899));
int messageLength = 2 + 3 + 4;
ByteBuffer[] byteBuffers = new ByteBuffer[3];
byteBuffers[0] = ByteBuffer.allocate(2);
byteBuffers[1] = ByteBuffer.allocate(3);
byteBuffers[2] = ByteBuffer.allocate(4);
SocketChannel socketChannel = serverChannel.accept();//阻塞
while (true){
int bytesRead = 0;
while (bytesRead < messageLength){//接收
long read = socketChannel.read(byteBuffers);//Scattering
bytesRead +=read;
Arrays.asList(byteBuffers).forEach(System.out::println);
}
Arrays.asList(byteBuffers).forEach(Buffer::flip);
long bytesWritten = 0;
while (bytesWritten < messageLength){//回写
long write = socketChannel.write(byteBuffers);//Gathering
bytesWritten +=write;
}
Arrays.asList(byteBuffers).forEach(Buffer::clear);
System.out.println("bytesRead "+bytesRead+",bytesWritten "+bytesWritten+" ,messageLength "+messageLength);
}
}
}
Socket网络编程
socket绑定的端口号并不是正在双方交互的端口号
- 而是选一个空闲的端口号,进行交互。代码中写的端口号只有标记作用。
每连接一个socket就会创建线程。
- 如果连接太多,线程就爆满。
NIO网络编程
聊天小程序
- server告诉所有client哪个client发送的消息。
- server
public class NioServer {
private static Map<String, SocketChannel> clientMap = new HashMap<>();//记录客户端信息,方便内容分发
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8899));
Selector selector = Selector.open();
/*
当accept触发时,就可以触发对应的事件逻辑,
是将channel绑定到selector ,注册会有 SelectionKey生成
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//一般以连接事件为起源
while (true){
selector.select();//阻塞,等待事件发生
Set<SelectionKey> selectionKeys = selector.selectedKeys();//返回已发生的注册事件
selectionKeys.forEach(key ->{//判断事件类型,进行相应操作
final SocketChannel client;
try {
if (key.isAcceptable()){//根据key获得channel
//之所以转换ServerSocketChannel,因为前面注册的就是这个类
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
client = serverChannel.accept();//新的channel 和客户端建立了通道
client.configureBlocking(false);//非阻塞
client.register(selector,SelectionKey.OP_READ);//将新的channel和selector,绑定
String clientKey = "【"+ UUID.randomUUID() +"】";//用UUID,标识客户端client
clientMap.put(clientKey,client);
//完成客户端注册
}else if (key.isReadable()){//是否有数据可读
client = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if (count>0){
readBuffer.flip();
Charset charset = StandardCharsets.UTF_8;
String receiveMassage = String.valueOf(charset.decode(readBuffer).array());
System.out.println(client +": "+receiveMassage);//显示哪个client发消息
String senderKey = null;
for (Map.Entry<String, SocketChannel> entry : clientMap.entrySet()){
if (client == entry.getValue()){
senderKey = entry.getKey();//确定哪个client发送的消息
break;
}
}
for (Map.Entry<String, SocketChannel> entry : clientMap.entrySet()){
SocketChannel channel = entry.getValue();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((senderKey + ": " + receiveMassage).getBytes());//告诉所有client ,谁发了消息,发了什么
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
//selectionKeys.clear();//处理完事件一定要移除
}catch (Exception e){
e.printStackTrace();
}finally {
selectionKeys.clear();//处理完事件一定要移除
}
});
}
}
}
- client
public class NioClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("127.0.0.1",8899));
while (true){
selector.select();//阻塞 等待事件发生
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.forEach(key ->{
try {
if (key.isConnectable()){
SocketChannel channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()){//是否正在连接
channel.finishConnect(); //结束正在连接
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((LocalDateTime.now() + " 连接成功").getBytes());
writeBuffer.flip();
channel.write(writeBuffer);//将buffer写入channel
ExecutorService service = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
service.submit(()->{//线程,从键盘读入数据
try {
while (true){
writeBuffer.clear();//清空buffer
InputStreamReader input = new InputStreamReader(System.in);
BufferedReader bufferedReader = new BufferedReader(input);
String senderMessage = bufferedReader.readLine();
writeBuffer.put(senderMessage.getBytes());
writeBuffer.flip();
channel.write(writeBuffer);
}
}catch (Exception e){
e.printStackTrace();
}
});
}
channel.register(selector,SelectionKey.OP_READ);//注册事件
}else if (key.isReadable()){//channel 有信息的输入
SocketChannel channel = (SocketChannel) key.channel();//哪个channel 触发了 read
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = channel.read(readBuffer);//server发来的
if (count > 0){
String receiveMessage = new String(readBuffer.array(),0,count);
System.out.println(receiveMessage);
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
selectionKeys.clear();//移除已经发生的事件
}
});
}
}
}
NIO 编程要点
- 服务端先open ServerChannel.设置非阻塞,channel.socket获取socket。
- 绑定端口号
- Selecto.open
- channel注册进selector
- 之后的channel从 SelectionKey.channel()获取,获取的是触发事件的channel。
- 最后要移除处理过的SelectionKey。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8899));
Selector selector = Selector.open();
/*
当accept触发时,就可以触发对应的事件逻辑,
是将channel绑定到selector ,注册会有 SelectionKey生成
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//一般以连接事件为起源
Set<SelectionKey> selectionKeys = selector.selectedKeys();//返回已发生的注册事件
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
selectionKeys.clear();//处理完事件一定要移除
关于socket和TCP/IP
我们平时说的最多的socket是什么呢,实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口。
(API),通过Socket,我们才能使用TCP/IP协议。 实际上,Socket跟TCP/IP协议没有必然的联系。Socket编程接。
在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、connect、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:
“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”