大数据 NIO

NIO

一、基础回顾

a 、 进程与线程

  1. 进程
    1. 进程: 程序加载到内存中之后被CPU所计算的过程 — 进程是计算机资源分配 、 任务调度的最小单位
    2. 三个维度考虑进程:
      1. 物理内存维度:每一个进程都要分配一块连续的内存空间(首地址 、 尾地址)
      2. 进程执行维度/逻辑维度: 每一个进程都能被CPU计算 , 每一个进程都能挂起然后让另外的进程被CPU计算 — 对于单核CPU而言 , 每一个时刻只能计算一个进程 。 对于windows而言 , (即使是多核CPU)默认只用一个核处理 。 对于Linux而言 , 有几个核就用几个。 — 从微观上 , 计算机是串行处理进程 。 从宏观上而言 , 是多个进程来并行执行 。 — 引入多道编程
      3. 时间维度: 在每一个时间段内 , 进程一定是向前扑进的 。
    3. 为什么要引入进程模型?
      1. 减少程序响应时间 , 提高使用效率 。
      2. 提高CPU利用率 。 IO事件(程序与硬件之间的交互)
    4. 进程的产生事件:
      1. 系统启动时 , 会创建系统进程 。
      2. 用户请求创建进程时 , 创建用户进程 。 (打开应用)
      3. 主进程自动启动子进程 。 (QQ等程序启动之后 , 会自动启动安全守护进程 。 )
    5. 进程的消亡事件
      1. 进程任务执行完毕 , 自动销毁
      2. 进程执行过程中出现错误或异常, 导致进程退出 。 意外身亡
      3. 一个进程被另外的进程强制关闭 他杀
    6. 进程的状态 (除启动 、 消亡)
      1. 就绪 就绪->运行
      2. 运行 运行->阻塞 运行->就绪
      3. 阻塞 阻塞->就绪
  2. 线程
    1. 线程: 是进程中执行某一个具体的任务 。 线程本质上是简化版的进程 。 线程不具有进程资源分配 、 任务调度的资格 。 一个进程中至少有一个线程是在执行的 。 — 线程是任务执行的最小单位 。
  3. 进程的任务调度算法
    1. 时间片轮转算法
    2. 优先级调度算法
    3. 短任务优先算法
    4. FICS 先来先执行

b、 Socket

  1. BIO BlockingIO— 阻塞式IO — 阻塞在一些场景下会相对影响效率 ; 由于流具有方向性, 所以在传输数据时往往要创建多个流对象。 如果一些流长时间不使用 , 却依然保持连接, 会造成资源的大量浪费 。 无法从流中准确的抽取一段数据
  2. 引入 NIO

