io

Java 的 I/O 大概可以分成以下几类:

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 网络操作:Socket
  • 新的输入/输出:NIO

装饰者模式

Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
InputStream 是抽象组件;
FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
实例化一个具有缓存功能的字节流对象

1
2
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

编码与解码

java.nio.charset.StandardCharsets#UTF_8
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
Java 的内存编码使用双字节编码 UTF-16be;一个中文或者一个英文都能使用一个 char 来存储。
乱码后一定能还原吗?
当字符集不支持某个字符时,编码错乱后就不能还原了,因为编码信息已经丢失,变成了不相关的字符。

磁盘操作

File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。

字节操作

实现文件复制

1
2
3
4
5
6
7
8
9
10
11
FileInputStream in=new FileInputStream(src);
FileOutputStream out=new FileOutputStream(dist);
byte[]buffer=new byte[20*1024];
int cnt;
while((cnt=in.read(buffer,0,buffer.length))!=-1){
out.write(buffer,0,cnt);
}
//使用流一定要记得 flush,close
in.close();
out.flush();
out.close();

字符操作

InputStreamReader 实现从字节流解码成字符流;
OutputStreamWriter 实现字符流编码成为字节流。
实现逐行输出文本文件的内容

1
2
3
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line = bufferedReader.readLine();

对象操作

序列化就是将一个对象转换成字节序列,方便存储和传输。该类需要实现 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. 创建选择器

    1
    Selector selector = Selector.open();
  2. 将通道注册到选择器上

    1
    2
    3
    ServerSocketChannel 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. 监听事件

    1
    2
    //使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
    int num = selector.select();
  2. 获取到达的事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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();
    }
  3. 事件循环
    因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    while (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
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
public class NIOServer {
public static void main(String[] args) throws IOException {
// 仅监听了两种事件:OP_ACCEPT,OP_READ
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

// TCP 服务端
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);

while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {

ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);

// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);

} else if (key.isReadable()) {

SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}

private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();

while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
public class NIOClient {

public static void main(String[] args) throws IOException {
// TCP 客户端
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}

Unix 有五种 I/O 模型:

完全阻塞(第一阶段阻塞):阻塞、复用
第二阶段阻塞:非阻塞、信号
都不阻塞:异步
ServerSocketChannel.configureBlocking(false);与 SocketChannel.configureBlocking(false); 作用有啥不同?
都继承自 AbstractSelectableChannel,对 Selector 的意义应该一样。

键盘输入 new Scanner(System.in)

1
2
3
4
5
//以换行符作为结束标记;
String str = scanner.next();int a = scanner.nextInt(); //用空格作为取下个字符的标志
String str = scanner.nextLine();//整行读取,换行符作为结束标记
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
str = bf.readLine();

文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Path projectRoot = Files.createTempDirectory("project-");
Path projectDirectory = projectRoot.resolve(description.getBaseDirectory());
Files.createDirectories(projectDirectory);

Path pomFile = Files.createFile(projectDirectory.resolve("pom.xml"));
Writer out = Files.newBufferedWriter(pomFile);
out.write(string.toCharArray(), 0, string.length());

//spring 下复制文件
StreamUtils,FileCopyUtils
Resource resource = new DefaultResourceLoader().getResource("classpath:xxxxx");
FileCopyUtils.copy(resource.getInputStream(), Files.newOutputStream(pomFile, StandardOpenOption.APPEND));
//返回 String 的形式
String str = IOUtils.toString(rs.getInputStream(), Charset.defaultCharset());
//获取多个 Resource
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(null).getResources("classpath*:xxxxx");

meituan-NIO浅析

原文链接:Java NIO浅析
NIO(Non-blocking I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础。
传统 BIO 为每个连接创建一个线程,但线程是很”贵”的资源,主要表现在:

  1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
  2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
  3. 线程的切换成本是很高的。
  4. 容易造成锯齿状的系统负载。

BIO 适用场景:活动连接数小于单机1000 。

存疑:
然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。(Selector,channel 怎么工作)

Selector.wakeup()
主要作用:解除阻塞在Selector.select()/select(long)上的线程,立即返回。
为什么要唤醒?:注册了新的channel或者事件。channel关闭,取消注册。优先级更高的事件触发(如定时器事件),希望及时处理。

meituan-磁盘IO

磁盘I/O那些事