IO简介

IO:应用程序调用操作系统进行数据的读取和写入。

同步:被调用方在得到最终结果之后才会返回

异步: 被调用方先响应,然后再去计算结果,结果出来之后再通知调用方

阻塞:调用方在调用之后一直等到结果返回

非阻塞:调用方在调用之后先去执行其他的任务

阻塞、非阻塞和同步、异步的区别(阻塞、非阻塞和同步、异步其实针对的对象是不一样的)

  • 1)阻塞、非阻塞的讨论对象是调用者;
  • 2)同步、异步的讨论对象是被调用者。

JAVA普通IO与NIO

普通IO:基于流Stream来实现,一次读取一个字节,可添加缓存区,是同步阻塞的;

NIO:基于块实现,一次读取一个块,是非阻塞的;

NIO组成

NIO主要有通道缓冲区两部分,到任何一个地方的数据(或者来自任何地方的数据)都需要经过通道(Channel),缓冲区(Buffer)是一个容器对象,发送到一个通道的所有数据都必须先放到缓冲区;类似的,从通道中读取的所有数据都要先放到缓冲区

通道

通道类似IO中的流,但是通道是双向的,同一个通道既可以读也可以写还可以一边读一边写。

缓冲区

缓冲区本质是一个数组,还提供了对数据的结构化访问,可以跟踪系统的读写进程。

缓冲区有多种类型的数组,最常见的是ByteBuffer,是byte类型的。每一个Java的基本类型都有着相应的类型的缓冲区。

状态变量

NIO通过状态变量来实现缓冲区的内部统计机制,缓冲区的每一次读写都会改变状态变量

可以使用三个值表示缓冲区在任意时刻的状态:

  • position

    在读取通道中的数据到缓冲区的时候表示下一个字节在数组中存放的地址;已经读取了多少个字节 在从缓冲区写入到通道中的时候表示读取的下一个字节的地址;已经写入了多少个字节
  • limit

    在从通道中读取数据的时候表示缓冲区中所能填充数据的数量。初始的时候与capacity相等,**position总是小于等于limit**

    向通道中写数据的时候表示缓冲区中已有的数据的数量;

  • capacity

    可以存储在缓冲区的最大数据容量limit不能大于capacity

读取过程(从通道读取到缓冲区)

读取之前需要先调用clear()方法,将limit设置与capacity相同,将position设为0。

1.初始状态,limit与capacity相等,position是从0开始的

2.读取数据到缓存区,position增加,limit不变

写入过程

在写入数据之前调用flip()方法将limit设置为当前的position,将position设置为0;

2.从缓存区写入数据到通道,position增加

访问方法

访问方法是用来直接访问缓冲区中的数据的方法。

方法分为绝对的和相对的,相对的方法会造成缓冲区三个变量的改变,绝对方法不会改变变量的值。

get()方法
指定位置的方法是绝对的。
put()方法
指定位置的方法是绝对的。

NIO中的数据读写

读写概述:

从通道中读取数据:只需要创建一个缓冲区,然后让通道将数据读到缓冲区。

写入数据:创建一个缓冲区,用数据填充它,然后让通道来执行相应的写入操作。

通道的读和写都是从缓存区的角度来定义的,缓存区读入数据的时候就用的通道的read方法,写数据的时候就调用通道的写方法。

读取文件:

1
2
3
4
5
6
7
//首先创建通道,从stream中获取
FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
//将数据读取到缓冲区
fc.read( buffer );

写入文件:

1
2
3
4
5
6
7
8
9
10
11
12
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
//填充数据到缓存区
for (int ii=0; ii<message.length; ++ii) {
buffer.put( message[ii] );
}
//通知缓存区要写入到通道
buffer.flip();
//缓存区写入数据到通道
fc.write( buffer );

一边读一遍写

1
2
3
4
5
6
7
8
9
10
11
12
13
//同一个缓冲区,一边读入数据一边写入数据,读一个写一个
while (true) {
//写入缓冲区需要先清空
buffer.clear();
int r = fcin.read( buffer );

if (r==-1) {
break;
}
//写入通道需要先flip
buffer.flip();
fcout.write( buffer );
}

缓冲区的更多操作

包装和分配

1.使用静态方法allocate()分配缓冲区;

1
2
//分配一个指定大小的数组,并包装为缓冲区
ByteBuffer buffer = ByteBuffer.allocate( 1024 );

2.现有数组包装为缓冲区;

1
2
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );

缓存分区

slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。(子缓冲区与主缓冲区的数据用的是同一份)

