• Java IO模型:BIO、NIO、AIO


    Java IO模型:BIO、NIO、AIO

    本来是打算直接学习网络框架Netty的,但是先补充了一下自己对Java 几种IO模型的学习和理解。分别是 BIO、NIO、AIO三种IO模型。

    IO模型的基本说明

    image-20200324063448217

    BIO模型图

    image-20200324064030007

    缺点:

    1. 如果有很多个Client,则会产生很多个线程。压力主要是在服务器端。客户端的压力并不大。

    2. 另外建立连接之后,并不是在时时刻刻的使用。会有空间时间。

    3. 会阻塞。

    NIO模型图

    image-20200324064940368

    特点:

    1. 事件驱动
    2. 多路复用
    3. Netty底层使用的NIO模型

    AIO模型

    目前还未得到广泛运用。异步非阻塞。先了解就可以。

    BIO、NIO、AIO使用场景分析

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

    JAVA BIO编程

    JAVA BIO 基本介绍

    image-20200324070601084

    JAVA BIO 工作机制

    image-20200324070450273

    JAVA BIO 应用实例

    image-20200324070636449

    package com.dawa.netty.bio;
    
    import com.sun.org.apache.xpath.internal.operations.String;
    
    import java.io.InputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Arrays;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 功能需求
     * 1. 使用BIO模型编写一个服务器,监听6666端口,当有客户连接的时候,就启动一个客户端线程与之连接
     * 2. 要求使用县城连接机制,可以连接过个客户端
     * 3. 服务器端可以接受客户端发送的数据(TeInet方法即可)
     */
    public class TestBIO {
        public static void main(String[] args) throws Exception {
            //1. 创建一个线程池. 这里 借助 Executors 这个工具类
            ExecutorService pool = Executors.newCachedThreadPool();
            //2. 建立一个监听服务,用来监听客户端连接
            ServerSocket serverSocket = new ServerSocket(6666);
    
            while (true) {
                final Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接");
                //就创建一个线程与之通信
                pool.execute(new Runnable() {
                    public void run() {
                        //编写一个处理方法.
                        handler(socket);
                    }
                });
            }
        }
    
        public static void handler(Socket socket) {
            byte[] bytes = new byte[1024];
    
            try (InputStream inputStream = socket.getInputStream()) {
                while (true) {
                    int read = inputStream.read(bytes);
                    if (read != -1) {
                        //注意这里,不能用String转换了.因为String已经不支持有参数的构造方法.
                        System.out.println(Arrays.toString(bytes));
                    } else {
                        break;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                System.out.println("关闭连接");
            }
    
        }
    }
    
    

    JAVA BIO 问题分析

    image-20200324073218636

    JAVA NIO编程

    NIO 基本介绍

    image-20200324193714767

    image-20200324195127910

    image-20200324194859453

    1. NIO的三个核心,channel相当于IO的Socket
    2. Buffer,Channel,Selector(选择器)三大核心组件。
    3. 通过Buffer实现非阻塞,
    4. 面向缓冲区,或者面向块编程(Buffer就是这样的)。
    5. NIO是事件驱动的

    NIO Buffer 基本使用

    image-20200324200742367

    这没有BooleanBuffer,另外StringBuffer继承自StringBuilder.

    一个简单的Buffer子类的使用案例如下

    package com.dawa.netty.bio;
    
    import java.nio.IntBuffer;
    
    public class TestNIO {
        public static void main(String[] args) {
            IntBuffer intBuffer = IntBuffer.allocate(5);
    
            for (int i = 0; i < intBuffer.capacity(); i++) {
                System.out.println(intBuffer.put(i*5));
            }
    
            intBuffer.flip();
    
            while (intBuffer.hasRemaining()) {
                System.out.println(intBuffer.get());
            }
        }
    }
    

    NIO 和 BIO 的比较

    image-20200324201708534

    NIO 三大核心原理示意图

    image-20200324201933663

    Selector、Channel和Buffer的关系图

    1. 每个Channel都会对应一个Buffer。
    2. Selector对应一个线程,一个线程对应多个Channel(连接)。
    3. 该图反应了有三个Channel注册到该Selector
    4. 程序切换到哪个Channel是由事件决定的。Event是一个重要概念。
    5. Select会根据不同的事件,在各个通道上切换。
    6. Buffer就是一个内存块、底层是由一个数组
    7. 数据的读取写入是通过Buffer,这个和BIO、BIO中要么是输入流、或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip()方法来进行切换。
    8. Channel也是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。

    三大核心——Buffer缓冲区详解

    Buffer的Doc源码(java11)

    image-20200324204447270

    基本介绍

    image-20200324204604333

    Buffer的子类

    image-20200324200742367

    容器对象(函数组),如何理解?从源码中可以看到。Int,Float等,每一个子类Buffer对象,都是[]数组。

    image-20200324204830461

    具有的四个子类

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    
    1. 缓冲区的容量是它所包含的元素数量。 缓冲区的容量从不为负,从来没有改变。

    2. 缓冲区的限制是不应读取或写入的第一个元素的索引。 缓冲区的限制是从不为负,并且永远不会比它更大的容量。

    3. 缓冲区的位置要被读出或写入的下一个元素的索引。 缓冲区的位置永远不会为负,并且永远不会比它的极限。

    4. // Invariants: mark <= position <= limit <= capacity
      

    代码跟踪-详解

    image-20200324205035963

    image-20200324205428253

    Buffer类及其子类中的重要的方法

    image-20200324210320538

    如,通过设置position的值,来读取指定位置的值。也可以修改limit的值等。

    ByteBuffer

    ByteBuffer,是最常用的。二进制数据。

    image-20200324210425809

    三大核心—— Channel 通道详解

    Channel接口的Doc源码(java11)

    image-20200324211700736

    基本介绍

    image-20200324212539027

    image-20200324212207511

    Channel的子类

    image-20200324211833203

    FileChannel类

    image-20200324212248256

    Channel应用实例

    Channel应用实例1——本地文件写数据

    image-20200325052304319

    实例代码如下:

    package com.dawa.netty.nio;
    
    import java.io.FileOutputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    public class TestNIOFileChannel01 {
        public static void main(String[] args) throws Exception{
    
            //准备字符串
            String string = "dawa,大娃,Bigbaby";
    
            //准备输出流.指定输出的文件地址
            FileOutputStream fileOutputStream = new FileOutputStream("dawa.txt");
    
            //准备Channel管道. 对输出流进行封装,封装为一个channel管道.
            FileChannel fileChannel = fileOutputStream.getChannel();
    
            //准备一个byte数组, 也就是一个 Buffer数组,来缓存数据
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            
            //读取数据
            byteBuffer.put(string.getBytes());
    
          	//这里第一次没有反转,文件里面乱码
            byteBuffer.flip();
          
            //完成写的操作
            fileChannel.write(byteBuffer);
    
            //关闭流
            fileOutputStream.close();
        }
    }
    

    注意:

    1. 是FileOutPutStream 包含 NIO FileChannel
    2. FileChannel的具体的实现类是:FileChannelImpl

    image-20200325052449120

    Channel应用实例2——本地文件读数据

    image-20200325052837040

    代码案例如下

    package com.dawa.netty.nio;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    //本地 读文件
    public class TestNIOFileChannel02 {
        public static void main(String[] args) throws Exception {
    
            //读到文件
            File file = new File("dawa.txt");
            FileInputStream fileInputStream = new FileInputStream(file);
    
            //fileInputStream 包装为 Channel
            FileChannel fileChannel = fileInputStream.getChannel();
    
            //借助Buffer byte[]缓冲数
            ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
    
            //将Channel的数据读入到byteBuffer
            fileChannel.read(byteBuffer);
    
            System.out.println(new String(byteBuffer.array()));
    
            fileInputStream.close();
    
        }
    }
    

    Channel应用案例3——使用Buffer完成文件的读写

    类似于拷贝的操作,使用文件Channel+Buffer完成

    image-20200325054011127

    代码案例如下

    package com.dawa.netty.nio;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    //使用一个Channel完成文件的读写
    public class TestNIOFileChannel03 {
        public static void main(String[] args)  throws  Exception{
    
            FileInputStream fileInputStream = new FileInputStream("dawa.txt");
            FileChannel channel01 = fileInputStream.getChannel();
    
            FileOutputStream fileOutputStream = new FileOutputStream("erwa.txt");
            FileChannel channel02 = fileOutputStream.getChannel();
    
            //Buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    
            while (true) {
                //这里注意使用Clear操作,不然会进入死循环
                /**
                 * public Buffer clear() {
                 *         position = 0;
                 *         limit = capacity;
                 *         mark = -1;
                 *         return this;
                 *     }
                 */
                byteBuffer.clear();
    
                int read = channel01.read(byteBuffer);
                if (read == -1) {
                    break;
                }
                //反转,切换流
                byteBuffer.flip();
                channel02.write(byteBuffer);
            }
    						fileInputStream.close();
                fileOutputStream.close();
        }
    }
    

    这里需要注意的是使用clear操作,重置缓冲区基本参数

    public Buffer clear() {
            position = 0;
            limit = capacity;
            mark = -1;
            return this;
        }
    

    Channel应用案例4——Transferform拷贝

    transferFrom方法

        public long transferFrom(ReadableByteChannel src,
                                 long position, long count)
            throws IOException
        {
            ensureOpen();
            if (!src.isOpen())
                throw new ClosedChannelException();
            if (!writable)
                throw new NonWritableChannelException();
            if ((position < 0) || (count < 0))
                throw new IllegalArgumentException();
            if (position > size())
                return 0;
            if (src instanceof FileChannelImpl)
               return transferFromFileChannel((FileChannelImpl)src,
                                              position, count);
    
            return transferFromArbitraryChannel(src, position, count);
        }
    
    

    案例如下

    package com.dawa.netty.nio;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    //使用一个Channel完成文件的读写
    public class TestNIOFileChannel03 {
        public static void main(String[] args)  throws  Exception{
    
            FileInputStream fileInputStream = new FileInputStream("dawa.txt");
            FileOutputStream fileOutputStream = new FileOutputStream("erwa.txt");
    
            FileChannel sourceCH = fileInputStream.getChannel();
            FileChannel destCH = fileOutputStream.getChannel();
    
            //直接通过通道,完成拷贝
            destCH.transferFrom(sourceCH, 0, sourceCH.size());
    
            fileInputStream.close();
            fileOutputStream.close();
    
        }
    }
    

    关于Buffer和Channel的注意事项和注意细节

    image-20200325060447130

    1. 存取类型需要保持一致(存取顺序一致)

      image-20200325061118654

    2. Buffer可以转为只读Buffer

      byteBuffer.asReadOnlyBuffer();
      
       ![image-20200325061059067](https://tva1.sinaimg.cn/large/00831rSTly1gd5rck2zicj31ja0tyh7e.jpg)
    
    3. **MappedBuffer 可以让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次**
    
       ![image-20200325061421909](https://tva1.sinaimg.cn/large/00831rSTly1gd5rge5zuaj30oi0b4tad.jpg)
    
       MappedByteBuffer是抽象类,实际能够操作的类型是 DirectByteBuffer
    
       > 代码案例如下:
    
       ```java
       package com.dawa.netty.nio;
       
       import java.io.RandomAccessFile;
       import java.nio.MappedByteBuffer;
       import java.nio.channels.FileChannel;
       
       //使用 MappedBuffer 直接完成文件在内存中的数据修改
       public class MappedBuffer01 {
           public static void main(String[] args) throws Exception {
       
               //获取一个读取文件流
               RandomAccessFile randomAccessFile = new RandomAccessFile("dawa.txt","rw");
       
               //获取指定的Channel
               FileChannel channel = randomAccessFile.getChannel();
       
               //读取模式. 0 代表从0开始, 5代表读取5个字节,也同时意味着只能在内存中操作这5个字节
               MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
       
               //对指定位置进行操作
               mappedByteBuffer.put(0, (byte) 'A');
               mappedByteBuffer.put(2, (byte) 9);
       
               randomAccessFile.close();
               channel.close();
           }
       }
    
    1. Scattering&Gathering的使用

      Scattering:将数据写入到Buffer时,可以采用Buffer数组,依次写入【分散】

      Gathering:从Buffer读取数据时,可以采用Buffer数组,依次读【聚合】

      解决的问题:当一个数组不够用的时候,可以用数组组,来完成类似的操作

      代码案例如下:使用 数组,来完成客户端 - 服务器端 读取操作

      package com.dawa.netty.nio;
      
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.channels.ServerSocketChannel;
      import java.nio.channels.SocketChannel;
      import java.util.Arrays;
      
      public class ScatteringGatheringGetPut {
          public static void main(String[] args) throws Exception {
      
              //创建服务器端的
              ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      
              //监听端口号
              InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 7000);
      
              //绑定端口号到服务器端的Channel
              serverSocketChannel.socket().bind(inetSocketAddress);
      
              //创建Buffer数组
              ByteBuffer[] byteBuffers = new ByteBuffer[2];
              byteBuffers[0] = ByteBuffer.allocate(5);
              byteBuffers[1] = ByteBuffer.allocate(3);
      
      
              // 等待连接,获取连接, 并生成客户端的 Channel
              SocketChannel socketChannel = serverSocketChannel.accept();
      
              //假设从 客户端读取 8个字节
              int messageLength = 8;
      
              while (true) {
                  //1. 将客户端的数据, 读取
                  int byteRead = 0;
                  while (byteRead < messageLength) {
                      long l = socketChannel.read(byteBuffers);
                      System.out.println("byteRead = " + byteRead);
                      byteRead += 1;//累积读取的字节数
      
                      //使用流打印,看看当前Buffer里面的position和limit
                      Arrays.asList(byteBuffers).stream()
                              .map(byteBuffer -> "position=" + byteBuffer.position() + ", limit=" + byteBuffer.limit()).forEach(System.out::println);
                  }
      
                  //将所有的 Buffer反转
                  Arrays.asList(byteBuffers).forEach(ByteBuffer::flip);
      
                  //2. 将读取到的数据,写回客户端
                  int byteWrite = 0;
                  while (byteWrite < messageLength) {
                      socketChannel.write(byteBuffers);
                      byteWrite += 1;
                  }
      
                  //将所有的Buffer进行Clear操作
                  Arrays.asList(byteBuffers).forEach(ByteBuffer::clear);
      
                  //读完之后,打印出来看看读写文件的长度
                  System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWrite + ", messageLength" + messageLength);
      
              }
      
          }
      }
      

    三大核心—— Selector 选择器详解

    Selector的Doc源码(java11)

    image-20200325074926300

    image-20200325075002638

    基本介绍

    image-20200325075828311

    1. 示意图:

      image-20200325075740264

    Select示意图和特点说明

    image-20200325075854846

    Selector的子类

    image-20200325080430928

    Selector类的相关方法

    image-20200325080845317

    PS:一个线程,对应一个Selector,每个Selector通过调用select()方法,获取不同的能够代表Channel的SelectionKey,得到一个能够被选择的Channel集合。

    注意事项

    image-20200327042610721

    NIO非阻塞网络编程原理分析图

    NIO非阻塞网络相关的(Selector、SelectionKey、ServerSocketChannel和SocketChannel)关系图梳理。

    image-20200327042415803

    对上图的说明

    1. 当客户端生成时,会通过ServerSocketChannel得到SocketChannel。

    2. Selector开始监听...Selector进行监听select方法,返回有事件发生的通道的个数。

    3. 将SocketChannel注册到Selector上.register(Selector sel,int ops).一个Selector上可以注册多个SocketChannel。

      SocketChannel的父类里面有注册方法

      SelectableChannel里面还有一个注册方法,这个用的比较多

    4. 注册后,返回一个SelectionKey,会和该Selector关联(集合)

    5. 进一步得到各个SelectionKey(有事件发生)

    6. 再通过SelectionKey 反向获取SocketChannel。

      SelectionKey类中的channel()方法

    7. 通过得到的Channel,完成业务处理

    NIO非阻塞网络编程快速入门

    创建服务器端。
      1. 创建ServerSocketChannel ,并设置非阻塞
      2. 得到一个Selector对象
      3. 绑定一个端口6666.在服务器端监听
      4. 把servrSocketChannel 注册到 selector  关心事件为 SelectionKey.OP_ACCEPT
      5. 循环等待客户端连接
      	//这里我们等待一秒,如果没有事件发生,返回
      	1. if(selector.selecct(1000)==0){//没有事件发生
          sout("服务器等待了一秒");
          continue;
        }
    
    		//如果返回的值>0,就获取到相关的selectionKey集合
    		// 1. 表示已经获取到关注的事件。
    		// 2. 通过selectionKeys()返回关注的集合。
    		// 3. 通过selectionKeys
    		seletor.selectedKeys().var;
    
    		//遍历得到的selectionKeys.
    			//1. 获取SelectionKey
    			//2. 根据key 对应的通道发生的事件做处理
    			//3. 如果是 OP_ACCEPT,有新的客户端连接
    					//1. 给该客户端生成一个SocketChannel
    					//2. 将SocketChannel 注册到select,关注事件为OP_READ,并关联一个Buffer
    			//4. 如果是 OP_READ,读取数据
    					//1. 通过key,反向获取对应的channel	
    					//2. 获取到该channel关联的buffer 
    			//5. 手动从集合中移动单签的SelectionKey,防止重复操作。
    
    创建客户端。
      1. 得到一个网络通道SocketChannel.并设置非阻塞
      2. 提供服务区的IP和端口,连接服务器
    

    服务器端代码

    package com.dawa.netty.nio;
    
    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;
    
    // NIO 服务端
    public class TestNIOServer {
        public static void main(String[] args) throws Exception {
    
            //  1. 创建ServerSocketChannel ,并设置非阻塞
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            //  2. 得到一个Selector对象
            Selector selector = Selector.open();
            //  3. 绑定一个端口6666.在服务器端监听
            serverSocketChannel.socket().bind(new InetSocketAddress(6666));
            //  4. 把serverSocketChannel 注册到 selector  关心事件为 SelectionKey.OP_ACCEPT
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            // 循环,等待客户端连接
            while (true) {
                if (selector.select(1000) == 0) {// 没有事件发生
                    System.out.println("服务器端等待1秒,没有客户端连接");
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();//得到所有被选中的Key
                //循环遍历每一个key,每一个key代表一个事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //根据key对应的事件,做响应的处理
                    if (selectionKey.isAcceptable()) {//如果是 Accept事件, 连接事件,则生成对应的客户端Channel
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        //将SocketChannel 注册到select,关注事件为OP_READ,并关联一个Buffer
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    }
                    if (selectionKey.isReadable()) {//如果是读事件
                        //1. 通过key,反向生成Channel
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        //设置非同步NIO
                        socketChannel.configureBlocking(false);
                        //2. 获取到该channel关联的buffer
                        ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                        socketChannel.read(buffer);
                        //打印出获取到的Buffer
                        System.out.println("from 客户端:" + new String(buffer.array()));
                    }
                    //这里一定要记得把处理过的key给移除掉,自己遇到了死循环.
                    iterator.remove();
                }
            }
        }
    }
    
    

    客户端代码

    package com.dawa.netty.nio;
    
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    
    //NIO 客户端
    public class TestNIOClient {
        public static void main(String[] args) throws Exception {
    
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            //服务器端的IP和端口
            InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);
    
            if (!socketChannel.connect(socketAddress)) {
                System.out.println("连接失败,但是可以干其他事情,非阻塞");
                while (!socketChannel.finishConnect()) {
                    System.out.println("在连接完成之前,我一直干其他的事情");
                }
            }
    
            String string = "hello,dawa";
            socketChannel.write(ByteBuffer.wrap(string.getBytes()));
            System.in.read();
        }
    }
    

    SelectionKey

    Selector.keys() 是 列出所有的key。

    Selector.selectedKeys()是列出所有被选中的key。

    这两个是不一样的。

    image-20200327063028759

    image-20200327064315095

    ServerSocketChannel

    image-20200327064402970

    SocketChannel

    image-20200327064850861

    NIO网络编程应用实例——群聊系统

    功能示意图

    image-20200327065308315

    1. 先写服务器端
      1. 服务器端启动并监听6667
      2. 服务器端接收客户端消息,并实现转发[处理上线和离线]
    2. 编写客户端
      1. 连接服务器
      2. 发送消息
      3. 接受服务器消息
    
    

    服务器端代码

    1. 构造器初始化

    image-20200327065929587

    1. 监听方法
      Listen()
      image-20200327070215028
      里面循环的写法:
      image-20200327070542182

    里面读数据的方法:

    image-20200327071122366

    image-20200327071218197

    try catch完成离线处理
    image-20200327071738717

    里面转发给其他客户端的方法

    image-20200327071538520

    客户端代码

    1. 构造器初始化

      image-20200327072218159

    2. 向服务器发消息

      image-20200327072357745

    3. 读取从服务器端回复的消息

      image-20200327072652113

      image-20200327072740228

    启动客户端和服务器端

    1. 启动客户端的方法

      image-20200327073043538

      image-20200327073116852

    2. 启动服务器端的方法

      image-20200327073200172

    自己编码:实现群发

    客户端代码

    package com.dawa.netty.nio.group;
    
    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.Scanner;
    
    //客户端
    public class GroupCharClient {
    
        private SocketChannel socketChannel;
        private static final int PORT = 6667;
        private static final String HOST = "127.0.0.1";
        private Selector selector;
        private String userName;
    
        public GroupCharClient() throws IOException {
            selector = Selector.open();
            //连接服务器
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            userName = socketChannel.getRemoteAddress().toString().substring(1);
        }
    
        //发送消息
        public void sendMessage(String message){
            message = userName + "说" + message;
            ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
            try {
                socketChannel.write(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //读取服务器端发来的消息
        public void readMessage() {
            try {
                int readChannels = selector.select();
                if (readChannels > 0) {//有可用的通道
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        if (key.isReadable()) {
                            SocketChannel socketChannel = (SocketChannel) key.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            socketChannel.read(buffer);
    
                            String msg = new String(buffer.array());
                            System.out.println(msg.trim());
                        }
                    }
                } else {
                    //没有可用的通道
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws IOException {
            GroupCharClient groupCharClient = new GroupCharClient();
    
            new Thread(() -> {
                while (true) {
                    groupCharClient.readMessage();
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
            //发送数据给服务器端
            Scanner scanner = new Scanner(System.in);
    
            while (scanner.hasNextLine()) {
                String message = scanner.nextLine();
                groupCharClient.sendMessage(message);
            }
        }
    
    }
    
    

    服务器端代码

    package com.dawa.netty.nio.group;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.net.SocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.util.Iterator;
    
    /**
     * 服务器端代码
     */
    public class GroupChatServer {
        private ServerSocketChannel serverSocketChannel;
        private Selector selector;
        private static final int PORT = 6666;
    
        public GroupChatServer() {
            try {
                //得到选择器
                selector = Selector.open();
                //绑定端口
                serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(PORT));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //服务器端监听的方法
        public void listen() {
            try {
                //循环监听
                while (true) {
                    int count = selector.select();
                    if (count > 0) {
                        Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
                        while (selectionKeyIterator.hasNext()) {
                            //取出Key
                            SelectionKey key = selectionKeyIterator.next();
    
                            //判断事件
                            if (key.isAcceptable()) {//监听访问
                                //key 转Channel
                                SocketChannel channel = serverSocketChannel.accept();
                                SocketAddress remoteAddress = channel.getRemoteAddress();
                                System.out.println(remoteAddress + ":上线了");
                            }
                            if (key.isReadable()) {//读取事件
                                //处理读
                                readData(key);
                            }
                            //移除已经处理的key
                            selectionKeyIterator.remove();
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //读数据
        public void readData(SelectionKey key) {
            SocketChannel channel = null;
            try {
                //根据key,取得Channel
                channel = (SocketChannel) key.channel();
                channel.configureBlocking(false);
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int read = channel.read(buffer);
                if (read > 0) {
                    String message = new String(buffer.array());
                    System.out.println("from: 客户端" + message);
    
                    // 向其他用户,转发消息
                    sendMessageToOtherCLient(message,key);
                }
    
            } catch (IOException e) {
                try {
                    System.out.println(channel.getRemoteAddress() + " :离线了");
                    key.cancel();
                    channel.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    
        //向其他用户转发消息
        public void sendMessageToOtherCLient(String message,SelectionKey self){
            System.out.println("服务器转发消息ing");
    
            selector.keys().forEach(key -> {
                //根据Key,取出对应的SocketChannel.或者是ServerSocketChannel
                Channel targetChannel = key.channel();
                //排除自己
                if (targetChannel instanceof SocketChannel && targetChannel != self) {
                    //转型
                    SocketChannel dest = (SocketChannel) targetChannel;
                    //Buffer
                    ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                    try {
                        dest.write(buffer);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
    
        }
    
        public static void main(String[] args) {
            GroupChatServer groupChatServer = new GroupChatServer();
            groupChatServer.listen();
        }
    }
    
    

    NIO与零拷贝

    什么是零拷贝?

    零拷贝,指的是没有CPU拷贝,而不是不拷贝。从操作系统角度看的。

    基本介绍

    image-20200330051538095

    传统IO数据读写

    image-20200330051657640

    传统IO模型图,状态切换:用户态和内核态的切换:4次拷贝,3次切换

    MMAP优化

    image-20200330052042194

    Mmap优化:3次拷贝,3次切换

    DMA拷贝:direct memory accect:直接内存访问

    sendFile优化

    image-20200330052351584

    sendFile优化:三次拷贝,两次切换

    零拷贝

    零拷贝,指的是没有CPU拷贝,而不是不拷贝。从操作系统角度看的。

    image-20200330052437619

    这里其实还有一次CPU拷贝的:kernel buffer->socket buffer但是,拷贝的信息很少,比如length,offset,消耗低,可以忽略

    零拷贝:2次拷贝,2次切换。

    image-20200330052906579

    零拷贝是我们在进行网络传输的重要优化手段。

    mmap和sendFile的区别:

    image-20200330052955634

    NIO零拷贝案例

    image-20200330053238276

    传统IO流案例

    image-20200330053408571

    image-20200330053600538

    传统IO耗费时间:60毫秒

    零拷贝案例。(NIO)

    transferTo 底层用的就是零拷贝

    image-20200330055530665

    image-20200330055035445

    NIO零拷贝耗时时间:20毫秒

    Java AIO编程

    Java AIO基本介绍

    image-20200330055721122

    这里暂时不深入扩展。

    BIO、NIO、AIO对比表

    image-20200330055912676

  • 相关阅读:
    微博CacheService架构浅析 对底层协议进行适配
    Lucene 查询原理 传统二级索引方案 倒排链合并 倒排索引 跳表 位图
    Linux kernel 同步机制
    对话 CTO〡用声音在一起,听荔枝 CTO 丁宁聊 UGC 声音互动平台的技术世界 原创 王颖奇 极客公园 2018-12-01
    当中台遇上DDD,我们该如何设计微服务?
    京东技术沙龙系列之二 | 深度解析京东微服务组件平台
    gRPC设计动机和原则
    微信全文搜索优化之路
    门户级UGC系统的技术进化路线——新浪新闻评论系统的架构演进和经验总结 提高响应性能的手段归根结底就是三板斧:队列(Queue)、缓存(Cache)和分区(Sharding)
    现加减乘除4则运算
  • 原文地址:https://www.cnblogs.com/bigbaby/p/12596350.html
Copyright © 2020-2023  润新知