avatar

java网络编程_NIO

NIO介绍

java NIO全称java non-blocking IO,是指JDK提供的新API。从JDK1.4开始,java提供了一系列改进的输入/输出的新特性,被统称为NIO(即new io),是同步非阻塞(我的理解是对于客户端是同步的,对于服务端线程是非阻塞的)的。

  • NIO有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

  • NIO是面向缓冲区编程的,数据读取到一个缓冲区中,需要时可在缓冲区中前后移动,这就增加了处理的灵活性,使用它可以提供非阻塞式的高伸缩性网络

  • java NIO是非阻塞模式,使一个线程从某个通道大宋请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直到数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求西融入一些数据到某一个通道,但不需要等待它完全写入,这个线程同时可以去做其他事。通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个线程。

    NIO和BIO的比较

  • BIO以流的方式处理数据,而NIO以缓冲区的方式处理数据,缓冲区I/O的效率比流I/O高很多

  • BIO是阻塞的,NIO则是非阻塞的

  • BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个管道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

    NIO三大核心原理示意图

    一张图描述NIO的Selector、Channel和Buffer的关系

  • 每个Channel都对应一个Buffer

  • Selector对应一个线程,一个线程对应多个channel(连接)

  • 每个Channel都注册到Selector选择器上

  • Selector不断轮询查看Channel上的事件事件是通道Channel非常重要的概念

  • Selector会根据不同的事件,完成不同的处理操作

  • Buffer就是一个内存块,底层就是一个数组

  • 数据的读取写入是通过Buffer,这个和BIO不太一样,BIO中要么是输入流,或者是输出流不能双向,但是NIO的Buffer是可以读也可以写,channel是双向的

缓冲区(Buffer)

基本介绍

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个数组,该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer

Buffer常用API介绍

1、Buffer类及其子类

在NIO中,Buffer是一个顶层父类,它是一个抽象类,类的层级关系图,常用的缓冲区分别对应byte,short,int,long,float,double,char这七种

2、缓冲区对象创建

  • 构造函数

    • static ByteBuffer allocate(长度) : 创建byte类型的指定长度的缓冲区
    • static ByteBuffer wrap(byte[] array) : 创建一个有内容的byte类型缓冲区
  • 示例代码

    /**
    * Buffer创建
    */
    public class CreateBufferDemo {
    public static void main(String[] args) {
    // 1、 创建指定长度的缓冲区
    ByteBuffer allocate = ByteBuffer.allocate(5);
    for (int i = 0; i < 5; i++) {
    // 从缓冲区中拿去数据
    System.out.println(allocate.get());
    }
    //System.out.println(allocate.get());// 再去拿去会报错
    // 2、创建一个有内容的缓冲区
    ByteBuffer wrap = ByteBuffer.wrap("zenshin".getBytes());
    for (int i = 0; i < 5; i++) {
    System.out.println(wrap.get());
    }
    }
    }

    3、缓冲区对象添加数据
    | 方法名 | 说明 |
    |int position()/position(int newPositin) | 获取当前要操纵的索引/修改当前要操作的索引位置|
    |int limit()/limit(int newLimit) | 最多能操作到哪个索引/修改最多能操作的索引位置|
    |int capacity() | 返回缓冲区的总长度 |
    |int remaining()/boolean hasRemaining() | 还有多少能操作索引个数/是否还有能操作的字符 |
    |put(byte b)/put(byte[] src) | 添加一个字节/添加字节数组 |
    图解:

代码实现:

/**
* 想缓冲区中添加数据
*/
public class PutBufferDemo {

public static void main(String[] args) {
//1、创建一个缓冲区
ByteBuffer allocate = ByteBuffer.allocate(10);
System.out.println(allocate.position()); // 0 获取当前索引所在位置
System.out.println(allocate.limit());// 10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());// 10 返回缓冲区总长度
System.out.println(allocate.remaining());// 10 还有多少个可以操作的个数
// // 修改当前索引所在位置
// allocate.position(1);
// // 修改最多能操作到哪个索引的位置
// allocate.limit(9);
// System.out.println(allocate.position()); // 1 获取当前索引所在位置
// System.out.println(allocate.limit());// 9 最多能操作到哪个索引位置
// System.out.println(allocate.capacity());// 10 返回缓冲区总长度
// System.out.println(allocate.remaining());// 8 还有多少个可以操作的个数
System.out.println("----------------");
allocate.put((byte) 97);
System.out.println(allocate.position()); // 1 获取当前索引所在位置
System.out.println(allocate.limit());// 10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());// 10 返回缓冲区总长度
System.out.println(allocate.remaining());// 9 还有多少个可以操作的个数


System.out.println("----------------");
allocate.put("abc".getBytes());
System.out.println(allocate.position()); // 4 获取当前索引所在位置
System.out.println(allocate.limit());// 10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());// 10 返回缓冲区总长度
System.out.println(allocate.remaining());// 6 还有多少个可以操作的个数

System.out.println("----------------");
allocate.put("123456".getBytes());
System.out.println(allocate.position()); // 10 获取当前索引所在位置
System.out.println(allocate.limit());// 10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());// 10 返回缓冲区总长度
System.out.println(allocate.remaining());// 0 还有多少个可以操作的个数
System.out.println(allocate.hasRemaining());// false 是否还能继续写入
// 缓冲区满了以后如果想继续写入,会报错
// 如果缓冲区满了,可以调整position位置,就可以重复写,会覆盖之前存入索引位置的值
System.out.println("----------------");
allocate.position(0);
allocate.put("123456".getBytes());
System.out.println(allocate.position()); // 6 获取当前索引所在位置
System.out.println(allocate.limit());// 10 最多能操作到哪个索引位置
System.out.println(allocate.capacity());// 10 返回缓冲区总长度
System.out.println(allocate.remaining());// 4 还有多少个可以操作的个数
System.out.println(allocate.hasRemaining());// true 是否还能继续写入

}
}

