NIO之bio、nio、多路复用器发展历程(一)

系列文章目录

NIO之bio、nio、多路复用器发展历程(一)
NIO之select、poll、epoll 内核触发模型对比(二)
NIO之epoll(三)



前言

ps:如果你是一个每天都在镜子面前给自己磕头硬核男人,可以忽略下面的建议

各位老表,如果是新来的,建议先看下,操作系统相关的知识:深层次详解同步IO、异步IO、阻塞IO、非阻塞IO

各位看官,这边看起。
那得从 long long ago谈起了,当时限于业务和pg智力,最开始流行的是BIO ,每次为新的连接分配一个线程,连接激增时会导致堆栈溢出、线程无法分配、系统崩溃现象。程序员一看,这是要背锅的节奏呀,三下五除二改成了伪异步IO(bio +线程池+任务队列),这下不用担心资源一直分配了,但是换汤不换药,进程阻塞、服务超时、系统崩溃等问题依然存在。dang dang dang 主角来了,NIO (非阻塞io)基于同步非阻塞io模型,在获取连接和从client 获取数据时实现了非阻塞(也可以设置为阻塞),虽然实现了非阻塞,但是还是需要程序循环连接(循环fds-文件描述符列表)一个个发生系统调用(会发生用户态、内核态切换),消耗资源、效率低。那怎么才能不一 一个循环的系统调用呢,你一次性给他1000个,内核自己去循环判断不得了,聪明,多路复用就是起到这个作用,多路复用是一种规则,实现方式有select>poll>epoll ,性能从低到高。

bio(阻塞,流操作)–>nio(只是非阻塞、块操作)–>多路复用器

在这里插入图片描述


一、BIO

包: java.io.*

类别

  • 传统的bio: 通常有一个独立的线程负责监听客户端的连接,会为每个新来的连接请求创建一个新的线程进行读写操作,由于jvm的资源有限,当访问量激增后,系统性能急剧下降,可能发生堆栈溢出、无法创建新线程等,最终导致进程宕机。
  • 伪异步io: 在bio基础上,采用了线程池+任务队列的方式,代替了为每个连接创建一个线程的方式。做到占用的资源可控,保证了服务的可用性。但底层依然采用的同步阻塞模型,由此导致的进程阻塞或者服务超时、系统崩溃等都无法破解。

特点

  • 同步堵塞io模型
  • accept()获取连接阻塞,方法返回值-1=无数据,只能等着有数据返回,所以抛到新的一个线程处理,耗资源
  • client.read() 读取数据阻塞,方法返回值-1=无数据,只能等着有数据返回,
  • 系统调用次数多,应用程序循环fds 一次次的调用内核,效率极低

BIO创建连接慢原因

> 三次握手,所有io模型是必走的,
> server端, 主线程,accept(系统调用)发生阻塞(没有连接进入一直阻塞),有连接请求,发生clone (系统调用)每次开启一个子线程,主要慢在clone阶段

在这里插入图片描述

例子:使用bio方式创建5W连接 可以观察创建的速度。
客户端代码


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

/**
 * @author: di
 * @create: 2020-06-06 15:12
 */
public class C10Kclient {

    public static void main(String[] args) {
        LinkedList<SocketChannel> clients = new LinkedList<>();
        InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);

