NIO快速入门 --- 三大核心组件Buffer,Channel,Selector,NIO聊天室系统,NIO与零拷贝

一、I/O模型简介

Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO

  • BIO:同步阻塞 传统IO就是BIO,实现模式为一个连接一个线程,即每有一个客户端请求时,服务器端就需要开启一个线程进行处理。如果这个连接不做任何事情就会造成不必要的线程开销。
    • 适用场景: BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高, 并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
  • NIO:同步非阻塞 实现模式为一个线程处理多个请求 ,单线程的多路复用技术,采用轮询机制,谁有IO请求就处理谁,其他的连接可以先干自己的事
    • 适用场景:NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
  • AIO:异步非阻塞 AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程
    • 适用场景:AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

二、BIO和NIO

2.1 BIO

Java BIO 就是传统的java io 编程,同步阻塞 会造成不必要的线程开销,可以采用线程池机制改进

工作原理图:

image-20220119155538605

工作流程:

  1. 服务器端启动一个ServerSocket
  2. 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户端建立一个线程与之通讯
  3. 客户端发出请求后, 先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
  4. 如果有响应,客户端线程会等待请求结束后,再继续执行

一个demo:

public class BIODemo {
    public static void main(String[] args) throws Exception{
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("Server启动了");
        while (true){
            System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
            //监听,等待客户端连接
            System.out.println("等待连接....");
            final Socket socket = serverSocket.accept();
            System.out.println("连接到了一个客户端");
            threadPool.execute(()->{
                handler(socket);
            });
        }

    }

    public static void handler(Socket socket) {
        try {
            System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket 获取输入流
            InputStream inputStream = socket.getInputStream();

            //循环的读取客户端发送的数据
            while (true) {

                System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());

                System.out.println("read....");
                int read =  inputStream.read(bytes);
                if(read != -1) {
                    System.out.println(new String(bytes, 0, read)); //输出客户端发送的数据
                } else {
                    break;
                }
            }

        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            }catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

使用telnet 127.0.0.1 6666 连接

image-20220119160131472

问题分析:

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

2.2 NIO

NIO基本介绍:

NIO:non-blocking IO,是JDK1.4之后提出的IO改进的新模式,是同步非阻塞的,采用多路复用技术,一个连接可以并发处理多个请求

核心三大部分:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector选择器

NIO是面向缓冲区,面向块编程的。将数据读取到一个缓冲区,需要时再调用,提高了处理过程的灵活性

非阻塞解释:

使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情

通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来, 根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。

NIO与BIO对比

  1. BIO以流的方式处理数据,而 NIO以块的方式处理数据。块 I/O 的效率比流 I/O 高很多
  2. BIO是阻塞的,NIO是非阻塞的
  3. BIO基于字节流和字符流,NIO基于Channel通道和Buffer缓冲区操作,用Selector选择器监听多个通道的事件。因此NIO可以使用单线程来监听多个客户端

NIO核心三大部分关系

image-20220119162916913
  1. 一个线程Thread对应一个选择器Selector,一个Selector对应多个Channel连接,即一个线程能对应多个客户端连接
  2. Channel需要注册到Selector中,Selector会根据不同事件Event在各个通道上切换
  3. 每个Channel对应一个Buffer,Channel可以双向,Buffer也可以双向
  4. Buffer是一个内存块,底层是一个数组,数据的读取写入是通过Buffer的

三、缓冲区Buffer

基本介绍

缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块

缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况

Channel 提供从文件、 网络读取数据的渠道,但是***读取或写入的数据都必须经由Buffer***

image-20220119172738198

在NIO中,Buffer是一个顶层父类

image-20220119171614509

常用子类:如图

Buffer重要属性和API

属性描述
Capacity容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
Limit表示缓冲区的当前终点,极限位置,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
Position位置,position表示当前的位置,读/写一个就会后移
Mark标记

注意:Buffer能够读写切换,读->写用clear,写->读用flip

开始为写模式,limit为capacity,表示最大能写capacity这么多

使用flip后,limit会指向原来position的位置,即最大只能读原来写入的长度

image-20220119174024041

Buffer常用API

public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPosition)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}

ByteBuffer

Buffer是一个抽象类,最常用的子类是ByteBuffer

public abstract class ByteBuffer {
//缓冲区创建相关api
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
//构造初始化位置offset和上界length的缓冲区
public static ByteBuffer wrap(byte[] array,int offset, int length)
//缓存区存取相关API
public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
public abstract byte get (int index);//从绝对位置get
public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
}

