网络编程-NIO编程

目录

一、I/O模型说明

 1.1 BIO(同步并阻塞)

 1.2 NIO(同步非阻塞)

 1.3 AIO(异步非阻塞)

 1.4 BIO、NIO、AIO 适用场景分析

 二、NIO详细介绍

三、NIO 三大核心原理示意图

四、缓冲区(Buffer)

 4.1 基本介绍

 4.2 Buffer常用API介绍 

  4.2.1 缓冲区对象创建API

  4.2.2 缓冲区对象添加数据

  4.2.3 缓冲区对象读取数据

五、通道(Channel)

  5.1 基本介绍

 5.2 Channel常用类以及API介绍

  5.2.1 Channel接口

  5.2.2 ServerSocketChannel实现服务端

  5.2.3 SocketChannel

六、Selector (选择器/多路复用器)

 6.1 基本介绍

 6.2 常用API介绍

  6.2.1 Selector和SelectionKey 类

  6.2.3  Selector 服务端实现


一、I/O模型说明

    I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。 Java 共支持 3 种网络编程模型/IO 模式:BIO(同步并阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)。其中阻塞与非阻塞主要指的是访问IO资源的线程是否会阻塞(或处于等待),非阻塞不会等待资源是否准备就绪,阻塞会一直阻塞等待;同步和异步主要是指的数据的请求方式,同步和异步是指访问数据的一种机制。

 1.1 BIO(同步并阻塞)

    Java BIO就是传统的 socket编程,就是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。

 BIO问题分析:

  1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
  2. 并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
  3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。

 1.2 NIO(同步非阻塞)

    NIO(同步非阻塞),就是服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。和BIO不一样是,不用每个链接都分配一个线程,连接注册selector(多路复用器)后,只有读写的需求时才会进行处理。

 1.3 AIO(异步非阻塞)

    AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。Proactor 模式是一个消息异步通知的设计模式,Proactor 通知的不是就绪事件,而是操作完成事件,这也就是操作系统异步 IO 的主要模型。

 1.4 BIO、NIO、AIO 适用场景分析

  1.  BIO(同步并阻塞) 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
  2. NIO(同步非阻塞) 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
  3. AIO(异步非阻塞) 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作, 编程比较复杂,JDK7 开始支持。

 二、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 三大核心原理示意图

 上面这一张图描述了 NIO 的 Selector 、 Channel 和 Buffer 的关系:

  • 每个 channel 都会对应一个 Buffer
  • Selector 对应一个线程, 一个线程对应多个 channel(连接)
  • 每个 channel 都注册到 Selector选择器上
  • Selector不断轮询查看Channel上的事件, 事件是通道Channel非常重要的概念
  • Selector 会根据不同的事件,完成不同的处理操作
  • Buffer 就是一个内存块 , 底层是有一个数组
  • 数据的读取写入是通过 Buffer, 这个和 BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO 的 Buffer 是可以读也可以写 , channel 是双向的。

四、缓冲区(Buffer)

 4.1 基本介绍

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

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

 4.2 Buffer常用API介绍 

  4.2.1 缓冲区对象创建API

方法名 说明
static ByteBuffer allocate(长度) 创建byte类型的指定长度的缓冲区
static ByteBuffer wrap(byte[] array) 创建一个有内容的byte类型缓冲区

    示例代码:

import java.nio.ByteBuffer;
/**
* 创建缓冲区
*/
public class CreateBufferDemo {
  public static void main(String[] args) {
    //1.创建一个指定长度的缓冲区, 以ByteBuffer为例
    ByteBuffer byteBuffer = ByteBuffer.allocate(5);
    for (int i = 0; i < 5; i++) {
      System.out.println(byteBuffer.get());
    }
    //由于上面代码已经取了5个数据,下标会移动到5,再在此调用会报错
    //System.out.println(byteBuffer.get());
    //2.创建一个有内容的缓冲区
    ByteBuffer wrap = ByteBuffer.wrap("ddd".getBytes());
    for (int i = 0; i < 5; i++) {
      System.out.println(wrap.get());
   }
 }
}

  4.2.2 缓冲区对象添加数据

 图解:

 示例代码:

