关于spring:不会吧做了这么久开发还有不会NIO的看看BAT大佬是怎么用的吧

56次阅读

共计 10115 个字符,预计需要花费 26 分钟才能阅读完成。

前言

  • 在将 NIO 之前,咱们必须要理解一下 Java 的 IO 局部常识。
  • BIO(Blocking IO)
  • 阻塞 IO,在 Java 中次要就是通过 ServerSocket.accept()实现的。
  • NIO(Non-Blocking IO)
  • 非阻塞 IO,在 Java 次要是通过 NIOSocketChannel + Seletor 实现的。
  • AIO(Asyc IO)
  • 异步 IO,目前不做学习。

BIO

简略实现服务器和客户端

package net.io;

import net.ByteUtil;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

//NIO(NonBlocking IO)非阻塞 IO
// 通过一个事件监听器,吧这些客户端的连贯保存起来,如果有工夫产生再去解决,没工夫产生不解决
public class Server {public Server(int port) {
        try {
            // 创立服务器端,监听端口 port
            ServerSocket serverSocket = new ServerSocket(port);
            // 对客户端进行一个监听操作,如果有连贯过去,就将连贯返回(socket)----- 阻塞办法
            while (true) {
                // 监听,阻塞办法
                Socket socket = serverSocket.accept();
                // 每个服务器和客户端的通信都是针对与 socket 进行操作
                System.out.println("客户端" + socket.getInetAddress());
                InputStream inputStream = socket.getInputStream();
                ObjectInputStream ois = new ObjectInputStream(inputStream);
                // 获取客户端发送的 message
                Object get = ois.readObject();
                System.out.println("接管到的音讯为:"  + get);

                // 服务器须要给客户端进行一个回应
                OutputStream outputStream = socket.getOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(outputStream);
                String message = "客户端你好,我是服务器端";
                // 我这里写了,不代表发送了,常识写到了输入流的缓冲区
                oos.writeObject(message);
                // 发送并清空
                oos.flush();}

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

    public static void main(String[] args) {new Server(7000);
    }
}
package net.io;

import net.ByteUtil;

import java.io.*;
import java.net.Socket;

public class Client {public Client(int port){
        try {Socket socket = new Socket("localhost",port);

            //inputStream 是输出流,从里面接管信息
            //outpurStream 是输入流,往外面输入信息
            OutputStream outputStream = socket.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(outputStream);
            // 发送的信息
            String message = "服务器你好,我是客户端";
            // 我这里写了,不代表发送了,常识写到了输入流的缓冲区
            oos.writeObject(message);
            // 发送并清空
            oos.flush();

            // 接管服务器的回应
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            Object get = ois.readObject();
            System.out.println("接管的信息为:" + get);
        } catch (Exception e) {e.printStackTrace();
        }
    }