MappedByteBuffer

MappedByteBuffer可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO来完成

public class MappedByteBufferDemo {
    public static void main(String[] args) throws Exception{
        RandomAccessFile file = new RandomAccessFile("nio.txt", "rw");

        FileChannel channel = file.getChannel();
        /**
         * 参数1:使用什么模式:READ_WRITE读写模式
         * 参数2:0 :从什么地方开始,可以修改的位置
         * 参数3:5 :文件映射到内存的大小,将nio.txt中5个字节的内容映射到内存,即能修改的部分只有前面5个字节
         */
        MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        map.put(0,(byte) 'G');
        map.put(1,(byte) 'O');
        map.put(2,(byte) 'O');
        map.put(3,(byte) 'D');
        map.put(4,(byte) '中');
        //从0开始只能改5个字节,中文占2个字节,最后一个修改是乱码的

    }
}

Buffer的Scattering 和 Gathering

前面我们讲的读写操作,都是通过一个Buffer完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即分散和聚集Scattering 和 Gathering

public class ScatteringAndGatheringDemo {
    public static void main(String[] args) throws Exception{
        //使用 ServerSocketChannel 和 SocketChannel 网络传输
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);

        //绑定端口
        serverSocketChannel.socket().bind(inetSocketAddress);

        //创建一个Buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[3];
        byteBuffers[0] = ByteBuffer.allocate(3);
        byteBuffers[1] = ByteBuffer.allocate(5);
        byteBuffers[2] = ByteBuffer.allocate(7);

        //等待客户端连接
        System.out.println("等待连接。。");
        SocketChannel socketChannel = serverSocketChannel.accept();
        System.out.println("连接了一个客户端");

        int messageLength = 15;
        while (true){
            int byteRead = 0;
            while (byteRead<messageLength){
                long read = socketChannel.read(byteBuffers);
                byteRead+=read;//读取的字节数加上去
                System.out.println("byteRead="+byteRead);
                Arrays.asList(byteBuffers).stream()
                        .map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit())
                        .forEach(System.out::println);
            }

            //将所有的buffer进行flip
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());

            //将数据读出显示到客户端
            long byteWirte = 0;
            while (byteWirte < messageLength) {
                long l = socketChannel.write(byteBuffers);
                byteWirte += l;
            }

            //将所有的buffer 进行clear
            Arrays.asList(byteBuffers).forEach(buffer-> { buffer.clear(); });

            System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messagelength" + messageLength);
        }

    }
}
image-20220119184228923

注意事项:

ByteBuffer支持类型化put和get,put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常

public class ByteBufferTypeDemo {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        byteBuffer.putInt(10);
        byteBuffer.putDouble(200L);
        byteBuffer.putFloat(100F);
        byteBuffer.putChar('L');
        byteBuffer.putShort((short) 2);

        byteBuffer.flip();

        System.out.println(byteBuffer.getInt());
        System.out.println(byteBuffer.getDouble());
        System.out.println(byteBuffer.getFloat());
        //System.out.println(byteBuffer.getFloat());把getFloat改为getDouble就报错了
        //byteBuffer能get的范围由原来put进来的值属性决定,原来有多少个字节,只能get这些字节范围内的值
        //如果范围加一起超过原来范围就抛出异常,如果小于之后的字节就乱组导致乱码
        System.out.println(byteBuffer.getChar());
        System.out.println(byteBuffer.getShort());

    }
}

byteBuffer能get的范围由原来put进来的值属性决定,原来有多少个字节,只能get这些字节范围内的值
如果范围加一起超过原来范围就抛出异常,如果小于之后的字节就乱组导致乱码

四、通道Channel

基本介绍

NIO通道类似于流,但是又比流高级

  • 通道可以同时读写,是双向的;而流只能读或只能写,是单向的;通道可以从缓冲读数据,也可以写数据到缓冲。
  • 通道可以实现异步读写数据
  • Channel在NIO中是一个接口

常用的Channel:

  • FileChannel:用于文件的数据读写
  • DatagramChannel:用于UDP数据的读写
  • ServerSocketChannel:类似ServerSocket。用于TCP的数据读写
  • SocketChannel:类似Socket。用于TCP数据的读写

FileChannel

FileChannel主要用来对本地文件进行IO操作。常用方法:

public int read(ByteBuffer dst) //从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) //把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count)//从目标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target)//把数据从当前通道复制给目标通道

ServerSocketChannel

