当前位置: 移动技术网 > IT编程>开发语言>Java > 大话IO

大话IO

2020年07月17日  | 移动技术网IT编程  | 我要评论

1. 基础知识

1.1 一次网络IO

图片

1.1.1 网络IO流程

  • 用户发送数据到服务器【网卡
  • 网卡使用DMA方式将数据拷贝到内核的Socket缓冲区的RevBuffer 【内核Socket缓冲区
  • 应用程序读数据时,将内核Socket缓冲区中的数据拷贝到当前线程的缓冲区里【用户缓冲区
  • 应用程序执行相应的业务处理,如果要操作磁盘文件,需要切换到内核态,调用系统函数,将磁盘上的数据拷贝到内核缓冲区中,然后再从内核缓冲区拷贝到用户缓冲区中。
  • 应用程序写数据时,先将用户缓冲区里的数据拷贝到内核的Socket缓冲区的sendBuffer中
  • 内核程序再通过DMA方式将数据拷贝到网卡上

1.1.2 什么是DMA?

  • DMA: Direct Memory Acces的缩写,直接内存存取的意思。
  • 一种允许【硬件直接访问内存】的机制
  • 基于DMA访问方式,可以【省去CPU调度

1.1.3 CPU如何知道接收数据?

  • 问题:数据从网卡到内核Socket缓冲区中采用DMA方式,那么CPU是如何感知的?
  • 解析:中断, CPU实现线程切换就是基于时钟中断。中断可以分为多种:①外部中断,计算机外设发出的中断,比如:键盘、鼠标、网卡 ②内部中断,计算机内部产生问题,比如:运算出错(除数为零)。 ③软中断:用户程序主动调用中断指令。 当DMA拷贝完数据,就会给CPU发送一个外部中断,这样CPU就能感知到数据已经准备好了,执行相应的处理逻辑。

1.2 CPU调度

1.2.1 阻塞为什么不消耗CPU?

1.2.1.1 工作队列

  • 操作系统为了实现进程调度,维护了一个【工作队列
  • 工作队列中存放的是【运行态】的进程
  • 操作系统会采用时间片的方式执行运行态的进程

1.2.1.2 等待队列

  • 客户端连接服务器后,服务器会为其创建一个Socket对象,也就是对应的文件描述符【Socket文件描述符】。
  • Socket文件描述符中包含【发送缓冲区】、【接收缓冲区】、【等待队列
  • 如果客户端还未发送数据,读取Socket数据时就会阻塞。此时会将当前线程从工作队列移出到SocketFD的等待队列中。这样,阻塞的线程就不会消费CPU资源。
  • 当Socket接收到数据后,DMA会发送个中断请求给CPU,CPU就会执行中断程序,将Socket等待队列中的线程移出到工作队列中,此时线程再继续执行代码,就会获取到Socket中的数据了。

2.BIO

2.1 简介下什么是BIO?

图片

  • BIO中的B指的是”Blocking“的意思,即”阻塞的IO"
  • 我们开发服务端时,我们先使用ServerSocket绑定要监听端口,然后调用accept方法获取一个连接,该方法底层调用了内核的recvFrom函数。如果客户端此时还未连接,该方法会阻塞。
  • 当我们通过accept方法获取到Socket后,就可以获取这个Socket的输入输出流,对应就是read和write方法。但是如果客户端没有输入数据,该方法会阻塞,此时其他客户端连接服务器也会阻塞,服务器无法及时响应。
  • 为了解决BIO阻塞的问题,我们一般采用多线程的模式,为每个请求建立一个线程来处理。即BIO是【线程驱动模型
  • BIO最大的问题是:线程开销大,很容易导致系统故障。

2.2 什么是C10K问题?

  • 我们每接到一个客户端请求,服务器就需要分配一个进程来处理这个请求。假如有10K个请求,我们就需要创建1万个进程,显然,我们单机系统是承受不住的。
  • 当我们的线程多了,线程上下文切换开销会增大,导致系统崩溃。
  • 解决思路:一个线程负责多个连接,即IO多路复用。