    public static void main(String[] args) {new Client(7000);
    }
}

针对于 BIO,为什么是阻塞 IO,是因为 BIO 是基于 Socket 实现数据的读写操作,Server 调用 accept()办法继续监听 socket 连贯,所以就阻塞在 accept()这里(前面的操作如果没有 socket 连贯则无奈执行),那这样就代表服务器只能同时解决一个 socket 的操作。
所以后序咱们通过为 socket 连贯创立一个线程执行,这样就能够增大服务器解决连贯的数量了。
然而新的问题也就进去了,如果客户端的连贯很多,那么就会导致服务器创立很多的线程
对 socket 进行解决,如果服务器端不断开连接的话,那么对应的线程也不会被销毁,这样大数量的线程的保护非常耗费资源。针对于这种状况设计出了 Java 的 NIO。

NIO

首先咱们须要介绍一下 NIO。如果说 BIO 是面向 socket 进行读写操作的话,那么 NIO 则是面向 channel 进行读写操作(或者说面向 buffer)。
这里咱们在解说 NIO 之前,须要先解说一下这个 buffer。家喻户晓这就是一个缓冲,并且咱们晓得 socket 具备输出流 inputStream 和输入流 outputStream(读写离开的),然而咱们的 channel 是同时具备 read 和 write 两个办法,而且两个办法都是基于 buffer 进行操作(这里就能够阐明 channel 仅能比一般输入输出流好,相当于 channel 是一条双向,输入输出流是两条单向),所以咱们能够晓得 buffer 的重要性。

Buffer

诸如 ByteBuffer,IntBuffer 等都是 Buffer 的派生抽象类,须要调用抽象类的静态方法 allocate(X capacity)办法进行一个初始化操作,该办法就是初始化 buffer 的大小, 或者应用 wrap(X x)办法,该办法相当于间接将信息存入缓冲中。至于存入 buffer 的 put()办法和取出缓存的 get()办法在上面代码中我就具体介绍(有底层常识,具备源码浏览能力的能够依据我的正文进行浏览),最要害的还有 flip()办法,它是作为一个读写切换的作用,他使的缓存可又读又写,又使得读写互相隔离(须要留神的是应用 buffer 尽量是顺次写完而后再一次读完,最初在调用 clear()办法进行复位,不然会导致 buffer 容量越来越小,具体解释在上面代码)。

package net.nio.buffer;

import java.nio.IntBuffer;

public class TestBuffer {public static void main(String[] args) {

        /*
         *IntBuffer 有四个重要参数
         * 1.mark 标记
         * 2.position 相当于以后下标、索引
         * 3.limit 代表缓冲的起点,读取不能超过该下标,当然也不能超过最大容量。(在调用 flip 时候会将以后下标 position 值赋值给 limit,而后 position 置 0)* 4.Capacity 最大容量,在初始化 IntBuffer 对象时候就定义好了,不能扭转(IntBuffer.allocate(int capacity))*
         * ctrl+h 能够查看该类的子类
         */

        //intBuffer 初始化
        IntBuffer intBuffer = IntBuffer.allocate(5);

        // 放数据到缓冲区中
        intBuffer.put(10);
        intBuffer.put(11);
        intBuffer.put(12);
//        intBuffer.put(13);
//        intBuffer.put(14);

        /*
         * 这里的读写反转的实现机制是:* 例如咱们缓冲区容量为 5,调用办法 put()将数据写入缓冲区中,如果咱们写入三个此时 position 为 3,此时 limit = capacity
         * 如果咱们调用 flip 办法使得 limit = 3 ,position = 0 ,mark 咱们当初先不论(下图源码已阐明)*  public Buffer flip() {
         *      limit = position;
         *      position = 0;
         *      mark = -1;
         *      return this;
         * }
         *
         * 此时咱们调用 get()办法时候,获得下标是 position 的值,即从 0 下标读取。直到读取到 position = limit = 3 时候进行(不包含 3)* 1. 如果咱们这个时候不调用 flip()办法间接再次 put()往缓冲区写入数据(即没从读状态切换到写状态),那么就会报错超过下标 overflow
         * 2. 如果咱们调用一次 flip()(即进入写状态)写入一个数据后,那么此时 position = 0,limit = 3,此时咱们最多寄存 3 个数据(即下标 0,1,2)* 如果咱们不再次调用 flip()切换状态那么就会导致,读取到谬误数据,(即只存入了一个数据,然而却取出来了 3 个数据)*
         * 上述阐明了一个问题,如果咱们存取的数据越来越小,那么这个缓冲区逐步放大,导致并不能存取他的最大容量,可能会节约内存,*(因为 position 是不能超过 limit 的,然而调用 flip()办法后会使的 limit = position(赋值操作),那么如果数据越来越少,* 就会导致缓冲区能应用的局部越来越小)*
         * 总结:缓冲区的大小设置应该依据理论应用进行设置(并且要及时调用 clear()),否则可能会导致缓冲区的内存节约。*/
        intBuffer.flip();
        // 切换读写状态
        // 判断缓存区是否还有残余
        while (intBuffer.hasRemaining()) {System.out.println(intBuffer.get());
        }
    }
}

Channel

Channel 是 NIO 实现的根底,对于 NIO,Channel 的位置相当于 BIO 的 socket。
Channel 具备十分多办法,其中应用最多的就是两个办法 write(ByteBuffer buf)和 read(ByteBuffer buf)办法。
(这里须要留神的是这个 read 和 write 是 buffer 作为主体的,即 read()办法是 channel 往 buffer 里写数据,而 write()办法是指 buffer 向 channel 写数据)

package net.nio.channel;