ServerSocketChannel 用来在服务器端监听新的客户端 Socket 连接 常用方法:

public static ServerSocketChannel open();//得到一个 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local)//设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block)//设置阻塞或非阻塞模式,取值false表示采用非阻塞模式
public SocketChannel accept()//接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops)//注册一个选择器并设置监听事件

SocketChannel

SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区

public static SocketChannel open();//得到一个 SocketChannel 通道public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞模式 取值false表示采用非阻塞模式public boolean connect(SocketAddress remote);//连接服务器public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作public int write(ByteBuffer src);//往通道里写数据public int read(ByteBuffer dst);//从通道里读数据public final SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据public final void close();//关闭通道

Buffer和Channel实例:

1. 本地文件写

public class NIOFileChannelWrite {    public static void main(String[] args) throws IOException {        String str = "hello,NIO\n";        //创建输出流,如果文件不存在则创建一个        File file = new File("E:\\教材作业\\Java\\Netty-Study\\nio.txt");        if (!file.exists()) file.createNewFile();        //输出流开启 append 开关,往后追加        FileOutputStream fileOutputStream = new FileOutputStream(file,true);        //获取输出通道 FileChannel        FileChannel fileChannel = fileOutputStream.getChannel();        //获取一个字节缓存区ByteBuffer        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);        //往里put值        byteBuffer.put(str.getBytes());        //读写转换        byteBuffer.flip();        //将字节缓存区的文件写入 文件中        fileChannel.write(byteBuffer);        fileOutputStream.close();    }}

2. 本地文件读

public class NIOFileChannelRead {    public static void main(String[] args) throws Exception{        //创建输入流        File file = new File("E:\\教材作业\\Java\\Netty-Study\\nio.txt");        FileInputStream fileInputStream = new FileInputStream(file);        //从输入流中拿取通道channel,FileChannel -> 实际类型 FileChannelImpl        FileChannel channel = fileInputStream.getChannel();        //new 一个和 文件等长的  字节缓冲区ByteBuffer        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());        //将通道中的数据读入到  buffer 中        channel.read(byteBuffer);        //将字节数组转为 String        System.out.println(new String(byteBuffer.array()));        fileInputStream.close();    }}

3. 使用一个Buffer完成读和写

public class NIOFileReadAndWrite {    public static void main(String[] args) throws Exception {        //输入文件为nio.txt        File fileInput = new File("nio.txt");        FileInputStream fileInputStream = new FileInputStream(fileInput);        //输入管道        FileChannel fromChannel = fileInputStream.getChannel();        //输出到nio2.txt        File fileOutput = new File("nio2.txt");        if (!fileOutput.exists()) fileOutput.createNewFile();        FileOutputStream fileOutputStream = new FileOutputStream(fileOutput);        //输出管道        FileChannel toChannel = fileOutputStream.getChannel();        ByteBuffer buffer = ByteBuffer.allocate(2);        while (true){            //buffer从头开始写            buffer.clear();            int read = fromChannel.read(buffer);            if (read==-1) break;//read为-1读完了			//buffer转换为读模式            buffer.flip();            toChannel.write(buffer);        }        fileInputStream.close();        fileOutputStream.close();        System.out.println("操作完成。。");    }}

4. 拷贝文件transferFrom

public class NIOFileChannelCopy {    public static void main(String[] args) throws Exception{        FileInputStream inputStream = new FileInputStream("E:\\谷歌浏览器下载\\8.jpg");        FileOutputStream outputStream = new FileOutputStream("copy.jpg");        FileChannel fromChannel = inputStream.getChannel();        FileChannel toChannel = outputStream.getChannel();        toChannel.transferFrom(fromChannel,0,fromChannel.size());                System.out.println("拷贝成功");        toChannel.close();        fromChannel.close();        inputStream.close();        outputStream.close();    }}

五、选择器Selector

基本介绍

NIO中单线程的多路复用就是用的Selector选择器

Selector 能够检测多个注册的通道上是否有事件发生 (注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然 后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个 通道,也就是管理多个连接和请求。

只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销

image-20220120141533942

Selector常用方法

Selector类是一个抽象类

public abstract class Selector implements Closeable {
    public static Selector open();//得到一个选择器对象
	public int select();//阻塞直到有通道可以选择
    public int selectNow();//不阻塞,立马返回
    public int select(long timeout);//监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
    public Selector wakeup();//唤醒selector
    public Set<SelectionKey> keys();//从内部集合中得到所有的 SelectionKey  
    public Set<SelectionKey> selectedKeys();//获取有事件发生的SelectionKey集合
}

NIO非阻塞网络编程原理

image-20220120142350857

如图:

  1. 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
  2. Selector 进行监听 select 方法, 返回有事件发生的通道的个数
  3. 将SocketChannel注册到Selector上, 一个 selector上可以注册多个SocketChannel
  4. 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
  5. 进一步得到各个 SelectionKey (有事件发生)
  6. 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
  7. 可以通过得到的 channel , 完成业务处理

SelectionKey

SelectionKey:表示 Selector 和网络通道的注册关系,一共有四种

  • OP_ACCEPT:有新的网络连接可以 accept,值为 16
  • OP_CONNECT:代表连接已经建立,值为 8
  • OP_READ:代表读操作,值为 1
  • OP_WRITE:代表写操作,值为 4

相关方法

public abstract class SelectionKey {
public abstract Selector selector();//得到与之关联的Selector 对象
public abstract SelectableChannel channel();//得到与之关联的通道
public final Object attachment();//得到与之关联的共享数据,如Buffer
public abstract SelectionKey interestOps(int ops);//设置或改变监听事件
public final boolean isAcceptable();//是否可以 accept
public final boolean isReadable();//是否可以读
public final boolean isWritable();//是否可以写
}

六、NIO综合实例

6.1 NIO简单通讯

服务端:

public class NIOServer {
    public static void main(String[] args) throws Exception{
        //创建一个ServerSocketChannel , 用 ServerSocketChannel  之后获取  SocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //创建一个选择器Selector
        Selector selector = Selector.open();
        //绑定服务器端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        serverSocketChannel.configureBlocking(false);   //非阻塞

        //ServerSocketChannel 也要注册 到Selector,关心事件为 OP_ACCEPT,等待连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true){
            //selector.select() == 0 :没有事件发生
            if(selector.select(2000)==0){  //没有事件发送
                System.out.println("服务器等待 2s ,无连接");
                continue;
            }
            //selector.select() > 0 :有事件发生,现在要获取这些事件
            //获取有事件发生的SelectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                //获取SelectionKey,根据key的事件是什么类型进行处理
                SelectionKey key = keyIterator.next();

                //根据不同事件进行处理
                if (key.isAcceptable()){//如果是OP_ACCEPT连接事件
                    //为该客户端生成一个SocketChannel进行传输
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    System.out.println("一个客户端连接成功,并生成一个SocketChannel"+socketChannel.hashCode());
                    //将客户端的通道注册到selector,并绑定事件为OP_READ,同时关联一个Buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if(key.isReadable()){//如果是OP_READ读事件
                    //用key拿到channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    //上面关联了一个Buffer,现在能拿到
                    ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                    //接收到客户端传来的消息
                    channel.read(byteBuffer);
                    System.out.println("从客户端传来的消息:"+new String(byteBuffer.array()));
                }

                //需要移除当前的SelectionKey,避免重复操作
                keyIterator.remove();

            }
        }
    }
}

客户端:

public class NIOClient {
    public static void main(String[] args) throws Exception {
        //获得一个SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        //服务器端的ip和端口号
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

        //连接服务器,如果失败
        if(!socketChannel.connect(inetSocketAddress)){
            while (!socketChannel.finishConnect()){
                System.out.println("连接需要时间,在这段时间客户端不会阻塞,可以做其他事");
            }
        }
        //连接成功之后
        String str = "hello,my NIO";
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        //将buffer中的数据写入channel
        socketChannel.write(byteBuffer);
        System.out.println("客户端发送一条消息,SocketChannel为"+socketChannel.hashCode());
        //客户端停在这
        System.in.read();
    }
}
image-20220120152101234

启动两个客户端,生成两个SocketChannel

6.2 NIO群聊系统

Demo任务:

