Java网络编程-NIO

网络编程实际上是进程间的通信。

1.IO

计算机处理数据的基本单位是字节。如果我们想要表示一个字符,比如char类型的,就需要使用2个字节表示,或者汉字,在utf8编码中需要3个字节表示。为了让计算机能直接处理字符,io流中就提供了字符流,即数据源是字符,在计算机中再把这些字符转换成字节进行处理。


字符流

CharArrayReader,数据源是字符数组,从这里读取数据。

CharArrayWriter,数据源是字符数组,往里面写入数据


字节流

2.NIO

即非阻塞(non-blocking) IO,也可以叫做new IO

2.1 三大组件

2.1.1 Channel

Channel:读写数据的双向通道,可以从channel中将数据读取数据,即向buffer中写入数据,也可以将buffer中的数据写入Channel。

常见的Channel有以下四种,其中FileChannel主要用于文件传输,其余三种用于网络通信

  • FileChannel(只能工作在阻塞模式)
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
1
2
3
4
5
6
7
8
9
10
11
12
//阻塞模式下........
//一个方法的调用会影响其他的方法

//下面都是在服务器中的逻辑
//服务器通道
ServerSocketChannel serverChannel = new ServerSocketChannel();
//绑定监听端口
serverChannel.bind(new InetSocketAddress(8080));
//开始监听..
SocketChannel clientChannel = serverChannel.accpet();//阻塞方法,线程会停止运行,等待一个新连接的建立,只要没有新连接,就会一直等待
//将clientChannel通道中的数据读出来,写入服务器端的buffer
int readBytes = clientChannel.read(buffer);//阻塞方法,只要没有客户端向服务器发送数据就会阻塞

Channel可以是阻塞的,也可以是非阻塞的,默认是阻塞的,可以将ServerSocketChannel手动设置为非阻塞模式。

1
2
3
4
5
6
7
8
9
//非阻塞模式下.........
serverChannel.configureBlocking(false);
...
//现在如果没有连接建立,返回的是null,非阻塞,线程会继续往下运行
SocketChannel clientChannel = serverChannel.accpet();
...
//将客户端通道也设置为非阻塞,那当调用该通道的read的时候,也不会阻塞,线程仍然会继续运行,如果没有读到数据,read返回0
clientChannel.configureBlocking(false);
int readBytes = clientChannel.read(buffer);

2.1.2 ByteBuffer

Buffer迎来缓冲读写数据,支持不同数据类型的缓冲区,有以下几种,其中使用较多的是ByteBuffer

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

所以下面都以ByteBuffer为例

2.1.2.1 属性

ByteBuffer中重要的属性:capacity(缓冲区容量),position(当前的读写位置指针,指向下一次要读写的位置),limit(读入写入限制大小指针)

bytebuffer默认是写模式,当写入数据后,flip()切换为读模式

clean()是切换写模式从头开始写,不管上次是否读完

compact()也是切换为写模式,会把上次未读完的放到缓冲区前面


反正始终记住,读前面先切换为读模式,写前切换为写模式

2.1.2.2 常见方法

  1. allocate(大小)

    创建buffer并给buffer分配空间。

    1
    2
    ByteBuffer buffer = ByteBuffer.allocate(16); //HeapByteBuffer
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(16);//DirectByteBuffer

    HeapByteBuffer(使用java堆内存分配空间,读写效率低,会受到GC影响)

    DirectByteBuffer(使用直接内存分配空间,读写效率高,不会受到GC影响)

  2. read()/put()

    向buffer写入数据。

    1
    2
    3
    // readBytes实际读到的字节数
    int readByets = channel.read(buffer);//从通道中读取数据,写入buffer
    buffer.put((byte)127);//调用buffer自己的put方法
  3. write()/get()

    从buffer中读取数据。

    1
    2
    3
    //writeBytes实际写入channel的字节数
    int writeBytes = channel.write(buffer);//从buffer中读数据,写入通道中
    byte b = buffer.get();//会让position读指针向后移动一位,get(i)只会获取索引为i的数,而不会移动指针,rewind(),把position置0,重新读
  4. bytebuffer和字符串的转换

    第一种方法将字符串放入buffer后,buffer还是写模式,如果想读,需要切换为读模式。

    第二种和第三种添加完会自动改为读模式。

2.1.3 Selector

管理Channel并监听Channel上是否有事件发生

1.阻塞直到绑定事件发生

1
int count = selector.select();

2.阻塞直到绑定事件发生,或者超时,事件单位是ms

1
int count = selector.select(Long timeout);
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
//多路复用.........

//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(16); //HeapByteBuffer
//创建channel
ServerSocketChannel serverChannel = new ServerSocketChannel();
serverChannel.configureBlocking(false);