2.3 BIO代码示范

  • 服务端代码
    static int port = 8080;
    public static void main(String[] args) {
        //1.建立服务器对象
        ServerSocket serverSocket = null;
        //2.建立客户端对象
        Socket socket = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            //3.监听指定端口
            serverSocket = new ServerSocket(port);
            System.out.println("start  server socket....");
            while (true){
                //4.获取连接,该方法会阻塞
                socket = serverSocket.accept();
                System.out.println("获取到socket连接...");
                //5.读取数据
                inputStream = socket.getInputStream();
                byte[] buffer = new byte[1024];
                int length = 0;
                //read方法会阻塞
                while ((length = inputStream.read(buffer)) > 0) {
                    String msg = new String(buffer, 0, length);
                    System.out.println("接收到客户端数据:" + msg);
                    //6.发送回执
                    outputStream = socket.getOutputStream();
                    outputStream.write("server get data".getBytes());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocket.close();
                socket.close();
                inputStream.close();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  • 客户端测试
telnet localhost 8080

2.4 BIO存在的问题

  • C10K问题:由于read方法会阻塞当前线程,因此我们一般会为每个客户端创建一个线程。如果我们的客户端请求很多,我们服务器就会因为创建多个线程而OOM,或者直接系统崩溃。虽然我们可以使用线程池来管理线程,但是如果客户端访问量很大,那么用户体验会非常差,线程池满了后,后续的请求就无法处理。这就是C10K问题。
  • 如果要解决C10K问题,我们就需要引入:NIO
  1. NIO

3.1 简介下什么是NIO?

  • Java中的NIO指的是NewIO,给我们提供了一套非阻塞的接口,底层通过JVM虚拟机调用操作系统kernel去实现。.
  • NIO三大核心组件:Buffer(缓冲区)、Selector(选择器)、Channel(通道)
  • 操作系统的NIO指的是Not Blocking IO,也就是非阻塞IO.NIO不会为每个请求创建一个线程,而是一个线程取监听多个Socket请求。
  • 操作流程: NIO包中提供了一个selector,我们需要先打开服务器端的管道,然后绑定端口,通过open方法找到Selector,再把需要检查的Socket注册到这个selector中。主线程会阻塞在selctor的select方法里,然后当select发现某个socket就绪了,就会唤醒主线程。然后主线程就可以通过selector获取到就绪状态的Socket,进行相应的处理。
  • NIO是基于事件驱动模型

3.2 NIO核心

3.2.1 通道

  • 通道-channel,表示打开到IO设备的连接,比如:Socket连接

3.2.2 缓冲区

  • 缓冲区-buffer,数据容器,用于存储数据。

3.2.2.1 Buffer简介

图片

  • buffer主要负责存储数据,底层实现是数组。
  • 根据数据类型不同,可以分为不同类型的缓冲区(boolean除外),ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
  • buffer四大属性: ①capacity:容量,表示缓冲区最大容量,一旦声明就不可改变。 ②limit:界限,表示可操作数据大小 ③position:位置,表示正在操作数据的位置 ④mark:存档,记录当前操作的位置,通过reset恢复到mark的位置 ⑤0<=mark <= postion <=limt <=capacity
  • buffer核心方法:①allocate:分配缓冲区 ②put:存储数据到缓冲区 ③get:获取缓冲区中的数据 ④flip:切换到写模式
/**
 * @Author pei.sun
 * @Date 2020/7/11 23:24
 * @Description 1.buffer-负责存储数据,可以存储不同类型的数据。底层是数组。
 * 根据数据类型不同,提供了相应的类型的缓冲区(除了boolean)
 * ByteBuffer、CharBuffer、ShortBuffer、IntBuffer
 * LongBuffer、FloatBuffer、DoubleBuffer
 * 2.缓冲区核心方法
 * allocate:分配缓冲区
 * put:存储数据到缓冲区
 * get:获取缓冲区中的数据
 * 3.缓冲区四大核心属性
 * capacity:容量,表示缓冲区中最大存储数据容量,一旦声明就不能改变。
 * limit:界限,表示缓冲区中可以操作数据大小。limit后的数据不能操作.
 * position:位置,表示正在操作数据的位置
 * mark:标记,记录当前position位置,通过reset()恢复到mark的位置
 * 0<=mark <=position<= limit <= capacity
 */
public class TestBuffer {
    public static void main(String[] args) {
        System.out.println("=======allocate()=======");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //写数据
        System.out.println("=======put()=======");
        String msg = "newio";
        buf.put(msg.getBytes());
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //flip 切换读写模式
        System.out.println("=======flip()=======");
        buf.flip();
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        System.out.println("=======get()=======");
        byte[] dst = new byte[buf.limit()];
        buf.get(dst);
        System.out.println("data:" + new String(dst, 0, dst.length));
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //rewind: 重复读
        System.out.println("=======rewind()=======");
        buf.rewind();
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //clear:清空缓冲区,实际只改变了指针,实际数据还存在,只是处于“被遗忘状态”
        System.out.println("=======clear()=======");
        buf.clear();
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //mark:存档
        System.out.println("=======mark()=======");
        buf.put(msg.getBytes());
        buf.flip();
        byte[] result = new byte[buf.limit()];
        buf.get(result, 0, 2);
        System.out.println("result:" + new String(result, 0, 2));
        System.out.println("position:" + buf.position());
        buf.mark();
        buf.get(result, 2, 2);
        System.out.println("result:" + new String(result, 2, 2));
        System.out.println("position:" + buf.position());
        System.out.println("=======reset()=======");
        buf.reset();
        System.out.println("position:" + buf.position());
        //缓冲区中是否有剩余数据
        System.out.println("=======remaining()=======");
        if (buf.hasRemaining()) {
            System.out.println("remaining:"+buf.remaining());
        }
    }
}

3.2.2.2 缓冲区分类

图片

  • 缓冲区可以分为【直接缓冲区】和【非直接缓冲区
  • 使用【allocate】方法创建的是非直接缓冲区,使用【allocateDirect】方法创建的是直接缓冲区
  • 非直接缓冲区分配在【JVM堆】内存中,直接缓冲区分配在【物理内存】中
  • 直接缓冲区底层实现了【零拷贝
  • 直接缓冲区缺陷: ①开销大 ②不归JVM管

3.2 如何提高NIO性能?

3.2.1 reactor

3.3 NIO与IO的区别

  • BIO是面向流的,NIO是面向缓冲区的。

4. IO多路复用

1.4.1 select函数

1.4.1.1 简介下select函数的工作原理?

  • 我们每次调用kenel的select函数,都需要切换用户态和内核态,还要把需要检查的Socket集合传过去,其实就是Socket的文件描述符。
  • Linux系统中,一切皆文件,操作系统会为每个socket都生成一个文件描述符。
  • 操作系统会根据传过去的Socket集合,去检查内存中Socket套接字的状态,这个复杂度是O(N)级别的,检查一遍后,如果有就绪状态的Socket,会为socket对应文件描述符打上一个标记,表示这个socket就绪了,然后返回就绪Socket数量。否则会阻塞调用线程,直到有某个Socket有数据后,才会唤醒调用线程
  • 调用者

1.4.1.2 select函数监听socket时,socket有没有数量限制?

  • select函数默认最大可以监听1024个socket,实际肯定是比这个数小的。
  • 由于select函数有个参数,是传进来的socket集合。这个集合长度是1024,如果需要这个长度,比较麻烦,需要重新编译操作系统内核。
  • 这个长度设置为1024,应该是处于性能考虑

1.4.1.3 第一次查找未发现就绪Socket,后续Socket就绪后,select如何感知,是轮询吗?

  • 知识铺垫: ①操作系统调度:一个CPU在同一时刻只能运行一个进程,CPU会在不同进程间来回切换。没有挂起的进程都在工作队列里,是有机会获取到CPU执行权。挂起的进程,会从工作队列内移除出去,就是阻塞了,没有机会去获取CPU执行权。 ②操作系统中断: 首先介绍下时钟中断,这个底层是借助晶振实现的,CPU每收到一个时钟中断就会切换下进程。然后是IO中断,比如:我们使用键盘打字,如果CPU正执行其他程序一直不释放,那么我们就无法打字,事实肯定不是这样的。我们摁下键时,会产生一个IO中断,触发CPU中断,然后CPU会保存现在执行线程的信息,然后处理紧急需求。这样中断程序就拿到了CPU执行权。
  • select第一次轮询,如果没有发现就绪状态的Socket,就把这个进程保存到socket的等待队列中,然后会把这个当前进程从工作队列移除了,然后就阻塞了。select函数也不会执行了。
  • 客户端发送数据,通过网卡到达内存中,整个过程CPU不参与,自然也无法感知。数据完成传输后,会触发网络中断,CPU就会处理这个中断程序,然后根据数据包中的端口信息,分析出来是哪个Socket的数据,然后把数据复制到Socket的读缓冲区里。数据导入完成后,就会检查Socket的等待队列,看是不是有等待者,如果有,就把等待着移动到工作队列中,然后select函数就会再执行了,然后再次检查,就发现有就绪的socket了,然后就会返回就绪数量给Java客户端。
  • 由于select函数返回的是就绪的数量,客户端没法获取就绪进程信息,因此还要再次遍历,检查socket是否就绪,如果就绪就处理。

1.4.2 poll

1.4.2.1 poll和select的区别是啥?

  • 参数不一致,select使用的是bitmap表示需要检查的socket集合,poll使用数组表示需要检查的Socket集合,这样就解决了select最多只能监听1024个socket缺陷。

1.4.2.2 select 和 poll的缺陷是啥?

  • select和poll的返回值是个整型值,表示有几个socket就绪了,无法表达具体是哪个socket就绪了。程序被唤醒后,要新的一轮系统调用去检查哪个socket是就绪状态。系统调用设计用户态和内核态切换。

1.4.3 epoll

1.4.3.1 为什么会有poll?

1.4.3.1 epoll的工作原理是啥?

  • epoll中有自己的数据结构是eventpoll对象,可以通过系统函数epoll_create去创建,创建返程回,返回这个对象的文件号。
  • Socket对象有三块区域,读缓冲区、写缓冲区和等待队列。
  • EventPoll对象有两个核心区域,一块用于存放需要监听的socket文件描述符列表,另一块存放就绪状态的Socket信息。EventPoll对象有两个核心函数,epoll_ctl和epoll_wait。epoll_ctl用于维护需要关注的Socket文件描述符列表,

本文地址:https://blog.csdn.net/sunpeiv/article/details/107371575

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网