美文网首页Linux
Socket网络编程:BIO,NIO,select,epoll

Socket网络编程:BIO,NIO,select,epoll

作者: leap_ | 来源:发表于2020-07-14 10:06 被阅读0次

    本文是观看了B站的马士兵的视频后的总结:
    清华大牛权威讲解nio,epoll,多路复用,更好的理解redis-netty-Kafka等热门技术
    和知乎的一篇文章:
    看不懂来砍我,epoll原理

    理解Socket基础1—计算机基础

    我们知道内存是被分为内核和用户两个部分的,内核用于运行操作系统和硬件相关的底层驱动,由于系统的保护机制,用户态的进程是无法直接访问硬件的,比如网络通信的硬件网卡;


    • 硬件设备接收事件(网卡接收数据帧,键盘接收输入等),当有了事件后,硬件层会产生一个中断,CPU会立刻停止当前的工作(比如当前正在执行用户进程)处理这个中断,处理的工作就是内核去实现,比如调用内核中对应硬件驱动的回调;

    • 用户态的进行想要访问硬件资源(和硬件交互)必须通过内核,内核会提供系统调用让用户态安全的访问计算机;

    拿Socket举例,网络数据通过物理网线传给网卡,此时网卡会产生一个中断,告诉CPU有网络数据进入电脑了,这时会将数据交给内核,具体放在哪我也没研究过,反正就是放在内核里面,用户态(Java)必须通过系统调用去拿到这个网络数据

    BIO

    传统的IO使用(伪代码)

            //  客户端
            Socket socket = new Socket("127.0.0.1",8090);
            socket.getOutputStream();
            socket.getInputStream();
    
    
            //  服务端
            ServerSocket serverSocket = new ServerSocket(8089);
            Socket socket = serverSocket.accept();
            socket.getOutputStream();
            socket.getInputStream();
    
    客户端:
    • 创建Socket对象,传入服务端对应的ip和端口,会自动连接
    • 获取IO流通信
    服务端:
    • 服务端创建ServerSocket对象,绑定ip和port
    • ServerSocket调用accept()监听客户端连接,练连接完成会返回客户端对应的Socket对象(这是一个阻塞方法,一般会在循环中开启线程去执行,即一个线程一个Socket连接)
    • 完事儿以后通过Socket获取IO流进行数据的读写
    这些是我们在java层做的事情,那么网络通信是如何发生的呢?

    首先java层是用户态的一个进程,他是无法直接读取网卡的数据的,必须通过系统调用到内核中去获取;系统调用是通过native层去实现的;


    BIO模型
    BIO存在的问题:
    • accept()和IO的读写是阻塞方法,必须开启多线程,每一个Socket连接建立一个线程
    • 很多Socket连接建立了并没有通信,会浪费大量的系统资源;
    NIO
    NIO模型

    为了解决线程浪费问题出现了NIO,将阻塞方法改为非阻塞方法,如果有连接,有数据,就去处理,没有的话继续执行下面,等待下次循环;

    NIO存在的问题:

    NIO虽然解决了线程浪费的问题,可是如果在大量网络请求的情况下,当前方案下的执行效率会变得非常的低,因为Java层的循环变得非常的长,并且每次循环都需要调用系统调用去询问内核这个请求有没有用,这个连接有没有数据,大量的无效的系统调用也会影响性能;

    Select:
    Select模型
    为了解决NIO在java层大量无效循环调用System call的情况,出现了一个select系统调用,Select的作用是将10000此循环全部通过一次SC交给内核,由内核去循环,判断哪些是有效的循环,比如100次有效循环,那么我的java就可以有目的性的去调用100次有效的SC去进行数据读写,Socket连接建立;
    select缺点:
    • 需要将连接一次性传递给内核
    • 虽然省去了大量的SC,但是内核需要去遍历循环,内核的内存压力会增大

    Epoll:

    等待队列红黑树

    Epoll将所有的Socket连接都在内核中保存了下来,就省去了Select一次性将所有的Socket连接发过来的这一步骤;

    就绪列表双向链表

    Select效率低的原因是因为需要遍历所有的连接才能知道哪个连接有数据,而epoll通过维护一个集合,存放所有的就绪连接,这样就避免了遍历的步骤;当有数据到达时,中断程序会产生一个中断将有数据的Socket添加到就绪列表;

    epoll将多路复用的实现拆分为三个步骤:
    • epoll_create:内核会产生一个epoll 实例数据结构并返回一个文件描述符,这个特殊的描述符就是epoll实例的句柄,后面的两个接口都以它为中心
    • epoll_ctl:维护等待队列将被监听的描述符添加到红黑树或从红黑树中删除,或者对监听事件进行修改
    • epoll_wait:阻塞进程,等待数据,程序执行到这一步时,如果就绪列表有数据,就直接返回,如果没有数据就会阻塞;

    NIO

    NonBlocking IO特点:

    • 非阻塞IO,没有数据时不会阻塞,而是返回0
    • 单线程处理多任务

    核心类:

    • channel
    • selector
    • buffer

    channel:

    channel通道类似流,既可以从流读取数据,也可以写入数据到流,流是单向的,通道是双向的;

    channel的实现:
    • FileChannel:从文件中读写数据,无法设置为非阻塞式
    • DataGramChannel:从UDP读写网络数据
    • SocketChannel:从TCP读写网络数据
    • ServerSocketChannel:监听新进来的TCP连接,每一个新的TCP连接都会创建一个新的SocketChannel

    buffer

    NIO buffer 提供了一组方法,用来访问缓冲区,对于缓冲区,本质上是一块可以写入数据,可以读取数据的内存;

    buffer的使用:

    1.channel写入数据到buffer
    2.调用buffer的flip()make buffer ready to read
    3. 从buffer中读取数据
    4.调用buffer的clear()`make buffer ready to write`

    buffer的工作原理:

    buffer的重要属性:capacity position limit

    • capacity:作为一个内存块,buffer有一个固定大小,capacity就是记录buffer的大小

    • position:当buffer写入的时候position从0开始,放入一个数据,position就后移一位;当buffer读取的时候,position从0开始,每读一个数据,后移一位;

    • limit:在写入的时候,limit同capacity,表示可以写入的大小;在读取时,表示当前可读取的数量;

    buffer的类型:
    • ByteBuffer:
    • CharBuffer:
    • DoubleBuffer:
    • FloatBuffer:
    • IntBuffer:
    • LongBuffer:
    • ShortBuffer:
    buffer的创建(分配):
        //  分配了48字节大小的字符Buffer
        CharBuffer charBuffer = CharBuffer.allocate(48);
    
    
    向buffer写入数据
            //  1 直接用 put() 写入
            charBuffer.put('1');
    
        //  2 channel写入到buffer
        channel.read(buffer);
    

    flip():将buffer从写模式转换成读模式

    从buffer读取数据
            // 1 直接使用 get() 读取
            char c = charBuffer.get();
    
        // 2 读取到channel中
        channel.write(buffer);
    
    • rewind():将position重新设置为0,可以再次读取buffer(limit保持不变)

    • clear():将buffer从读模式转为写模式,clear不会保存原来的数据,

    • compact():compact会将未读的数据拷贝到buffer的起始处,并且将position移到最后一个数后面

    • mark() & reset() :通过mark 记录position的值,再通过reset恢复到之前记录的position

    • equals() :比较buffer内的剩余元素,如果它们类型相等,数量相等,元素值相等,那么两个buffer 就相等

    • compareTo() :比较元素的数量和元素值的大小;

    分散和聚集(Scatter/Gather):

    • 分散:将channel的数据分散读取到多个buffer中
      scatter read
            //  分散 , 一个channel的数据读取到多个buffer
            ByteBuffer head = ByteBuffer.allocate(20);
            ByteBuffer body = ByteBuffer.allocate(480);
            ByteBuffer[] buffers = {head,body};
            try {
                // 从channel读取数据
                channel.read(buffers);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
    
    • 聚集:将多个buffer数据聚集写入到一个channel中
      gather write
            //  聚集 , 多个buffer数据写入channel
            ByteBuffer head = ByteBuffer.allocate(20);
            ByteBuffer body = ByteBuffer.allocate(480);
            ByteBuffer[] buffers = {head,body};
            try {
                // 写入数据到channel
                channel.write(buffers);
            } catch (IOException e) {
                e.printStackTrace();
            }
    

    Selector

    选择器,用于实现单线程管理多个channel,即管理多个网络连接

    1. selector的创建:
           try {
               Selector selector = Selector.open();
           } catch (IOException e) {
               e.printStackTrace();
           }
    
    2. 向selector中注册channel
                //  将channel设置为非阻塞式
                socketChannel.configureBlocking(false);
                //  注册到selector上
                SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
    

    注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false); 因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的
    register()第二个参数用于指定selector对channel的什么事件感兴趣,常见的事件有:

    • SelectionKey.OP_ACCEPT:确认事件
    • SelectionKey.OP_CONNECT:连接事件,TCP连接
    • SelectionKey.OP_READ:读出事件
    • SelectionKey.OP_WRITE:写入事件
    SelectionKey:

    每次向Selector中注册一个channel都会拿到一个SelectionKey对象;通过selectionKey对绑定事件进行控制,SelectionKey重要的成员变量:

    • interest Set:感兴趣事件的集合
    • ready Set:已准备就绪的操作的集合
    • Channel:
    • Selector:
    • 附加对象:
                // 获取 channel
                key.channel();
                //  获取 selector
                key.selector();
                //  获取 感兴趣的事件
                key.interestOps();
                //  附加对象
                key.attach(new Object());
    

    Selector.select():

    调用该方法后会阻塞,知道被注册的channel有事件出现,或者出现新的channel注册事件


    selector 的工作流程
                Set keySet = selector.selectedKeys();
                Iterator iterator = keySet.iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = (SelectionKey) iterator.next();
                    // TODO: 通过 selectionKey 获取channel 处理事件
                    iterator.remove();  // 删除当前元素(key)
                }
    

    相关文章

      网友评论

        本文标题:Socket网络编程:BIO,NIO,select,epoll

        本文链接:https://www.haomeiwen.com/subject/cwtecktx.html