        import java.io.FileOutputStream;
        import java.nio.ByteBuffer;
        import java.nio.channels.FileChannel;

public class TestChannel {public static void main(String[] args) throws Exception{
        String abc = "我写入文件了";

        // 写入的文件地址与文件名
        FileOutputStream fileOutputStream = new FileOutputStream("C:\\xxx\\xxx\\xxx\\test.txt");

        // 从输入流中获取 channel 实例
        FileChannel channel = fileOutputStream.getChannel();

        // 创立字节缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 将字符串转化成字节数组并放入缓冲区中,而后缓冲区转换读写状态,由写入状态变为读取状态
        byteBuffer.put(abc.getBytes());
        byteBuffer.flip();

        // 将缓冲区数据写入到 channel 中(这里 write 代表从缓冲区写入,read 代表从 channel 读取到缓冲区)channel.write(byteBuffer);
        // 敞开通道和输入流
        channel.close();
        fileOutputStream.close();}
}

简略 NIO 实现

下面在介绍 NIO 时候讲过,NIO 是须要一个 Selector 线程去监听那些客户端有实现产生,从而在进行解决,而不是 BIO 的一个线程保护一个 socket。

上面针对于 NIO 咱们先不引入 Selector,就用 BIO 的形式实现一个客户端和服务器端。(相当于作为一个练手)

Server

package net.nio.socket;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

public class Server {public static void main(String[] args) throws Exception {
        // 开启 nio 的服务器端,并且绑定 8000 端口进行监听
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(8000);
        serverSocketChannel.bind(inetSocketAddress);

        // 创立缓冲区数组
        ByteBuffer byteBuffer = ByteBuffer.allocate(40);

        // 服务器端接管来自客户端的申请,创立客户端的 socket 的实例
        SocketChannel socketChannel = serverSocketChannel.accept();
        // 将客户端发送的数据读取到 buffer 数组中
        socketChannel.read(byteBuffer);
        byte[] array = byteBuffer.array();
        String msg = new String(array);
        System.out.println("服务器收到信息:" + msg);

        // 对 buffer 数组进行读写反转,由读状态到写状态
        byteBuffer.flip();

        // 将数据回显到客户端去
        byteBuffer.put("ok".getBytes());

        // 做完一套读写操作后,须要进行 clear
        byteBuffer.clear();}
}

Client

package net.nio.serverclient;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {public static void main(String[] args) throws Exception {SocketChannel socketChannel = SocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);
        socketChannel.configureBlocking(false);

        // 如果客户端未连贯上服务器
        if (!socketChannel.connect(inetSocketAddress)) {System.out.println("客户端连贯不上服务器。。。。");

            // 如果客户端没有实现连贯
            while (!socketChannel.finishConnect()) {System.out.println("连贯中。。。。");
            }
        }

        // 进入到这里阐明连贯胜利
        String message = "hello , Server!";
        ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());

        // 将 buffer 中的数据写入 socketChannel
        socketChannel.write(byteBuffer);
        System.out.println("发送结束");
    }
}

NIO 实现(基于 Selector)

首先咱们须要晓得 Selector 是什么?
Selector 是一个选择器,既然是一个选择器,那么必定是先有选项再有抉择,了解这个后就晓得 channel 必定有的就是 rigister()办法(因为须要将本人注册到 Selector 中)。

