Java 的 I/O 大概可以分成以下几类:
- 磁盘操作:File
- 字节操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 对象操作:Serializable
- 网络操作:Socket
- 新的输入/输出:NIO
装饰者模式
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
InputStream 是抽象组件;
FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
实例化一个具有缓存功能的字节流对象
1 | FileInputStream fileInputStream = new FileInputStream(filePath); |
编码与解码
java.nio.charset.StandardCharsets#UTF_8
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
Java 的内存编码使用双字节编码 UTF-16be;一个中文或者一个英文都能使用一个 char 来存储。
乱码后一定能还原吗?
当字符集不支持某个字符时,编码错乱后就不能还原了,因为编码信息已经丢失,变成了不相关的字符。
磁盘操作
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
字节操作
实现文件复制
1 | FileInputStream in=new FileInputStream(src); |
字符操作
InputStreamReader 实现从字节流解码成字符流;
OutputStreamWriter 实现字符流编码成为字节流。
实现逐行输出文本文件的内容
1 | FileReader fileReader = new FileReader(filePath); |
对象操作
序列化就是将一个对象转换成字节序列,方便存储和传输。该类需要实现 Serializable 接口。
序列化:ObjectOutputStream.writeObject()
反序列化:ObjectInputStream.readObject()
网络操作
Java 中的网络支持:
- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
- URL:统一资源定位符;
- Sockets:使用 TCP 协议实现网络通信;
- Datagram:使用 UDP 协议实现网络通信。
NIO
通道
通道包括以下类型:
- FileChannel:从文件中读写数据;
- DatagramChannel:通过 UDP 读写网络中数据;
- SocketChannel:通过 TCP 读写网络中数据;
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
缓冲区
也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
选择器
NIO(Selector) 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的(多种)事件,从而让一个线程就可以处理多个事件。
创建选择器
1
Selector selector = Selector.open();
将通道注册到选择器上
1
2
3ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器(Selector)就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
- SelectionKey.OP_READ 1 << 0;
- SelectionKey.OP_WRITE 1 << 2;
- SelectionKey.OP_CONNECT 1 << 3;
- SelectionKey.OP_ACCEPT 1 << 4;
可以看出每个事件可以被当成一个位域,从而组成事件集整数。
例如:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
监听事件
1
2//使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
int num = selector.select();获取到达的事件
1
2
3
4
5
6
7
8
9
10
11Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。1
2
3
4
5
6
7
8
9
10
11
12
13
14while (true) {
int num = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}
套接字 NIO 实例
I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
1 | public class NIOServer { |
Unix 有五种 I/O 模型:
完全阻塞(第一阶段阻塞):阻塞、复用
第二阶段阻塞:非阻塞、信号
都不阻塞:异步
ServerSocketChannel.configureBlocking(false);与 SocketChannel.configureBlocking(false); 作用有啥不同?
都继承自 AbstractSelectableChannel,对 Selector 的意义应该一样。
键盘输入 new Scanner(System.in)
1 | //以换行符作为结束标记; |
文件操作
1 | Path projectRoot = Files.createTempDirectory("project-"); |
meituan-NIO浅析
原文链接:Java NIO浅析
NIO(Non-blocking I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础。
传统 BIO 为每个连接创建一个线程,但线程是很”贵”的资源,主要表现在:
- 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
- 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
- 线程的切换成本是很高的。
- 容易造成锯齿状的系统负载。
BIO 适用场景:活动连接数小于单机1000 。
存疑:
然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。(Selector,channel 怎么工作)
Selector.wakeup()
主要作用:解除阻塞在Selector.select()/select(long)上的线程,立即返回。
为什么要唤醒?:注册了新的channel或者事件。channel关闭,取消注册。优先级更高的事件触发(如定时器事件),希望及时处理。