4、缓冲区对象读取数据
|方法名|介绍|
|flip()|写切换读模式 limit设置position位置,position设置为0|
|get()|读取一个字节|
|get(byte[] dst)|读取多个字节|
|get(int index)|读取指定索引的字节|
|rewind()|将position设置为0,可以重复读|
|clear()|切换写模式 position设置为0,limit设置为capacity|
|array()|将缓冲区转换为字节数组返回|

图解flip()方法

图解clear()方法

代码演示

/**
* 从缓冲区中读取数据
*/
public class GetBufferDemo {
public static void main(String[] args) {
ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("0123".getBytes());
print(allocate);// 4 10 10 6

// 切换读模式
System.out.println("读取数据-------------------");
allocate.flip();
print(allocate);// 0 4 10 4
for (int i = 0; i < allocate.limit(); i++) {
System.out.println(allocate.get());
// 读取完毕后,再读取会报错。超过limit
}
// 读取指定索引字节
// 读取指定索引的时候,不需要考虑limit是否超过的问题
System.out.println("读取指定索引字节-------------------");
System.out.println(allocate.get(1));

// 读取多个的时候需要注意是否超过了limit的限制
System.out.println("读取多个字节-------------------");
System.out.println("rewind前");
print(allocate);// 4 4 10 0
// 重复读取
allocate.rewind();//将position设置为0,可以重复读
System.out.println("rewind后");
print(allocate);// 0 4 10 4
byte[] bytes = new byte[4];
allocate.get(bytes);
System.out.println(new String(bytes));

// 将缓冲区转化字节数组返回
System.out.println("将缓冲区转化字节数组返回----------------");
byte[] array = allocate.array();
System.out.println(new String(array));

// 切换写模式,覆盖之前索引所在的位置,覆盖不到的位置还显示原来的信息
System.out.println("切换写模式----------------");
allocate.clear();
allocate.put("abc".getBytes());
System.out.println(new String(array)); // abc3
}

private static void print(ByteBuffer byteBuffer){
System.out.println("position:"+byteBuffer.position());
System.out.println("limit:"+byteBuffer.limit());
System.out.println("capacity:"+byteBuffer.capacity());
System.out.println("remaining:"+byteBuffer.remaining());
}
}

通道(Channel)

基本介绍

通常来说NIO中的所有IO都是从Channel(通道)开始的。NIO的通道类似于流,但有些区别:

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写,所有之前我们用流操作IO操作的时候需要分别创建一个输入流和一个输出流)
  • 通道可以异步读写
  • 通道总是基于缓冲区Buffer来读写

Channel常用类介绍

常用的Channel实现类有:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel。

  • FileChannel用于文件的数据读写
  • DatagramChannel用于UDP的数据读写
  • ServerSocketChannel和SocketChannel用于TCP的数据读写

ServerSocketChannel

服务端实现步骤:

  • 打开一个服务端通道
  • 绑定对应的端口号
  • 通道默认是阻塞的,需要设置为非阻塞
  • 检查是否有客户端连接,有客户端连接会返回对应的通道
  • 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
  • 给客户端回写数据
  • 释放资源
    代码实现:
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.StandardCharsets;

    /**
    * 服务端
    */
    public class NIOServer {
    public static void main(String[] args) throws IOException, InterruptedException {
    // 打开服务端通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 绑定对应的端口
    serverSocketChannel.bind(new InetSocketAddress(9999));
    // 通道默认是阻塞的,需要设置为非阻塞
    serverSocketChannel.configureBlocking(false);
    System.out.println("服务端启动成功");
    while (true){
    // 检查是否有客户端连接 有客户端连接会返回对应的通道 如果没有客户端的时候不会阻塞在这里
    SocketChannel socketChannel = serverSocketChannel.accept();
    if(socketChannel==null){
    System.out.println("没有客户端连接...我去干别的事情了");
    Thread.sleep(1000);
    continue;
    }
    // 获取客户端传递过来的数据,并把数据放在buffer中
    ByteBuffer allocate = ByteBuffer.allocate(1024);
    //正数:标识本地读到的有效字节数
    //0 : 本次没有读到数据
    // -1 : 标识读到末尾
    int read = socketChannel.read(allocate);
    System.out.println("客户端消息:"+new String(allocate.array(),0,read, StandardCharsets.UTF_8));
    // 给客户端回写数据
    socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
    // 释放资源
    socketChannel.close();
    }
    }
    }

