IO - 网络通信IO演变过程

91 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情

网络通信IO演变过程

BIO

image-20210216162912608

服务端serverSocket指定一个端口,做了三件事

  • 调用内核socket指令,返回一个文件描述符 : 3
  • 调用内核bind指令,绑定文件描述符和端口
  • 调用内核listen指令,监听返回的文件描述符

进入while(true)死循环,接收客户端请求

  • 调用内核accept指令,传入socket返回的文件描述符和连接信息。此时没有客户端连接,会产生阻塞
  • 当客户端连接后,accept通过,返回一个文件描述符 :5

得到客户端的文件描述符后,调用内核clone指令,创建一个线程,在线程中执行读写,当客户端没有发送数据时,recv指令阻塞

BIO

每个线程,是每一个连接

优势:可以接收很多的连接

劣势:线程内存浪费。CPU调度损耗

根源:阻塞(BLOCKING)。accept,recv指令阻塞

解决方案:非阻塞(NONBLOCKING)

BIO受阻于内核,内核指令不支持非阻塞

NIO

内核的发展,支持非阻塞

优势:避免多线程的问题

劣势:假设1万个连接,只有一个发来数据,每循环一次,必须向内核发送1万次recv的系统调用(产生软中断),那么就有9999次是无意义的,浪费时间和资源。

​ 用户空间向内核空间的循环遍历,复杂度在系统调用上

​ 解决方案:==多路复用== - selector - 基于内核的发展

C10K问题?

在内核指令socket中有一个参数:

image-20210216163157776

Java中的NIO:new IO

java的nio包,提供了新的接口

Java的NIO包括Channel,Buffer,Selector(内核中的多路复用可能是select,poll,epoll,kqueue)

代码实现

package org.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SocketNIO {

    private ServerSocketChannel server = null;
    private Selector selector = null;// 多路复用(可能是select,poll,epoll,kqueue)
    int port = 9090;

    public void initServer(){
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            selector = Selector.open();//epoll下,open()相当于epoll_create  =>fd3

            // 此时server已经执行了 listen指令,文件操作符 fd4

            /*
                如果是:
                    select,poll:就在jvm中开辟一个数组,将fd4放进去
                    epoll:      执行epoll_ctl(fd3,ADD,fd4,EPOLLIN)
             */
            server.register(selector, SelectionKey.OP_ACCEPT);

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

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

                /*
                    select()啥意思?:
                        1。select,poll:就是调用内核指令select(fd4)或者poll(fd3),将所有的文件操作符拷贝到内核进行遍历,拿到所有的有状态的文件操作符
                        2。epoll       :就是调用内核epoll_wait(),拿到所有有状态的文件操作符
                 */
                while (selector.select(500) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {

                        } else if (key.isWritable()) {

                        }

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

    /**
     * 创建连接
     * @param key
     */
    private void acceptHandler(SelectionKey key) throws IOException {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel client = ssc.accept();// 接收连接
        client.configureBlocking(false);// 非阻塞
        ByteBuffer buffer = ByteBuffer.allocate(8192);

        /*
            这里将接收的连接,注册到selector中
               select,poll:就是在jvm的数组中,放入了一个 fdx 文件操作符
               epoll:执行epoll_ctl(fd3,ADD,fdx,EPOLLIN)
         */
        client.register(selector, SelectionKey.OP_READ, buffer);
        System.out.println("-----------------------------------");
        System.out.println("新客户端:" + client.getRemoteAddress());
        System.out.println("-----------------------------------");
    }


    public static void main(String[] args) {

    }
}

操作系统的NIO : NONBLOCK

image-20210216163157776

解决无效的软中断 - 多路复用(内核层面)

假设1万个连接,只有一个发来数据,每循环一次,必须向内核发送1万次recv的系统调用(产生软中断),那么就有9999次是无意义的,浪费时间和资源。

​ 用户空间向内核空间的循环遍历,复杂度在系统调用上

多路复用 - selector - 基于内核的发展,还有poll,epoll。都是同步的

多路复用器:只是拿到了状态

select

select只能接受1024个客户端连接

只需要拿到可以读的 文件描述符,遍历可读的,进行读取即可

image-20210216185425923

epoll

JVM在Linux上启动默认使用epoll

epoll的大概逻辑图

相比如poll在内核中多了两块空间,用于存放连接的文件操作符 和 有状态的文件操作符

如果计算机有多核CPU,那么有一个CPU存放所有的文件操作符

  • CPU1用来处理网卡到来的连接,数据,将文件操作符添加到第一个空间
  • CPU2用来处理上层应用调用拿到所有有状态的文件操作符

所以CPU1和CPU2就是并行异步的

image-20210216192852285

为了解决select每次都要把所有的文件描述符dfs拷贝到内核进行循环遍历,epoll在内核中开辟了2个空间

  • 一个用来存放文件描述符 :一创建连接,就把文件描述符放到该空间中,只放一次
  • 一个用来存放哪些文件描述符有状态 :当该连接有状态变化,就会把改文件描述符拷贝到该空间中,用于返回给APP程序

如何把第一个空间的文件描述符复制到返回区,暂不了解,太底层了

epoll指令,可以通过Linux命令man 2 epoll查看

有3个指令:参考上图

  • epoll_create():创建第一个空间的指令,会返回一个epoll的文件描述符,比方说7

  • epoll_ctl():当有一个连接创建,执行该指令,放入到第一个空间中epoll_ctl(7,ADD,3,accept)

    • 7 - epoll创建的空间的文件描述符
    • ADD - 表示新增一个连接的文件描述符
    • 3 - 表示新连接的文件描述符
    • accept - 事件(可连接,可读,可写)

    如果一个连接被创建了,连上后accept返回的文件描述符是8,那么就要监听他的可读时间,那么就要调用epoll_ctl(7,ADD,8,read):在fd7对应的空间,添加一个fd8,事件是:读事件

  • epoll_wait():阻塞的,但是可以设置阻塞时间(timeout)。用于将有状态的文件操作符返给上层应用

    O(1)复杂度

这三个指令都是内核提供给上层应用调用的

查看java线程情况

image-20210216170744937

java:执行java的指令

SocketNIO:执行的Java文件