这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战
Java 网络IO API
Java 网络IO相关的API有三类:NIO、BIO、AIO、IO Multiplexing。
那么对应的IO如何使用到呢?
BIO
阻塞IO,在调用read和write时均会阻塞。read通常比较容易理解,当socket接受到数据read就会返回。那么write呢?write会阻塞我一开始也是迷迷糊糊,知道后来学到了 socket buffer这个数据结构才略懂一点,write API会将传入的byte 写到socket文件对应socket buffer中,这个数据结构类似一个队列或者链表,并且是有限的,当socket buffer容量不足时write API就会阻塞,知道有空闲空间。
在Java中,可以使用JDK提供的API使用BIO。下面是一个bio便携的client、server交互的例子。
1 | java复制代码public class Server { |
NIO
BIO满足了应用层进行通信的基本需求,但是如果一直在accept、read、write这种地方干等着,CPU一直不被使用,岂不是很浪费。所以NIO出现了,NIO称作NO Block IO,其特点在于提供了非阻塞的accept,read,write。相当于Java中Lock 的tryLoack。当tryAccept返回失败,则表示没有socket建立连接,此时可以去干点别的。
但其实这个效果非常的鸡肋,假若你去干其它事情的时候,有client建立连接,你就无法及时响应,java中通过NIO java.nio.channels.spi.AbstractSelectableChannel#configureBlocking
可以设置非阻塞模式,但是这个API只支持NIO的socket设置。
IO Multiplexing
BIO满足了应用层传输数据的基本需求,但在实际使用中,client一定是多个,并且还可能同时与一个server进行数据传输,所以在server端需要使用多线程的方式来接收客户端的请求。所以请求的并发数量会与机器的线程数量成正比,对外服务的性能也会受到操作系统分配给进程的最大线程数的影响。因为每个线程都需要阻塞到read、write操作,完全不能干点别的了。
操作系统的网络协议栈的开发者也觉得这个操作非常拉胯,如同LOL王者辅助带一个青铜ADC,任由你机器配置在牛,性能还是不行,熟话说好马配好鞍,经过操作系统开发者的反复努力,开发出了一个API,我们大众程序员调用这个API可以直接获取到哪些socket可以read,哪些socket可以write,哪些socket需要建立连接,这个直接获取的机制,称之为IO多路复用,其最大的意义在于单线程可以处理多个socket,IO多路复用的具体实现依赖于底层的操作系统,不同实现有:
- select
- poll
- epoll
java中可以如下使用select实现的IO多路复用:
1 | java复制代码public class NIOClient { |
JAVA NIO API相关资料:tutorials.jenkov.com/java-nio/in…
AIO
IO Multiplexing 以及非常牛皮了,但在实际使用中,感觉还是不够方便也不够快捷。因为作为开发者的我还需要主动查询哪些socket可以read、可以write、可以accept。
能不能更方便点,我事先给你个回调函数,操作系统你接收到cliet的数据久直接调用我这个函数,这样不是更方便,我什么也不用等,岂不是非常舒服呀呀呀呀!
贴心的操作系统开发者看到这个需要给你竖起了大拇指,不愧是996的程序员真有想法,给你扣波“666”。
以下为在Java中使用AIO,可以看到使用起来十分复杂,并且性能与IO Multiplesing差不并不是特别大,所以通常开发很少用AIO。
server.java
1 | java复制代码package com.github.jiangxch.jdk.demo.netio.aio; |
client.java
1 | java复制代码package com.github.jiangxch.jdk.demo.netio.aio; |
AIO与NIO相比,除了上面使用不同之外,异步回调的方式也不会因为系统调用而产生更多的上下文切换和数据拷贝。
操作系统网络 IO
如果仅仅只是开发功能,上面的APi基本足够了。但是对于面试、优化来说,还需要更深入一层理解操作系统如何实现IO多路复用。Linux中提供到了以下API接口。
select
Linux中select函数定义:
1 | c复制代码#include <sys/select.h> |
- timeout参数
timeout是一个timeval的结构体,其数据结构定义:
1 | c复制代码struct { |
fd_set数据结构定义:
1 | c复制代码typedef struct fd_set { |
fdset是一个数组,类型是32位的int类型。一个int类型可以表示32个文件描述符。可以通过判断int对应的位是0?or1?来判断是否可读可写。
该参数表示等待多久如果还无事件就绪,则直接返回。
传null,表示无超时设置,会永远等待直到事件就绪。
传0表示不等待,会立即返回。
- excepset、writeset、readset
这三个参数代表要监听的条件,分别是异常事件就绪,写事件就绪,读事件就绪。 - maxfdpl
该参数表示最大文件描述符的数量。
那么seelct是怎么工作的呢?
比如我们编写了一个NIO通信的java进程,就会在操作系统的内存地址中创建一个Process的数据结构,该数据结构会存储进程的pid,以及进程打开的文件对应的文件描述符,进程运行后,当进程接收到连接建立会创建一个套接字,Linux中一切皆文件,套接字也会被存放到Process,当调用select函数对套接字进行监听时,内核就会遍历这些套接字文件描述符对应的文件,在使用文件相关的api判断socket对应的文件是否可读可写,如果对应事件就绪,就会修改传入的event参数的对应bit位最后返回就绪的套接字给调用方。
那么对于一个socket套接字来说,什么情况下称作可读,什么情况称为可写呢?我们知道对于一个套接字,有读写两个缓冲区。在select函数中,定义了读的低水位,当读缓冲区的数据大于低水位,才会出发套接字的读就绪事件。写就绪也相同,当写缓冲区空闲大小大于写缓冲区的低水位才会触发。
所以并不是网卡家接收1byte就触发,接收1byte就触发一次。
poll
poll的功能与select类似。他俩的区别在于select出现的早,文件描述的最大限制是在代码中定义了一个常量表示=1024,poll则没有这个限制,poll能使用的最大文件描述仅收操作系统的限制。
epoll
epoll是对select的一个优化,我们来看下seelct的操作步骤:
1 | c复制代码while(1) { |
根据以上为代码,我们捋一下整个操作的性能开销:
- select是系统调用,系统调用一定伴随着上下文切换和内存拷贝,频繁select会使得CPU过高。
- select函数是通过遍历传入的所有描述符对应的文件来判断是否有事件就绪,因此如果传入的文件描述符过多,会使得扫描耗时更久。
- select调用时参数和返回值混在了一起,需要我们调用方再次遍历进行判断。
epoll主要是对上面三个情况进行了优化。epoll的API定义如下:
1 | c复制代码int epoll_create(int size); |
1 | csharp复制代码int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
1 | arduino复制代码int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
epoll使用分为了三个函数,epoll_create只需调用一次,用于创建epoll的文件描述符,size参数只需大于0即可,最开始size表示初始化文件描述符的数量,若不够就会进行扩容。e poll_ctl函数则是向内核中的epoll实例添加、修改、删除对某个文件描述符上的事件监听。
op表示对于的操作,可以传入EPOLL_CTM_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。fd参数表示要监听的文件描述符。event参数结构体定义:
1 | c复制代码struct epoll_event { |
结构体中 events可以是以下几个宏的集合:
- EPOLLIN: 监听读
- EPOLLOUT:监听写
- EPOLLERR: 监听操作
- EPOLLPRI:监听外来数据
- EPOLLHUP:描述符被挂断
- EPOLLET:设置触发方法,边缘触发,水平触发
- EPOLLONESHOT:仅监听一次,设置该参数如果需要继续监听需要重新加入到epoll的监听事件队列中
epoll_wait方法的events参数用于接收返回值,会返回就绪的事件。
那么epoll相对selec,它快在哪里呢?
仅返回就绪的文件描述符
select在返回返回值时,会将所有描述符(不论就绪还是未就绪的)进行返回,而epoll只会返回就绪的文件描述符,以及拷贝就绪socket的到用户内存。
更高效的查找就绪事件的方式
select需要遍历传入的所有的文件描述符,找到描述符在操作系统中对应的文件,逐个判断文件是否满足设置的条件,进而触发对应的事件。所以select会随着文件描述符数量的增长性能降低,而epoll则不会。
操作系统会注册一个中断函数,当网卡接收到数据会出发这个中断函数,如果接收到的数据是需要监听的文件描述符,就会直接将该描述符放入就绪列表。当调用epoll_wait时,直接返回该列表。
除此之外,epoll还有比较高级特性就是工作方式。
- LT:水平触发,当如果设置事件为水平触发,则调用epoll_wait时会判断事件是否是水平触发,如果是水平触发并且该事件未经过用户程序处理,则会重新放入就绪列表中。
- ET:边缘触发,当epoll_wait返回后,必须立即处理就绪事件。
那epoll为什么要设计LT与ET呢?为了面试,此处说一下自己的理解吧:
最开始之初,epoll只有LT的触发模式,水平触发对应到代码中,如果你监听某个socket读数据,当读队列一直有数据时都会触发事件,这种方式会导致一直有事件触发,从而到导致频繁的系统调用。而如果设置了ET,只有当读缓冲区接收到数据时才会触发(从空-》有数据),会降低系统调用。
参考文章
cloud.tencent.com/developer/a…
本文转载自: 掘金