  1. 实现服务器 端和客户端之间的数据简单通讯(非阻塞)
  2. 实现多人群聊
  3. 服务器端:可以监测用户上线,离线,并实现消息转发功能
  4. 客户端:通过channel 可以无阻塞发送 消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)

服务器端:

public class GroupServer {

    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int port = 6666;

    //初始化
    public GroupServer(){
        try {
            //获取Selector和ServerSocketChannel
            selector = Selector.open();
            listenChannel = ServerSocketChannel.open();
            //绑定属性
            listenChannel.socket().bind(new InetSocketAddress(port));
            listenChannel.configureBlocking(false);
            //关注连接事件
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //监听
    public void listen(){
        try {
            while (true){
                //如果count大于0就有事件需要处理
                int count = selector.select(2000);
                if (count>0){
                    //获取有事件的SelectionKey集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                    while (keyIterator.hasNext()){
                        //遍历集合中的key
                        SelectionKey key = keyIterator.next();
                        if(key.isAcceptable()){//连接事件
                            //用ServerSocketChannel建立一个SocketChannel连接
                            SocketChannel socketChannel = listenChannel.accept();
                            socketChannel.configureBlocking(false);
                            //注册
                            socketChannel.register(selector,SelectionKey.OP_READ);

                            System.out.println(socketChannel.getRemoteAddress()+"上线了");

                        }
                        if (key.isReadable()){//读事件
                            readData(key);
                        }

                        keyIterator.remove();
                    }


                }else {
                    System.out.println("等待客户端连接。。");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读数据
    private void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            if (count>0){
                String msg = new String(buffer.array());
                System.out.println( "客户端发来:"+msg);
                //转发
                sendToOtherClients(msg,channel);
            }
        }catch (IOException e){
            try {
                System.out.println(channel.getRemoteAddress()+"离线了");
                //取消注册
                key.channel();
                //关闭通道
                channel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    //排除自己给其他客户发
    private void sendToOtherClients(String msg,SocketChannel self) throws IOException{
        System.out.println("转发消息中。。");
        for (SelectionKey key : selector.keys()){
            Channel target = key.channel();
            if(target instanceof SocketChannel && target!=self){
                //转型
                SocketChannel targetChannel = (SocketChannel) target;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                targetChannel.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        GroupServer server = new GroupServer();
        server.listen();
    }
}

客户端:

public class GroupClient {
    private final String host = "127.0.0.1";
    private final int port = 6666;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public GroupClient(){
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(host,port));
            socketChannel.configureBlocking(false);
            //注册
            socketChannel.register(selector, SelectionKey.OP_READ);
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println("初始化成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendInfo(String info){
        info = username + "说:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readInfo(){

        try {
            int read = selector.select();
            if(read>0){
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                while (keyIterator.hasNext()){
                    SelectionKey key = keyIterator.next();
                    if(key.isReadable()){
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        System.out.println(new String(buffer.array()));
                    }else {
                        System.out.println("没有可以用的通道。。");
                    }

                    keyIterator.remove();
                }

            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        GroupClient client = new GroupClient();
        new Thread(()->{
            while (true){
                client.readInfo();
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String msg = scanner.nextLine();
            client.sendInfo(msg);
        }
    }
}

运行界面:

image-20220120161036330

七、NIO与零拷贝

我们都知道NIO会更快,那么为什么呢?NIO快的原因:零拷贝。

那么零拷贝是什么呢?

零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽

Java中零拷贝的两种常用方式:

  • mmap内存映射
  • sendFile

传统IO方式

image-20220120164514651

image-20220120171040921

  1. 从硬盘由DMA方式拷贝到内核态缓冲区,用户态->内核态切换
  2. 从内核缓冲区由CPU拷贝到用户态缓冲区,内核态->用户态切换
  3. 从用户态缓冲区由CPU拷贝到套接字缓冲区,用户态->内核态切换
  4. 从套接字缓冲区由DMA方式拷贝到协议栈,这一段不需要进行上下文切换

一共4次拷贝, 3次状态切换

DMA :direct memory access 直接内存拷贝,不使用CPU

mmap优化

image-20220120165526168

image-20220120170700738

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。

现在,只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝,但不减少上下文切换次数。

一共3次拷贝,3此状态切换

sendFile优化

image-20220120165929378

image-20220120170936385

  • Linux 2.1 版本提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
  • Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。(这里其实有 一次cpu 拷贝 kernel buffer -> socket buffer 但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略)

一共3次拷贝,2次切换


总结:

  • 零拷贝是从操作系统的角度来看的。内核缓冲区之间, 没有数据是重复的(只有kernel buffer有一份数据)。
  • 零拷贝不仅仅带来更少的数据复制, 还能带来其他的性能优势: 如更少的上下文切换, 更少的 CPU 缓存伪共享以及无CPU校验和计算。
  • mmap适合小数据两读写, sendFile适合大文件传输
  • mmap 需要3次上下文切换, 3次数据拷贝; sendFile 需要3次上下文切换, 最少2次数据拷贝。
  • sendFile 可以利用 DMA 方式, 减少 CPU 拷贝, 而 mmap则不能(必须从内核拷贝到Socket缓冲区)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值