子缓冲区的初始位置和结束位置通过position和limit来进行指定,最后调用slice方法。

1
2
3
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();

只读缓冲区

通过asReadOnlyBuffer() 方法将普通的缓冲区转换为只读缓冲区。但是不能将只读缓冲区转换为普通缓冲区。

文件锁定

锁定文件

获取文件一部分的锁。需要使用FileChannel,如果需要一个排他锁,则需要以写的方式打开文件。

1
2
3
4
5
6
7
8
RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );
FileChannel fc = raf.getChannel();
FileLock lock = fc.lock( start, end, false );

//释放锁

lock.release();

联网与异步IO

异步IO是没有阻塞的读写数据的方法。通过注册一系列事件,当事件发生的时候通知处理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class MultiPortEcho {

private int[] ports; // 存储监听的端口
private ByteBuffer echoBuffer = ByteBuffer.allocate(1024); // 缓冲区,用于存放接收到的数据

// 构造函数,接受端口数组作为参数,并初始化服务端
public MultiPortEcho(int[] ports) throws IOException {
this.ports = ports; // 保存传入的端口列表
go(); // 启动服务
}

private void go() throws IOException {
// 创建一个Selector,这个对象用于管理多个通道的I/O事件
Selector selector = Selector.open();

// 为每个端口创建ServerSocketChannel,并注册到Selector
for (int port : ports) {
// 打开一个ServerSocketChannel(非阻塞通道)
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式

// 获取ServerSocket对象,并将其绑定到指定端口
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
serverSocket.bind(address); // 绑定端口

// 将这个ServerSocketChannel注册到Selector上,关注接受连接事件(OP_ACCEPT)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Listening on port " + port);
}

// 无限循环,监听事件的发生
while (true) {
// 阻塞,直到至少有一个事件发生
int num = selector.select(); // 返回发生的事件数

// 获取所有已就绪的SelectionKey
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();

// 遍历所有已就绪的SelectionKey
while (iterator.hasNext()) {
SelectionKey key = iterator.next();

// 如果事件是接收连接
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
// 获取ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

// 接受客户端连接,返回一个SocketChannel(代表与客户端的连接)
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); // 设置为非阻塞模式

// 将新连接的SocketChannel注册到Selector上,关注读取事件(OP_READ)
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from " + socketChannel);
}
// 如果事件是读取数据(客户端发来数据)
else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
// 获取当前就绪的SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();

int bytesEchoed = 0; // 记录回显的字节数

// 循环读取数据并回显
while (true) {
echoBuffer.clear(); // 每次读取前清空缓冲区

// 读取数据到缓冲区
int bytesRead = socketChannel.read(echoBuffer);

// 如果读取到的数据为-1,表示客户端关闭了连接
if (bytesRead == -1) {
socketChannel.close(); // 关闭连接
System.out.println("Connection closed by client: " + socketChannel);
break; // 跳出当前的while循环
}

if (bytesRead > 0) {
echoBuffer.flip(); // 切换缓冲区模式:准备写入数据

// 将缓冲区中的数据写回客户端,完成回显
while (echoBuffer.hasRemaining()) {
socketChannel.write(echoBuffer); // 写入数据到客户端
bytesEchoed += bytesRead; // 累计写入的字节数
}
}
}

// 打印已回显的字节数
System.out.println("Echoed " + bytesEchoed + " bytes from " + socketChannel);
}

iterator.remove(); // 处理完当前的SelectionKey后,移除它
}
}
}

// main方法,用于启动服务
public static void main(String[] args) throws IOException {
// 如果没有传递任何端口参数,打印用法并退出
if (args.length == 0) {
System.err.println("Usage: java MultiPortEcho port [port port ...]");
System.exit(1);
}

// 将传入的端口参数转换为整数数组
int[] ports = new int[args.length];
for (int i = 0; i < args.length; i++) {
ports[i] = Integer.parseInt(args[i]);
}

// 创建并启动MultiPortEcho服务器
new MultiPortEcho(ports);
}
}

字符处理

可以直接把缓冲区丢入编码和解码器中,将会返回对应格式的缓冲区;

1
2
3
4
5
6
7
//新建字符集
Charset latin1 = Charset.forName( "ISO-8859-1" );
CharsetDecoder decoder = latin1.newDecoder();
CharsetEncoder encoder = latin1.newEncoder();
CharBuffer cb = decoder.decode( inputData );
ByteBuffer outputData = encoder.encode( cb );
//使用解码后的数据再进行写入