既然选项有了,那么如何抉择呢?
Selector 是针对已注册的 channel 中对有事件(例如:服务器:承受,读写,客户端:读写,服务器是在服务器开始就将本人注册,客户端是连贯胜利后由服务器将其注册)产生的 channel 进行解决。

Selector 注册的不是简略的 channel,而是将 channel 和其监听事件封装成一个 SelectionKey 保留在 Selector 底层的 Set 汇合中。

Selector 的 keys()和 selectedKeys()两个办法须要留神:
keys()办法是返回已注册的所有 selectionKey。
selectedKeys()办法是返回有事件产生的 selectionKey。

下面就是 Selector 的简略工作流程,上面我将附上代码,因为有较具体的正文,所以除了重要知识点我不再多介绍。
Server

package net.nio.serverclient;

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;

public class NIOServer {public static void main(String[] args) throws Exception {
        // 开启 ServerSocketChannel 的监听
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 绑定端口 8000
        serverSocketChannel.bind(new InetSocketAddress(8000));

        // 创立 Selector 对象
        Selector selector = Selector.open();

        // 设置监听为非阻塞
        serverSocketChannel.configureBlocking(false);

        // 将 ServerSocketChannel 注册到 Selector 中(注册事件为 ACCEPT)serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //Selector 监听 ACCEPT 工夫
        while (true) {
            // 未有事件产生(上面是期待),返回值为 int, 代表事件产生个数
            if (selector.select(1000) == 0) {System.out.println("服务器期待了 1S,无事件产生。。。。");
                continue;
            }

            // 有客户端申请过去,就获取到相干的 selectionKeys 汇合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // 获取到事件
                SelectionKey selectionKey = iterator.next();
                // 移出读取过的事件
                iterator.remove();

                // 依据对应事件对应解决
                if (selectionKey.isAcceptable()) {
                    // 有新的客户端连贯服务器
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 给客户端设置非阻塞
                    socketChannel.configureBlocking(false);
                    // 设置该 SocketChannel 为读事件,并为它绑定一个 Buffer
                    socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()) {
                    // 通过 Key 反向获取到事件的 channel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    // 获取到事件绑定的 buffer
                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                    socketChannel.read(byteBuffer);
                    // 重置缓冲
                    byteBuffer.clear();
                    String message = new String(byteBuffer.array());
                    System.out.println("接管到客户端信息为:"+ message);
                }
            }
        }
    }
}

Client

package net.nio.serverclient;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class NIOClient {public static void main(String[] args) throws Exception {SocketChannel socketChannel = SocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000);
        socketChannel.configureBlocking(false);

        // 如果客户端未连贯上服务器
        if (!socketChannel.connect(inetSocketAddress)) {System.out.println("客户端连贯不上服务器。。。。");

            // 如果客户端没有实现连贯
            while (!socketChannel.finishConnect()) {System.out.println("连贯中。。。。");
            }
        }

        // 进入到这里阐明连贯胜利
        while(true) {Scanner scanner = new Scanner(System.in);
            String message = scanner.nextLine();
            ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());

            // 将 buffer 中的数据写入 socketChannel
            socketChannel.write(byteBuffer);
            System.out.println("发送结束");
        }

    }
}

咱们这里能够发现,只须要主函数中进行一个死循环,死循环中对 selector 注册的 channel 进行监听(select()办法),有事件产生则依据 channel 注册的监听事件对应进行解决。

这里须要留神的是须要将 ServerSocketChannel 和 SocketChannel 编程非阻塞(调用 configureBlocking(false)),不然是无奈注册到 Selector 中。

还有一件事须要留神:咱们每次是通过 iterator(迭代器)遍历产生工夫的 Set,为了防止反复解决工夫,咱们在获取产生工夫的 selctionKey 当前,就将其 remove()。

最初

感激你看到这里,看完有什么的不懂的能够在评论区问我,感觉文章对你有帮忙的话记得给我点个赞,每天都会分享 java 相干技术文章或行业资讯,欢送大家关注和转发文章!

正文完
 0