Linux

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

2020-07-14  本文已影响0人  leap_

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

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

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


拿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();
客户端:
服务端:
这些是我们在java层做的事情,那么网络通信是如何发生的呢?

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


BIO模型
BIO存在的问题:
NIO
NIO模型

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

NIO存在的问题:

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

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

Epoll:

等待队列红黑树

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

就绪列表双向链表

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

epoll将多路复用的实现拆分为三个步骤:

NIO

NonBlocking IO特点:

核心类:

channel:

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

channel的实现:

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

buffer的类型:
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);

分散和聚集(Scatter/Gather):

        //  分散 , 一个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
        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:

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

            // 获取 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)
            }
上一篇下一篇

猜你喜欢

热点阅读