import java.nio.ByteBuffer;
/**
* 添加缓冲区
*/
public class PutBufferDemo {
  public static void main(String[] args) {
    //1.创建一个指定长度的缓冲区, 以ByteBuffer为例
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    System.out.println(byteBuffer.position());//0 获取当前索引所在位置
    System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
    System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
    System.out.println(byteBuffer.remaining());//10 还有多少个能操作
    //修改当前索引位置
    //byteBuffer.position(1);
    //修改最多能操作到哪个索引位置
    //byteBuffer.limit(9);
    //System.out.println(byteBuffer.position());//1 获取当前索引所在位置
    //System.out.println(byteBuffer.limit());//9 最多能操作到哪个索引
    //System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
    //System.out.println(byteBuffer.remaining());//8 还有多少个能操作
    //添加一个字节
    byteBuffer.put((byte) 97);
    System.out.println(byteBuffer.position());//1 获取当前索引所在位置
    System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
    System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
    System.out.println(byteBuffer.remaining());//9 还有多少个能操作
    //添加一个字节数组
    byteBuffer.put("abc".getBytes());
    System.out.println(byteBuffer.position());//4 获取当前索引所在位置
    System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
    System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
    System.out.println(byteBuffer.remaining());//6 还有多少个能操作
    //当添加超过缓冲区的长度时会报错
    byteBuffer.put("012345".getBytes());
    System.out.println(byteBuffer.position());//10 获取当前索引所在位置
    System.out.println(byteBuffer.limit());//10 最多能操作到哪个索引
    System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度
    System.out.println(byteBuffer.remaining());//0 还有多少个能操作
    System.out.println(byteBuffer.hasRemaining());// false 是否还能有操作的
数组
    // 如果缓存区存满后, 可以调整position位置可以重复写,这样会覆盖之前存入索引的对
应的值
    byteBuffer.position(0);
    byteBuffer.put("012345".getBytes());
 }
}

  4.2.3 缓冲区对象读取数据

 图解:flip()方法

图解:clear()方法:

import java.nio.ByteBuffer;
/**
* 从缓冲区中读取数据
*/
public class GetBufferDemo {
  public static void main(String[] args) {
    //1.创建一个指定长度的缓冲区
    ByteBuffer allocate = ByteBuffer.allocate(10);
    allocate.put("0123".getBytes());
    System.out.println("position:" + allocate.position());//4
    System.out.println("limit:" + allocate.limit());//10
    System.out.println("capacity:" + allocate.capacity());//10
    System.out.println("remaining:" + allocate.remaining());//6
    //切换读模式
    System.out.println("读取数据--------------");
    allocate.flip();
    System.out.println("position:" + allocate.position());//4
    System.out.println("limit:" + allocate.limit());//10
      System.out.println("capacity:" + allocate.capacity());//10
    System.out.println("remaining:" + allocate.remaining());//6
    for (int i = 0; i < allocate.limit(); i++) {
      System.out.println(allocate.get());
   }
    //读取完毕后.继续读取会报错,超过limit值
    //System.out.println(allocate.get());
    //读取指定索引字节
    System.out.println("读取指定索引字节--------------");
    System.out.println(allocate.get(1));
    System.out.println("读取多个字节--------------");
    // 重复读取
  allocate.rewind();
    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(allocate.array()));
 }
}

 注意事项:

  • capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写哪个索引)
  • 获取缓冲区里面数据之前,需要调用flip方法
  • 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了才会消失

五、通道(Channel)

  5.1 基本介绍

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

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

 5.2 Channel常用类以及API介绍

  5.2.1 Channel接口

    常 用 的Channel实现类类 有 :FileChannel , DatagramChannel ,ServerSocketChannel和
SocketChannel 。FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读
写, ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写。SocketChannel 与ServerSocketChannel类似 Socke和ServerSocket,可以完成客户端与服务端数据的通信工作。

  5.2.2 ServerSocketChannel实现服务端

 服务端实现步骤:

  1. 打开一个服务端通道
  2. 绑定对应的端口号
  3. 通道默认是阻塞的,需要设置为非阻塞
  4. 检查是否有客户端连接 有客户端连接会返回对应的通道
  5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
  6. 给客户端回写数据
  7. 释放资源