        for (int i = 10000; i < 65000; i++) {
            try {
                SocketChannel client1 = SocketChannel.open();
                client1.bind(new InetSocketAddress("192.168.150.1", i));
                //  192.168.150.1:10000   192.168.150.11:9090
                client1.connect(serverAddr);
                clients.add(client1);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("clients "+ clients.size());

        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketIO {

    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090,20);
        System.out.println("step1: new ServerSocket(9090) ");

        while (true) {
            Socket client = server.accept();  //阻塞1
            System.out.println("step2:client\t" + client.getPort());

            new Thread(() -> {
                InputStream in = null;
                try {
                    in = client.getInputStream();

                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    while(true){
                        String dataline = reader.readLine(); //阻塞2

                        if(null != dataline){
                        System.out.println(dataline);
                        }else{
                            client.close();
                            break;
                        }
                    }
                    System.out.println("客户端断开");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}


可以忽略这部分

/**
可以忽略这里,只是处理本地网卡和虚拟网卡网络互通问题 
window机器,本地会有一块网卡(例如ip:192.168.110.100),安装了vmware后,以后用一个虚拟网卡net8 
(例如,虚拟网络为:192.168.150.0,net网关服务为:192.168.150.2),在vm上安装虚拟机node1(ip为192.168.150.11),

创建11W 个连接,通过本地的两个ip(192.168.150.1、192.168.110.100)+port,循环5.5W次,
调用server端(192.168.150.11:9090)创建连接,需要增加一个route关系,不然client可以往虚拟机server发送,
server无法给192.169.110.100的发送ack,网络不通。

route -n:
*/

在这里插入图片描述


二、NIO

定义

  • NIO: 官方称为 New I/O,但称为非阻塞io,更能体现NIO的特点,同步非阻塞。
    包: java.nio.*

特点

  • 非阻塞 (可以设置阻塞或者非阻塞)
  • accept() 获取连接非阻塞。该方法立即返回,底层返回-1,方法返回null,无连接;有连接会返回连接 client
  • client.read(buffer),读取数据不阻塞,有数据返回正数,无数据返回非正数。
  • 可以在一个线程里面处理接收连接和读取数据,

在这里插入图片描述

缺点

  • 一个线程可以处理连接和数据读取等,假如10W连接,但是进行数据读取时,循环一次,无论有无数据都会进行10W次的系统调用,很多调用无意义,消耗很大。
  • read是没错的,但是无用无效的read 在被不断调用,用户态、内核态不断切换。
    在这里插入图片描述

NIO和传统IO(简称IO)之间最大的区别

  • IO是面向流的,Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方,操作简单,但慢;NIO是面向缓冲区的。 NIO 以块的方式处理数据,可以前后移动的操作数据,每一个操作都在一步中产生或者消费一个数据块,按块处理数据比按(流式的)字节处理数据要快得多,但缺少IO的优雅性和简单性。
  • IO同步阻塞;NIO同步非阻塞

例子:同nio方式创建5W连接,注意观察速度
客服端代码


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

/**
 * @author: di
 * @create: 2020-06-06 15:12
 */
public class C10Kclient {

    public static void main(String[] args) {
        LinkedList<SocketChannel> clients = new LinkedList<>();
        InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);

       
        for (int i = 10000; i < 65000; i++) {
            try {
                SocketChannel client1 = SocketChannel.open();
                client1.bind(new InetSocketAddress("192.168.150.1", i));
                //  192.168.150.1:10000   192.168.150.11:9090
                client1.connect(serverAddr);
                clients.add(client1);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("clients "+ clients.size());

        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端代码

import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

public class SocketNIO {

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

        LinkedList<SocketChannel> clients = new LinkedList<>();

        ServerSocketChannel ss = ServerSocketChannel.open();// 相当于fd
        ss.bind(new InetSocketAddress(9090));// 绑定端口
        ss.configureBlocking(false); //重点  OS  NONBLOCKING!!!

        ss.setOption(StandardSocketOptions.TCP_NODELAY, false);

		/**
			主线程里面负责接收连接及遍历连接获取数据
		*/

        while (true) {
            Thread.sleep(1000);
            //不会阻塞,返回  -1 =null 以为着无连接
            // NONBLOCKING 的时候,非阻塞,没有连接返回为null;设置Blocking 为阻塞 ,直到有连接进来,accept() 才往下走
            SocketChannel client = ss.accept();
           /** accept 调用了内核,
           	1、没有客户端连接进来,返回值? BIO 的时候,一直卡着,但在NIO不卡着,内部返回0,accept返回null。
			2、如果有客户端的连接,内部是返回的是客服端的fd  ,该方法client 为 object
			3、NONBLOCKING 就是代码能往下走了,只不过有不同的情况
			*/
            if (client == null) {
                System.out.println("null.....");
            } else {
                client.configureBlocking(false);// 重点 socket(服务端的listen socket<双方三次握手后,往这里扔,我去通过accept得到后面的连接socket>,,连接的socket<连接后的数据传递使用> ),指的下面的c.read(buffer) 读取不阻塞
                int port = client.socket().getPort();
                System.out.println("client...port: " + port);
                clients.add(client);
            }

           // ByteBuffer buffer = ByteBuffer.allocateDirect(4096);  //可以在堆里   堆外

            for (SocketChannel c : clients) {   //串行化!!!!  or 多线程!!
                int num = c.read(buffer);  // >0  -1  0   //不会阻塞,有无数都会返回,但会发生系统态切换,影响速度
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);

                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }
            }
        }
    }
}

错误

1、Exception in thread “main” java.io.IOException:Too many open file
ulimit -a : 可以查看open files 等全部配置
ulimit -n : 只查看 open files 限制大小 (一般用户是根据这个限制来的,root会突破限制),1024 一个进程最大可以打开的fd数量。
ulimit -sha 50000 : 把openfiles 设置为5W
netstat -natp: 网络连接状态


三、多路复用器

定义

多条路(下方图中多个io链路):通过一个系统调用(就是复用),把一批fd 传给kernel,内核获取fds其中的io状态,然后由程序自己对有状态的io进行读写。

多路复用器是一种规范,会有select、poll、epoll (三者都是同步非阻塞模型)实现。三种方式体现到java就是selector<多路开关选择器>,一个选择器能够管理多个信道上的I/O操作,

nio:直接采用和bio一样的获取连接方式accept(),
nio+多路复用:也可以通过将socketChannel 注册到selector 上的方式,使用selector 多路复用器获取有状态连接,

与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

在这里插入图片描述

**例子:**通过创建5W连接,可以看到速度明显快了很多。
客户端代码


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

/**
 * @author: di
 * @create: 2020-06-06 15:12
 */
public class C10Kclient {

    public static void main(String[] args) {
        LinkedList<SocketChannel> clients = new LinkedList<>();
        InetSocketAddress serverAddr = new InetSocketAddress("192.168.150.11", 9090);

       
        for (int i = 10000; i < 65000; i++) {
            try {
                SocketChannel client1 = SocketChannel.open();
                client1.bind(new InetSocketAddress("192.168.150.1", i));
                //  192.168.150.1:10000   192.168.150.11:9090
                client1.connect(serverAddr);
                clients.add(client1);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("clients "+ clients.size());

        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端代码


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;

public class SocketMultiplexingSingleThread {
    private ServerSocketChannel server = null;
    //linux 多路复用器(select>poll>epoll)
    private Selector selector = null;   
    int port = 9090;

    public void initServer() {
        try {
            server = ServerSocketChannel.open();//fd4
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));

            //如果在epoll模型下,open--》  epoll_create -> fd3
            //  select  poll  epoll  优先选择:epoll , 但是可以 -D修正指定使用哪个多路复用器
            selector = Selector.open();  

			//server 约等于 listen状态的fd4
            /*
            register
            如果:
            	select,poll:jvm里开辟一个数组 fd4 放进去
           	 	epoll:epoll_ctl(fd3,ADD,fd4,EPOLLIN) 红黑树
             */
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {  //死循环
                Set<SelectionKey> keys = selector.keys();
                System.out.println(keys.size()+"   size");

                //1,调用多路复用器(select,poll  or  epoll  (epoll_wait))
                /*
                select()是啥意思:
                1,select、poll :其实是内核的select(fd4)、 poll(fd4)
                2,epoll: 其实内核的epoll_wait()
                    参数可以带时间:没有时间,0  :  阻塞,有时间设置一个超时
                selector.wakeup()  结果返回0

                懒加载:
                其实在触碰到selector.select()调用的时候才触发了epoll_ctl的调用
                 */
                while (selector.select() > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();  //返回的有状态的fd集合
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    //管你啥多路复用器,你只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!
                    //  NIO 自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
                    //前边强调过,socket:  listen   通信 R/W
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove(); //set  不移除会重复循环处理
                        if (key.isAcceptable()) {
                            //看代码的时候,这里是重点,如果要去接受一个新的连接
                            //语义上,accept接受连接且返回新连接的FD对吧?
                            //那新的FD怎么办?
                            //select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
                            //epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);  //连read 还有 write都处理了
                            //在当前线程,这个方法可能会阻塞  ,如果阻塞了十年,其他的IO早就没电了。。。
                            //所以,为什么提出了 IO THREADS
                            //redis  是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
                            //tomcat 8,9  异步的处理方式  IO  和   处理上  解耦
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端  fd7
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);  //前边讲过了

            //你看,调用了register
            /*
            select,poll:jvm里开辟一个数组 fd7 放进去
            epoll:  epoll_ctl(fd3,ADD,fd7,EPOLLIN)
             */
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");

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

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();

        }
    }

    public static void main(String[] args) {
        SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
        service.start();
    }
}

附加----------------------------------------------------------------
IO分两阶段:

  • 1.数据准备阶段:数据从网卡、磁盘复制到内核缓存区
  • 2.内核空间复制到用户进程缓冲区、

阻塞、非阻塞、同步、异步 只关注io,不关注io读写之后的事情

  • 同步: 强调io的第二步,需要程序自己去完成R/W
  • 异步: 强调io的第二步,需要kerner自己去完成R/W,不依赖程序
  • 阻塞: 程序执行没有等到结果,一直等着
  • 非阻塞: 无论有无数据都直接返回,不会让当前线程一直等着。有数据返回数据,没数据返回空。
    在这里插入图片描述

可以参考此文:[深层次详解同步IO、异步IO、阻塞IO、非阻塞IO]

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值