//创建selector,管理多个channel
Selector selector = Selector.open();
//建立selector与channel的联系,即将channel注册在selector上
//SelectionKey唯一绑定一个channel,通过它可以 知道事件和哪个channel的事件
//0表示不关注任何事件
SelectionKey serverKey = serverChannel.register(selector,0,null);
//表示这个key只关注accept事件
serverKey.interestOps(SelectionKey.OP_ACCEPT);
//绑定监听端口
serverChannel.bind(new InetSocketAddress(8080));
while (true){
//开始监听,没有事件发生,线程阻塞,有事件,线程才会运行
//在事件未处理时,不会阻塞,会重新将事件放到selectionKeys中,让服务器处理
selector.select();
//被触发的事件集合
//在发生事件后selector会往selectionKeys里面加事key,但是不会删除
selectionKeys = selector.selectedKeys();
for (SelectionKey key :
selectionKeys) {
if(key.isAcceptable()){
//获得事件对应的channel
ServerSocketChannel serverChannel = key.channel();
//服务器端处理事件
SocketChannel clientChannel = serverChannel.accpet();
clientChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
//attachment,附件,将bytebuffer作为附件关联到clientKey上
SelectionKey clientKey = clientChannel.register(selector,0,buffer);
clientChannel.interestOps(SelectionKey.OP_READ);
//key.cancel();把事件取消
}else if(key.isReadable()){
try{
//当前是读事件,说明客户端向服务器发送数据了
//拿到触发事件的channel
SocketChannel clientChannel = (SocketChannel) key.channel();
//获取key上关联的附件
ByteBuffer buffer = (ByteBuffer)key.attachment();
//将客户端通道中的数据写入服务器端的buffer
int read = clientChannel.read(buffer);
//如果是正常断开,read的值为-1
if(read == -1){
key.cancel();
}else{
//正常情况,没有断开,buffer切换为读模式
buffer.flip();
}
}catch(IOException e){
e.printStackTrace();
key.channel();//异常断开。因为客户端断开,因此需要将key取消
}
}
}
//处理完key集合后,需要删除
selectionKeys.clear();
}

事件种类

accpet:会在有连接请求时触发

connect:是客户端,连接建立时触发

read:可读事件

write:可写事件


2.2 阻塞&非阻塞&多路复用

阻塞模式下,一个方法会影响其他方法,阻塞住无法继续运行。

非阻塞模式下,如果没有事件发生,也会继续运行,会一直进行空循环,非常浪费cpu性能

多路复用模式下,只有事件发生了,selector.select()才会继续运行,处理事件,如果没有事件,这里会阻塞住,不会让事件白忙活~

引用:https://www.bilibili.com/video/BV1py4y1E7oA?p=39&spm_id_from=pageDriver

2.3 Stream vs Channel

  1. strean不会自动缓冲数据,channel会利用系统提供的发送缓冲区和接受缓冲区,将通道中的数据暂存到缓冲区中
  2. stream仅支持阻塞模式,channel可以支持阻塞和非阻塞,网络channel可以配合selector实现多路复用
  3. 二者均为全双工,即读写可以同时进行

2.4 IO模型

当用户调用一次channel.read(没有设置为非阻塞的情况)或stream.read时,会切换到操作系统内核来完成数据的真正读取,而读取又分为等待数据和复制数据阶段。

2.4.1 阻塞IO

用户线程阻塞住,阻塞IO在做一件事的时候不能做另一件事

2.4.2 非阻塞IO

在等待数据阶段,用户线程会一直询问是否有数据,如果没有就会返回0,然后继续询问,这之间多次进行用户态和内核态的切换,非常耗费CPU性能,当有数据后,还是会阻塞住,等待内核复制数据。

2.4.3 多路复用


多路复用和阻塞IO的区别


2.4.4 同步和异步

同步:线程自己去获取结果(只有一个线程)

异步:线程自己不去获取结果,而是由其他线程送结果(至少两个线程)

上面三种都是同步的,即由线程自己发起的动作,也是由它主动接收结果。

异步:用户线程自己发起动作,并且由操作系统调用回调方法,由操作系统将最终的结果通过回调方法返回给用户线程。如图:

2.5 零拷贝

零拷贝指的是数据无需拷贝到 JVM 内存(用户缓冲区)中,同时具有以下三个优点

  • 更少的用户态与内核态的切换
  • 不利用 cpu 计算,减少 cpu 缓存伪共享
  • 零拷贝适合小文件传输

传统io

1
2
3
4
5
6
7
8
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
file.read(buf);

Socket socket = ...;
socket.getOutputStream().write(buf);


NIO优化

ByteBuffer.allocateDirect(10)

  • 底层对应DirectByteBuffer,使用的是操作系统内存

Java 可以使用 DirectByteBuffer 将堆外内存映射到 JVM 内存中来直接访问使用

减少了一次数据拷贝,用户态与内核态的切换次数没有减少


以下两种方式都是零拷贝,即无需将数据拷贝到用户缓冲区中(JVM内存中)

底层采用了 linux 2.1 后提供的 sendFile 方法,Java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据

②linux 2.4 对上述方法再次进行了优化


Java网络编程-NIO
https://vickkkyz.fun/2022/04/27/Java/IO/网络编程/
作者
Vickkkyz
发布于
2022年4月27日
许可协议