二、 NIO

  1. NIO — NewIO — NonBlockingIO — 非阻塞式IO — 基于通道和缓冲区。
  2. Buffer类 — 一个基本数据类型的容器 。 缓冲区
  3. Buffer子类 ByteBuffer

    1. 底层是依靠字节数组来存储数据 理解 容量位capacity 、 position 操作位 、 limit限制位
    2. 在读取数据之前往往要做一次filp操作 , 反转缓冲区 , 先将限制位设置为操作位 , 然后将操作位归0
    3. 重绕缓冲区 – 将操作位归0 , 限制位不变
    4. 代码1

      import java.nio.ByteBuffer;
      
      public class BufferDemo {
           public static void main(String[] args) {
      //       //创建缓冲区 , 并指定了大小为1024个字节
               //当创建好缓冲区的时候 , 就有了一下属性
               //1. capacity 容量位 --- 表示缓冲区容量
               //2. position 操作位 --- 表示要操作的位置 ---- 当缓冲区刚刚创建的时候 , 操作位默认为0  , 每添加一个字节的数据 , position就会向后挪一位
               //3. limit 限制位 ---- 表示position 所能达到的最大位置 --- 当缓冲区刚刚创建的时候 , limit就是容量位 。
               //获取数据时 , 默认是从操作位开始获取的
      //       ByteBuffer buffer = ByteBuffer.allocate(1024);//最多能存放1k数据
      //       //向缓冲区添加数据
      //       buffer.put("hello".getBytes());
               //以上方法存在资源浪费
      
              //******************************************************* 
      //       //在已知具体数据的情况下 , 建议使用这种方法创建缓冲区
               //使用wrap方式创建缓冲区 , 参数实际上是一个字节数组  , 底层实际上就是将参数字节数组复制给底层的实际存储数据的数组 , 此时操作位并没有改变还是0 
               //为什么是数组使用复制 , 而不是直接使用赋值?
               //保持数据的不变和唯一
      //       ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());//创建与数据大小相对应的缓冲区
      //       
      //       //获取数据 ,   每一次获取  , 只能获取一个字节
               byte b = buffer.get();
               System.out.println(b);
      //       
      //       //获取缓冲区所有数据
      //       while(buffer.hasRemaining()) {//判断是否还有剩余数据
      //       
      //           byte b = buffer.get();
      //           System.out.println(b);
      //       }
               //*******************************************************
              //但是使用固定缓冲区大小的情况下获取数据会出现获取到0的情况 , 需要将默认的操作位归0 , 并且读取到有效数据结束即可 
               ByteBuffer buffer = ByteBuffer.allocate(10);
               buffer.put("hello".getBytes());
      
               //遍历方法一 : 记录操作位位置后循环遍历
      //       int position = buffer.position();
      //       for(int i = 0 ;i < position ; i++) {
      //           System.out.println(buffer.get(i));
      //       }
               //遍历方法二: 设置限制位为操作位后  , 操作位归0   遍历
      //       buffer.limit(buffer.position());
      //       buffer.position(0);
      //       while(buffer.hasRemaining()) {
      //           System.out.println(buffer.get());
      //       }
               //遍历方法三: 反转缓冲区
               //先将限制位设置为当前的操作位 , 然后把操作位归0
               buffer.flip();
      //       buffer.hasRemaining()该方法  本质上就是判断操作位是否小于限制位
               while(buffer.hasRemaining()) {
                   System.out.println(buffer.get());
               }
      
               //获取缓冲区中的底层数组
               byte[] array = buffer.array();//底层也是使用的数组复制 , 返回的是整个底层数组 , 而不是有效数据
               System.out.println(new String(array , 0 , buffer.position()));
      
               //如果使用过反转
               buffer.flip();
               System.out.println(new String(array , 0 , buffer.limit()));
          }
      }
      
    5. 代码2

      import java.nio.ByteBuffer;
      
      public class BufferDemo2 {
          public static void main(String[] args) {
              ByteBuffer buffer = ByteBuffer.allocate(10);
              buffer.put("hello".getBytes());
      
              System.out.println("操作位:"+ buffer.position());
              System.out.println("限制位:"+ buffer.limit());
      //      buffer.flip();
      //      System.out.println("操作位:"+ buffer.position());
      //      System.out.println("限制位:"+ buffer.limit());
              //重绕缓冲区
              buffer.rewind(); //作用: 将操作位归0  , 限制位不变 。 
              System.out.println("操作位:"+ buffer.position());
              System.out.println("限制位:"+ buffer.limit());
          }
      }
      
  4. Channel 通道

    1. SocketChannel

      1. 客户端步骤
        1. 打开客户端通道 – open
        2. 将客户端通道设置为非阻塞
        3. 发起连接
        4. 人为阻塞 , 防止产生无效连接 finishConnect()
        5. 写出数据
      2. 服务器端步骤
        1. 打开服务器端通道
        2. 绑定侦听的端口号
        3. 设置为非阻塞
        4. 接收连接
        5. 人为阻塞 , 防止没有获取真正的连接
        6. 读取数据
      3. 代码示例:

        客户端
        import java.io.IOException;
        import java.net.InetSocketAddress;
        import java.nio.ByteBuffer;
        import java.nio.channels.SocketChannel;
        
        public class SocketChannelDemo {
            public static void main(String[] args) throws IOException, InterruptedException {
                //打开通道
                //SocketChannel 默认为阻塞连接  此时和Socket基本一样
                SocketChannel s = SocketChannel.open();
                //设置SoceketChannel为非阻塞的
                s.configureBlocking(false);
                //发起连接
                s.connect(new InetSocketAddress("localhost", 8090));
                //由于SoceketChannel为非阻塞的 , 所以不能保证连接的真正建立
                //在实际开发中往往会认为的设置阻塞 , 来保证连接的建立
                //判断连接是否成功 , 如果没有连接成功finishConnect()底层会试图再次建立连接
                //如果多次试图连接没有成功 , 则报错
                while(!s.finishConnect()) ;
        
                //写出数据
                s.write(ByteBuffer.wrap("hello".getBytes()));
        
                //获取服务器端的响应
                Thread.sleep(100);
                ByteBuffer b = ByteBuffer.allocate(100);
                s.read(b);
                b.flip();
                System.out.println(new String(b.array() , 0 , b.limit()));
        //      关闭通道  
                s.close();
        
            }
        
        
        服务端
        import java.io.IOException;
        import java.net.InetSocketAddress;
        import java.nio.ByteBuffer;
        import java.nio.channels.ServerSocketChannel;
        import java.nio.channels.SocketChannel;
        
        public class ServerSocketChannelDemo {
            public static void main(String[] args) throws IOException, InterruptedException {
                //打开服务器通道
                ServerSocketChannel s=  ServerSocketChannel.open();
        
                //绑定侦听的端口
                s.bind(new InetSocketAddress( 8090));
                //设置非阻塞
                s.configureBlocking(false);
        
                //接收连接
                SocketChannel accept = s.accept();
        
                //由于ServerSocketChannel是非阻塞的 , 所以可能出现还没有客户端联入 但是服务器已经结束的现象
                //所以需要人为的设置为阻塞的 。 
                while(accept == null) {
                    accept = s.accept();
                }
                //将socketChannel设置为非阻塞
                accept.configureBlocking(false);
                //读取数据
                ByteBuffer buffer = ByteBuffer.allocate(100);
                accept.read(buffer);
                buffer.flip();
                System.out.println(new String(buffer.array() , 0 , buffer.limit()));
        
                //向客户端做出响应
                accept.write(ByteBuffer.wrap("服务器端接收成功!".getBytes()));
                Thread.sleep(1000);//如果不加延时 , 服务器端写出数据立即结束 , 此时客户端还没有接收完数据会报错
            }
        }
        
    2. 通道特点 :

      1. 能够进行数据的双向传输 , 减少流的数量 , 降低服务器的内存消耗
      2. 由于数据时存储在缓冲区的 , 所以我们可以根据缓冲区的数据做定向操作 **
      3. 能够利用一个或者少量的服务器来完成大量的用户的请求处理(一个服务器能够接受多个客户端的请求) — NIO适用于短任务场景 , BIO适用于长任务场景
  5. Selector 选择器

    1. 每一个客户端 或者服务器端都需要注册到选择器上 , 让这个选择器进行管理 , 选择器管理的时候需要监听事件:
      1. 可连接事件 — 一般是客户端
      2. 可接受事件 — 一般是服务器端
      3. 可读事件
      4. 可写事件
    2. 代码:

      客户端
      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.SocketChannel;
      import java.util.Iterator;
      import java.util.Set;
      
      
      public class ClientDemo {
          public static void main(String[] args) throws IOException {
      
              //打开客户端的通道
              SocketChannel sc = SocketChannel.open();
              //设置为非阻塞
              sc.configureBlocking(false);
              //获取选择器
              Selector selc  = Selector.open();
              //将通道注册到选择器上
              sc.register(selc, SelectionKey.OP_CONNECT);//并给予连接权限
              //发起连接
              sc.connect(new InetSocketAddress("localhost", 8080));
      
              while(true) {
                  //进行选择 , 筛选出有用的连接
                  selc.select();
                  //获取筛选之后有用的事件
                  Set<SelectionKey> keys = selc.selectedKeys();
                  Iterator<SelectionKey> iterator = keys.iterator();
                  while(iterator.hasNext()) {
                      //将遍历到的事件读取出来
                      SelectionKey next = iterator.next();
                      //可能向服务器发起连接
                      //可能向服务器写数据
                      //可能接收服务器的数据
                      if(next.isConnectable()) {//判断是否是一个连接事件
                          //从该事件中获取到对应的通道
                          SocketChannel scx = (SocketChannel) next.channel();
                          //判断之前的连接是否成功
                          while(!scx.finishConnect());
                          //连接成功之后   进行读写操作
                          scx.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                      }
                      if(next.isWritable()) {
                          //从该事件中获取到对应的通道
                          SocketChannel scx = (SocketChannel) next.channel();
      
                          //写数据
                          scx.write(ByteBuffer.wrap("读取数据成功!".getBytes()));
                          //执行完写操作之后 , 需要将这个通道的写权限注销掉  ,防止不停地向服务器写数据
                          scx.register(selc, next.interestOps() ^ SelectionKey.OP_WRITE);//可用^ 或& ~
                      }
                      if(next.isReadable()) {
                          //从该事件中获取到对应的通道
                          SocketChannel scx = (SocketChannel) next.channel();
      
                          //读数据
                          ByteBuffer buffer = ByteBuffer.allocate(100);
                          scx.read(buffer);
                          buffer.flip();
                          System.out.println(new String(buffer.array() , 0 , buffer.limit()));
      
                          //移除可读事件
                          scx.register(selc, next.interestOps() & ~SelectionKey.OP_READ);
                      }
      
                      //为了防止事件移除失败 , 处理完成后将事件移除
                      iterator.remove();
      
                  }
              }
      
      
          }
      }
      
      
      服务器端
      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.util.Iterator;
      import java.util.Set;
      
      import javax.swing.plaf.SliderUI;
      
      public class ServerDemo {
          public static void main(String[] args) throws IOException {
              //打开服务器端通道
              ServerSocketChannel ssc = ServerSocketChannel.open();
              //绑定侦听的端口号
              ssc.bind(new InetSocketAddress(8080));//接收任何IP客户端8080端口传来的数据
              //将通道设置为非阻塞
              ssc.configureBlocking(false);
              //将服务器注册到选择器上
              Selector selc = Selector.open();
              //为服务器注册一个接受请求的权限
              ssc.register(selc, SelectionKey.OP_ACCEPT);
      
              while(true) {
                  //进行选择
                  selc.select();
                  //将选择后的事件获取出来
                  Set<SelectionKey> keys = selc.selectedKeys();
                  Iterator<SelectionKey> it = keys.iterator();
                  while(it.hasNext()) {
                      //获取这个事件
                      SelectionKey key = it.next();
                      //可能是接受连接事件
                      //可能是可读事件
                      //可能是可写事件
                      if(key.isAcceptable()) {
                          //获取事件的通道
                          ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
                          //接受连接
                          SocketChannel sc = sscx.accept();
                          while(sc == null) {
                              sscx.accept();
                          }
                          //设置为非阻塞
                          sc.configureBlocking(false);
                          //注册一个可读事件
                          sc.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
      
                      }
                      if(key.isReadable()) {
                          //获取事件的通道
                          SocketChannel scx = (SocketChannel) key.channel();
                          //读取数据
                          ByteBuffer buffer = ByteBuffer.allocate(100); 
                          scx.read(buffer);
                          buffer.flip();
                          System.out.println(new String (buffer.array() , 0 , buffer.limit()));
                          //消除可读事件
                          scx.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
                      }
                      if(key.isWritable()) {
                          //获取事件的通道
                          SocketChannel scx = (SocketChannel) key.channel();
                          //写出数据
                          scx.write(ByteBuffer.wrap("hello".getBytes()));
                          //消除可以写事件
                          scx.register(selc, key.interestOps() & ~SelectionKey.OP_WRITE);
      
                      }
      
                      it.remove();
                  }
              }
      
          }
      }
      

三 、 考虑 : 数据粘包怎么处理?

  1. 数据定长 — 如果数据长度不够 , 填充无用数据 — 怎样区分无用数据
  2. 约定数据结尾符号 — 结尾符号可能会和实际的数据内容冲突
  3. 约定协议 — 序列化/反序列化 — 底层实际上是约束了起始和结束的协议
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值