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)
            }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351