37

IO与NIO的知识理解

 4 years ago
source link: https://www.tuicool.com/articles/YrYrMfV
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、IO  同步阻塞

1.什么是IO?

IO是指 Input 和Output,是针对内存来讲,IO又分为磁盘IO和网络IO。可以根据下图了解

FfiQFvb.jpg!web

自网络兴起之后网络IO变为流行起来,但使用此方式解决网络IO问题性能不佳,所以对IO进行了演

进。解决阻塞问题,于是把IO叫Block IO也称BIO。

2.为什么我们经常写的IO叫同步阻塞IO?

read操作完成后才可以write操作,或write操作完后才可以再次read。总结:在处理任务时,同一时间点只能处理一个任务所以被同步了,在处理任务时只能处理一个线程,所以被阻塞了。可以根据下图了解

INjaQvJ.jpg!web

3.示例代码

server端

public class BIOServer {

private ServerSocket server;


public BIOServer(int port) {

try {

server = new ServerSocket(port);

System.out.println("BIOServer 启动端口:" + port);

} catch (IOException e) {

e.printStackTrace();

}

}


public void listen() {

while (true) {

try (Socket client = server.accept()) {

InputStream is = client.getInputStream();

byte[] buff = new byte[128];

int len = is.read(buff);

if (len > 0) {

String msg = new String(buff, 0, len);

System.out.println("接收消息:" + msg);

}

is.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}


public static void main(String[] args) {

new BIOServer(8888).listen();

}

}

client端

public class BIOClient {


private String host;

private int port;


public BIOClient(String host, int port) {

this.host = host;

this.port = port;

}


public void run() {

try (Socket client = new Socket(host, port);

OutputStream os = client.getOutputStream()) {

String content = UUID.randomUUID().toString();

System.out.println("发送数据:" + content);

os.write(content.getBytes());

} catch (IOException e) {

e.printStackTrace();

}

}


public static void main(String[] args) {

new BIOClient("127.0.0.1", 8887).run();

}

}


二、NIO 同步非阻塞

1.什么是NIO?

NIO是在BIO基础上作了升级,采用通道和缓冲区的形式进行处理数据,解决了多路非阻塞式的网络IO。

ZBzYZjr.jpg!web

2.什么是通道?

通道(Channel)是面向缓冲区的,可以通过它读取和写入操作。通道操作的数据是由Buffer来处理,而不是通道直接操作数据。当写入数据时,通道把数据写入缓冲区。当读取数据时,是通道把数据读到缓冲区,在由缓冲区读取数据。

通道操作与流式操作相比。通道是双向操作,可以读或写或同时读写操作。而流式操作是单向操作,必须一个是读操作一个是写操作。

3.什么是缓冲区?

缓冲区是指申请了固定大小的基本类型的数组内存区域。除此之外还有位置和界限,位置是读写下一个元素的数组下标,界限是第一个读写元素的数组下标。

操作缓冲区的方式可以根据下图理解

AZVBVjJ.jpg!web

缓冲区Buffer

  • ByteBuffer

  • LongBuffer

  • DoubleBuffer

  • CharBuffer

  • FloatBuffer

  • IntBuffer

  • ShortBuffer

  • MappedBuffer

4.轮询方式

轮询方式是指把轮询器(Selector)中的 SelectionKey进行迭代处理,每次只能拿到一个Key,每次只处理一种状态。在轮询时,只有在读或写状态时才可以对IO做读写操作。

5.IO多路复用

轮询的方式缺点就是浪费CPU资源,大部分时间可能没有数据可读,不必反复执行read操作,IO多路复用解决了此问题。多路复用模式通过一个线程可以管理多个socket,执行selector.select()操作时,去查询每个通道是否有到达事件,若没有事件,则一直阻塞。适合连接数多的场景,比多线程效率要高。

6.示例代码

public class NIOServer {

//轮询器

private Selector selector;

//缓冲区

private ByteBuffer buffer = ByteBuffer.allocate(128);


public NIOServer(int port) {

try {

ServerSocketChannel server = ServerSocketChannel.open();

server.bind(new InetSocketAddress(port));

System.out.println("监听端口:" + port);

//设置非阻塞

server.configureBlocking(false);

//打开连接

selector = Selector.open();

//注册接收数据

server.register(selector, SelectionKey.OP_ACCEPT);

} catch (IOException e) {

e.printStackTrace();

}

}


public void listen() {

try {

while (true) {

//

selector.select();

//

Set<SelectionKey> keys = selector.selectedKeys();

Iterator<SelectionKey> iterator = keys.iterator();

//轮询所有的key,每次拿到一个key,每次只处理一种状态

while (iterator.hasNext()) {

SelectionKey key = iterator.next();

iterator.remove();

//轮询处理事件

process(key);

}

}

} catch (IOException e) {

e.printStackTrace();

}

}


private void process(SelectionKey key) throws IOException {

//处理每一种状态

if (key.isAcceptable()) {

ServerSocketChannel server = (ServerSocketChannel) key.channel();

SocketChannel channel = server.accept();

channel.configureBlocking(false);

channel.register(selector, SelectionKey.OP_READ);

} else if (key.isReadable()) {

SocketChannel channel = (SocketChannel) key.channel();

int len = channel.read(buffer);

if (len > 0) {

buffer.flip();

String content = new String(buffer.array(), 0, len);

channel.register(selector, SelectionKey.OP_WRITE);

key.attach(content);

System.out.println("读取内容:" + content);

}

} else if (key.isWritable()) {

SocketChannel channel = (SocketChannel) key.channel();

String content = (String) key.attachment();

channel.write(ByteBuffer.wrap(("输出:" + content).getBytes()));

channel.close();

}

}


public static void main(String[] args) {

new NIOServer(8887).listen();

}

}

三、Reactor 异步阻塞

内容来源于《从零开始学架构》李运华

在前面分析IO多路复用时提到,selector.select()操作时,通过轮询的方式来检测是否有事件到达,并且对事件逐一进行响应。若响应体很大,在响应过程中迟迟未处理会影响其它事件轮询。于是我们采用IO多路复用+线程池的方式解决这个问题,大神们取了很牛的名字:Reactor(反应堆),“事件反应”是指来了一个事件我就有相应的反应。

Reactor模式的核心包括Reactor和处理资源池,其中Reactor负责监听和分配事件,处理资源池负责处理事件。Reactor模式具体实现方案灵活多变:Reactor的数量可以变化,资源池的数量可以变化。根据两种变化得出三种典型的实现方案:

  • 单Reactor单进程/线程

  • 单Reactor多线程

  • 多Reactor多进程/线程

1.单Reactor单进程/线程

nIB7niM.jpg!web

方案说明:

  • Reactor对象通过select监控连接事件,收到事件后通过dispatch进行分发。

  • 如果是连接建立的事件,由Acceptor处理。Acceptor通过accept接收连接并创建一个Handler来处理连接后续的各种事件。

  • 如果不是连接建立事件,则Reactor会调用连接对应的Handler来进行响应。

  • Handler会完成整个业务流程

此方案的缺点

  • 只有一个进程,无法发挥多核CPU的性能。只能采取部署多个系统来利用多核CPU,运维复杂。

  • Handler在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈。

使用此模式的场景,目前比较著名开源软件 Redis 是采用单 Reactor 单进程

2.单Reactor多线程

为了避免单Reactor单进程/线程的缺点,引入多线程,那么就产生了第二个方案:单Reactor多线程。

aE7j2qA.jpg!web

方案说明:

  • 主线程中,Reactor对象通过select 监控连接事件,收到事件后通过dispatch进行分发。

  • 如果是连接建立的事件,则由Acceptor处理,Acceptor通过accept接受连接,并创建一个Handler来处理连接后续的各种事件。

  • 如果不是连接建立事件,则Reactor会调用连接对应的Handler来进行响应

  • Handler 只负责响应事件,不进行业务处理。Handler通过read读取到数据后,会发给Processor进行业务处理

  • Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的Handler处理。Handler收到响应后通过send 将响应结果返回给client。

此方案的缺点

  • 多线程数据共享和访问比较复杂

  • Reactor承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈

3.多Reactor多进程/线程(主从Reactor多进程/线程)

为了解决单Reactor多线程的缺点问题,把单Reactor改为多Reactor,那么产生了第三个方案:多Reactor多进程/线程。

RbYZbuE.jpg!web

此方案说明:

  • 父进程中mainReactor 对象通过seelct 监控连接建立事件,收到事件后通过Acceptor接收,将新的连接分配给某个子进程/线程

  • 子进程/线程的subReactor将mainReactor 分配的连接加入连接队列进行监听,并创建一个Handler 用于处理连接的各种事件

  • 当有新的事件发生时,subReactor 会调用连接对应的Handler来进行响应

  • Handler完成 read->业务处理->send 的完整业务流程

使用此模式的场景:

Nginx 采用多Reactor多进程模式,但与标准的有差异

Memcache 和 Netty 采用多Reactor多线程模式

四、Proactor异步非阻塞

内容来源于《从零开始学架构》李运华

Reactor是同步非阻塞网络模型,用户进程在执行read和send这类的IO操作的时候是同步的,如果把IO操作改为异步就能进一步提升性能,于是就有了异步网络模型Proactor。

Proactor含义是主动的,我们翻译成主动器。Reactor可以理解为“来了事件我通知你,你来处理”,而Proactor理解“来了事件我来处理,处理完我通知你”。这里的我是指操作系统内核,事件是指有新的连接、有数据可读有数据可写的IO操作。

JVZz6rJ.jpg!web

方案说明:

  • Proactor Initiactor 负责创建Proactor和Handler,并将Proactor和Handler都通过异步操作处理注册到内核。

  • 异步操作处理负责处理注册请求,并完成IO操作

  • 异步操作处理完成IO操作后通知Proactor

  • Proactor根据不同的事件类型回调不同的Handler进行业务处理

  • Handler完成业务处理,Handler也可以注册新的Handler到内核进程。

理论上Proactor比Reactor效率要高一些,但实现真正的异步IO,操作系统需要做大量的工作,目前Windows下通过IOCP实现了真正的异步IO,而在Linux系统下AIO并不完善,因此在LInux下实现高并发网络编程时都以Reactor模式(采用epoll)模拟出来的异步模型。

以上是根据个人理解做了分析,如有不正确请留言讨论。

----------------------------------------------------------

再次感谢,欢迎关注微信公众号“零售云技术”,文章持续更新,或留言讨论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK