JAVA的NIO(一)

NIO


非阻塞io模型。

很多人会先谈谈阻塞io的坏处, 阻塞io会单线程接受然后多线程处理的结构。而他的读写io其实会很花费时间,而创建线程后,会有很多时间浪费与此处。

不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:

  1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
  2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
  3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
  4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大

NIO重要部分

在我理解里NIO重要概念有3个,Select,Channel,buff。还有一个逻辑概念是handler。

select从Channel拿数据,从buff中储存。buff是内存的部分,相当于从内核区域拷贝到用户区域的媒介,提高效率。select和channel不直接接触,而是通过buff来接触。

channel

channel,很抽象的概念,一个传输过程理解。把他类比为进程通信过程使用的管道会好理解点。作用是一个媒介吧,现代设备都很多管线,其实很多设计都来源生活。你去出行,火车站都会有登陆口,每个人通过车票通过登陆口引导,可以直接到达指定的站台。channel作用类似于这个,但channel是双工,每一端可进可出,和tcp类似。

4种类型

FileChannel: 从文件中读写数据;

DatagramChannel: 通过 UDP 读写网络中数据;

SocketChannel: 通过 TCP 读写网络中数据;

ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

buff

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffe
缓冲区变量

因为读写每个channel都是相同的缓冲区。所以读写相当于在一个数组在操作。靠着2个变量limit和position操作和一个函数flip操作。通过通过clear进行置位操作。

selector

选择器,核心部分,作用:轮询channel。对感兴趣的事件进行注册,使用一个单独的channel也就是ServerSocketChannel来接受连接时间,然后根据这个再去处理其他的时间。

public class NIOServer {

    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) {

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // 这个新连接主要用于从客户端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) {

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) {

            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) {
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) {
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}

整体代码如上。其中主要channel是serverSocketchannel,可以用来接受新的连接。然后对于新连接,在创建一个新的channel,在对该channel进行绑定感兴趣事件。而select具体对于channel绑定,需要根据底层算法决定(select,poll,epoll)也就是数组还是红黑树还是链表。

IO过程

IO过程是区分了各个不同IO方式。IO过程宏观上分为

  1. 数据等待过程
  2. 内核区域复制数据到用户区

过程1每次查询数据是否准备好,都会发起系统调用(操作系统高消耗操作),过程1进行阻塞等待结果就是阻塞io,过程1进行轮询查询结果就是非阻塞io。对于io方式的选择并不唯一,都有各自的好处,主要部分是cpu与线程切换消耗的平衡。要根据实际任务场景决定方式,从阻塞时间出发,短期阻塞使用非阻塞方式较好,长期使用非阻塞会造成cpu消耗多。

阻塞io模型

img

非阻塞io模型

img

            selector.select();

而采用IO多路复用,其实本质和阻塞io很像,但区别是多路复用是一次性查询多个IO事件,使用的是同一个线程,一旦有数据准备好了,就会通知返回进行处理。下面简化了这个过程。

Set<SelectionKey> keys = selector.selectedKeys();

然后获得完成的文件描述符,然后得到对应的channel,这个时候数据拷贝到内核区域,然后在使用handler过程处理。

img

NIO的accpet
  public SocketChannel accept() throws IOException {
        synchronized(this.lock) {
            if (!this.isOpen()) {
                throw new ClosedChannelException();
            } else if (!this.isBound()) {
                throw new NotYetBoundException();
            } else {
                SocketChannelImpl var2 = null;
                int var3 = 0;
                FileDescriptor var4 = new FileDescriptor();
                InetSocketAddress[] var5 = new InetSocketAddress[1];

                InetSocketAddress var6;
                try {
                    this.begin();
                    if (!this.isOpen()) {
                        var6 = null;
                        return var6;
                    }

                    this.thread = NativeThread.current();

                    do {
                        var3 = this.accept(this.fd, var4, var5);
                    } while(var3 == -3 && this.isOpen());
                } finally {
                    this.thread = 0L;
                    this.end(var3 > 0);

                    assert IOStatus.check(var3);

                }

                if (var3 < 1) {
                    return null;
                } else {
                    IOUtil.configureBlocking(var4, true);
                    var6 = var5[0];
                    var2 = new SocketChannelImpl(this.provider(), var4, var6);
                    SecurityManager var7 = System.getSecurityManager();
                    if (var7 != null) {
                        try {
                            var7.checkAccept(var6.getAddress().getHostAddress(), var6.getPort());
                        } catch (SecurityException var13) {
                            var2.close();
                            throw var13;
                        }
                    }

                    return var2;
                }
            }
        }
    }

应该是从fd读入,然后又有一个文件描述,然后他设置得InetSocketAddress是一个大小,所以我猜测他是一次只能督导一个。然后通过v6创建v2然后返回。再往里面就是native方法,还不知道怎么看native方法.总的来说accept只能接受一个调用accept.然后accept之后,会将SocketChannelimpl返回,然后就是新的channel了.这个channel就是新channel.

handler

相当于IO完成之后的处理,handler在NIO中是个很弱的概念吧,我看了许多文章都写的很潦草,应该出于NIO本身没对handler这个概念封装,所以NIO直接使用上往往都没逻辑解耦,而在Netty中handler会好使用很多,使用封装后对这个概念进行强化。
来说accept只能接受一个调用accept.然后accept之后,会将SocketChannelimpl返回,然后就是新的channel了.这个channel就是新channel.

handler

相当于IO完成之后的处理,handler在NIO中是个很弱的概念吧,我看了许多文章都写的很潦草,应该出于NIO本身没对handler这个概念封装,所以NIO直接使用上往往都没逻辑解耦,而在Netty中handler会好使用很多,使用封装后对这个概念进行强化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值