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

本文是观看了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)