SocketChannel

实现步骤

  • 打开通道
  • 设置连接IP和端口号
  • 写出数据
  • 读取服务器写回的数据
  • 释放资源

代码实现

public class NIOClient {
public static void main(String[] args) throws IOException {
// 打开通道
SocketChannel socketChannel = SocketChannel.open();
// 设置连接ip和端口号
socketChannel.connect(new InetSocketAddress("127.0.0.1",9999));
// 写数据
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();
socketChannel.write(ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8)));
// 读取服务端返回数据
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = socketChannel.read(allocate);
System.out.println("服务端消息:"+new String(allocate.array(),0,read,StandardCharsets.UTF_8));
// 释放资源
socketChannel.close();
}
}

Selector(选择器)

基本介绍

可以用一个线程,处理多个的客户端连接,就会使用到NIO的Selector,Selector能够检测多个注册的服务端通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

在这种没有选择器的情况下,对应每个连接对应一个处理线程,但是连接并不能马上就发消息,所有还会产生资源浪费

只有在通道真正有读写事件发生时,才会进行读写,就大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间的上下文切换导致的开销

常用API介绍

Selector是一个抽象类
常用方法:

  • Selector.open() : 得到一个选择器对象
  • selector.select() : 阻塞,监控所有注册的通道,当有对应的事件操作时,会将SelectionKey放入集合内部并返回事件数量
  • selector.select(1000): 阻塞一千毫秒,监控所有注册的通道,当有对应的事件操作时,会将SelectionKey放入集合内部并返回
  • selector.selectedKeys() : 返回存有SelectionKey的集合

SelectionKey

  • 常用方法
    • SelectionKey.isAcceptable() : 是否是连接继续事件
    • SelectionKey.isConnectable() : 是否是连接就绪事件
    • SelectionKey.isReadbale() : 是否是读就绪事件
    • SelectionKey.isWritbale() : 是否是写就绪事件
  • SelectionKey中定义的4种事件:
    • SelectionKey.OP_ACCEPT – 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
    • SelectionKey.OP_CONNECT – 接收就绪事件,表示客户与服务器的连接已经建立成功
    • SelectionKey.OP_READ – 读就绪事件,表示管道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
    • SelectiionKey.OP_WRITE – 写就绪事件,表示已经可以向管道中写数据了(通道目前可以用于写操作)

Selector编码

服务端实现步骤:

  • 打开一个服务端通道
  • 绑定对应的端口号
  • 通道默认是阻塞的,需要设置为非阻塞
  • 创建选择器
  • 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
  • 检查选择器是否有事件
  • 获取事件集合
  • 判断事件是否是客户端连接事件SelectionKey.IsAcceotbale()
  • 得到客户单通道,并且将通道注册到选择器上,并指定监听事件为OP_READ
  • 判断是否是客户端读就绪事件SelectionKey.isReadbale()
  • 得到客户端通道,读取数据到缓冲区
  • 给客户端回写数据
  • 从集合中删除对应的事件,因为防止二次处理

代码实现:

/**
* 服务端Selector
*/
public class NIOSelectorServer {
public static void main(String[] args) throws IOException {
// 打开一个服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.bind(new InetSocketAddress(9999));
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 创建一个选择器
Selector selector = Selector.open();
// 将服务端通道注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端启动成功");
while (true){
// 检查选择器上是否有相应的事件
int select = selector.select(2000);
if (select==0){
System.out.println("没有事件发生");
continue;
}
// 获取事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//判断事件是否是客户端连接事件
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()){
// 得到客户端,并且将通道注册到选择器上,并指定监听事件为OP_READ
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("有客户端连接。。。。。");
// 将通道必须设置为非阻塞,因为Selector选择器需要轮训监听所有的通道
socketChannel.configureBlocking(false);
//指定监听事件为OP_READ事件
socketChannel.register(selector,SelectionKey.OP_READ);
}
// 判断是否是客户端 读就绪事件
if(selectionKey.isReadable()){
// 得到客户端通道
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
int read = socketChannel.read(allocate);
if(read>0){
System.out.println("客户端消息:"+new String(allocate.array(),0,read, StandardCharsets.UTF_8));
//客户端回写数据
socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
socketChannel.close();
}
}
// 将这次结束的事件,删除掉,防止二次处理
iterator.remove();
}

}

}
}
文章作者: zenshin
文章链接: https://zlh.giserhub.com/2022/01/23/cl35o0n3x00h4p4tg2kgm82ln/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 zenshin's blog
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论