示例代码:

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 {
    //1. 打开一个服务端通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    //2. 绑定对应的端口号
    serverSocketChannel.bind(new InetSocketAddress(9999));
    //3. 通道默认是阻塞的,需要设置为非阻塞
    // true 为通道阻塞 false 为非阻塞
    serverSocketChannel.configureBlocking(false);
    System.out.println("服务端启动成功..........");
    while (true) {
      //4. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回null
      SocketChannel socketChannel = serverSocketChannel.accept();
      if (socketChannel == null) {
        System.out.println("没有客户端连接...我去做别的事情");
        Thread.sleep(2000);
        continue;
     }
      //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
      ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      //返回值:
      //正数: 表示本次读到的有效字节个数.
      //0  : 表示本次没有读到有效字节.
      //-1 : 表示读到了末尾
      int read = socketChannel.read(byteBuffer);
      System.out.println("客户端消息:" +
          new String(byteBuffer.array(), 0, read,
StandardCharsets.UTF_8));
      //6. 给客户端回写数据
      socketChannel.write(ByteBuffer.wrap("没
钱".getBytes(StandardCharsets.UTF_8)));
      //7. 释放资源
      socketChannel.close();
   }
 }
}

  5.2.3 SocketChannel

  客户端实现步骤:

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

示例代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
/**
* 客户端
*/
public class NIOClient {
  public static void main(String[] args) throws IOException {
    //1.打开通道
    SocketChannel socketChannel = SocketChannel.open();
    //2.设置连接IP和端口号
    socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
    //3.写出数据
    socketChannel.write(ByteBuffer.wrap("老板, 该还钱
拉!".getBytes(StandardCharsets.UTF_8)));
    //4.读取服务器写回的数据
    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    int read=socketChannel.read(readBuffer);
    System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read,
StandardCharsets.UTF_8));
    //5.释放资源
    socketChannel.close();
 }
}

六、Selector (选择器/多路复用器)

 6.1 基本介绍

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

 6.2 常用API介绍

  6.2.1 Selector和SelectionKey 类

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

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

  Selector是一个选择器,进行监控,而API中经常显示的SelectionKey 代表了监控到的具体事件,其核心API包括:

  • SelectionKey.isAcceptable(): 是否是连接继续事件
  • SelectionKey.isConnectable(): 是否是连接就绪事件
  • SelectionKey.isReadable(): 是否是读就绪事件
  • SelectionKey.isWritable(): 是否是写就绪事件

与之对应SelectionKey中定义的4种事件:

  • SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
  • SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户端与服务器的连接已经建立成功
  • SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
  • SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作) 

  6.2.3  Selector 服务端实现

  加入Selector 服务端实现步骤(客户端不变): 

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

示例代码 :

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* 服务端-选择器
*/
public class NIOSelectorServer {
  public static void main(String[] args) throws IOException,
InterruptedException {
    //1. 打开一个服务端通道
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    //2. 绑定对应的端口号
    serverSocketChannel.bind(new InetSocketAddress(9999));
    //3. 通道默认是阻塞的,需要设置为非阻塞
    serverSocketChannel.configureBlocking(false);
    //4. 创建选择器
    Selector selector = Selector.open();
    //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    System.out.println("服务端启动成功...");
    while (true) {
      //6. 检查选择器是否有事件
      int select = selector.select(2000);
      if (select == 0) {
        continue;
     }
      //7. 获取事件集合
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = selectionKeys.iterator();
      while (iterator.hasNext()) {
        //8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
        SelectionKey key = iterator.next();
        //9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
        if (key.isAcceptable()) {
          SocketChannel socketChannel = serverSocketChannel.accept();
          System.out.println("客户端已连接......" + socketChannel);
          //必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
          socketChannel.configureBlocking(false);
          //并指定监听事件为OP_READ
          socketChannel.register(selector, SelectionKey.OP_READ);
       }
        //10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
        if (key.isReadable()) {
          //11.得到客户端通道,读取数据到缓冲区
          SocketChannel socketChannel = (SocketChannel) key.channel();
          ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
          int read = socketChannel.read(byteBuffer);
          if (read > 0) {System.out.println("客户端消息:" +
                new String(byteBuffer.array(), 0, read,
StandardCharsets.UTF_8));
            //12.给客户端回写数据
            socketChannel.write(ByteBuffer.wrap("没
钱".getBytes(StandardCharsets.UTF_8)));
            socketChannel.close();
         }
       }
        //13.从集合中删除对应的事件, 因为防止二次处理.
        iterator.remove();
     }
   }
 }
}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值