深入理解Java三种IO模式和Epoll模型

IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO

一、BIO(Blocking IO )

同步阻塞模型,一个客户端连接对应一个处理线程

深入理解Java三种IO模式和Epoll模型插图

1、BIO代码示例

1)服务器代码

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Handler;
  
public class SocketServer {
  
    public static void main(String[] args) throws  Exception {
  
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true){
            System.out.println("等待连接");
            //阻塞连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("有客户端连接。。。");
//            handle(clientSocket);
  
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handle(clientSocket);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
  
    public  static void handle(Socket clientSocket) throws  Exception{
        byte[] bytes = new byte[1024];
        System.out.println("准备read。。");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = clientSocket.getInputStream().read(bytes);
        System.out.println("read 完毕。");
        if (read !=-1){
            System.out.println("接收到客户端数据:" + new String(bytes,0,read));
        }
        clientSocket.getOutputStream().write("helloClient".getBytes());
        clientSocket.getOutputStream().flush();
    }
}

2)客户端代码

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import java.io.IOException;
import java.net.Socket;
  
public class SocketClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost", 9000);
        //向服务端发送数据
        socket.getOutputStream().write("HelloServer".getBytes());
        socket.getOutputStream().flush();
        System.out.println("向服务端发送数据结束");
        byte[] bytes = new byte[1024];
        //接收服务端回传的数据
        socket.getInputStream().read(bytes);
        System.out.println("接收到服务端的数据:" + new String(bytes));
        socket.close();
    }
}

2、缺点

从上面的代码我们可以看出来,BIO代码中连接事件和读写数据事件都是阻塞的,所以这种模式的缺点非常的明显

  • 1. 如果我们连接完成以后,不做读写数据操作会导致线程阻塞,浪费资源
  • 2. 如果每来一个连接我们都需要启动一个线程处理,那么会导致服务器线程太多,压力太大,比如C10K;

3、应用场景

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,但是程序比较简单。

二、NIO(Non Blocking IO)

同步非阻塞模型,服务器实现模式为一个线程可以处理多个请求连接,客户端发送的连接请求都会注册到多路复用器(selector)上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。

1、没有引入多路复用器的代码

1)示例

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
  
public class NioServer {
  
    static List<SocketChannel> channelList = new ArrayList<>();
  
    public static void main(String[] args) throws Exception {
        //创建NIO
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        //设置非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动。。");
        while (true) {
            //非阻塞模式accept方法不会阻塞,否则会阻塞
            //NIO的非阻塞模式是由操作系统内部实现,底层调用了Linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            if (socketChannel != null) {
                System.out.println("连接成功");
                //设置socketchannel为非阻塞
                socketChannel.configureBlocking(false);
                //保存客户端连接到list
                channelList.add(socketChannel);
            }
            //遍历连接读数据
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                //非阻塞模式read 方式不会阻塞 否则会阻塞
                int len = sc.read(byteBuffer);
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

2)缺点

如果连接数太多的话,会有大量的无效遍历,假如有10000个连接,其中只有1000个 连接有写数据,但是 由于其他9000个连接并没有断开看我们还是每次轮询遍历一万次,其中有 十分之一的遍历都是无效的,这显然是一个非常浪费资源的做法。

2、NIO引入多路复用器的代码

1)示例

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.security.Key;
import java.util.Iterator;
import java.util.Set;
  
public class NioSelectorServer {
  
    public static void main(String[] args) throws Exception {
  
        //创建NIO ServerSocketChannle
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress(9000));
        //设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        //打开Selector处理channel,即创建epoll
        Selector selector = Selector.open();
        //把ServerSocketChannel注册selector上,并且select对客户端accept连接操作感兴趣
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动");
        //
        while (true) {
            //阻塞等待需要处理的事件发生
            selector.select();
            //获取selector中注册的全部事件的SelectionKey实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            //遍历selectionKeys对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //如果是accept事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {
                    //进行数据读取
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

上面代码是利用NIO一个线程处理所有请求,这种单个线程处理的方式肯定是存在问题的,例如现在有10w个请求中,有1w个连接进行读写数据,那么SelectionKey就会有1w个请求,所以我们需要循环这1w个事件进行处理,比较费时间,如果这个时候再有连接进来,只能阻塞。

3、NIO有三大核心组件

NIO有三大核心组件:Channel(通道),Buffer(缓冲区)Selector(多路复用器)

  • 1. channel 类似流,每个channel对应一个buffer缓冲区,buffer底层是个数组。
  • 2. channel会注册到selector上,由selector根据channel的读写事件发生将其交由某个空闲的线程处理
  • 3. NIO的Buffer和channel都是既可以读又可以写的
深入理解Java三种IO模式和Epoll模型插图2

NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO。

4、示例

举个例子:例如我们去酒吧喝酒,在吧台坐下了20个人,中间一个服务员,select()或者poll()模式就是,服务员每次都是询问这个20个人是否需要喝酒,而epoll模型则是,20个人谁需要喝酒谁就举手,服务员每次只处理举手的那几个人即可。

NioSelectorServer 代码里如下几个方法非常重要,我们从Hotspot与Linux内核函数级别来理解下

1
2
3
Selector.open()  //创建多路复用器
 socketChannel.register(selector, SelectionKey.OP_READ)  //将channel注册到多路复用器上
selector.select()  //阻塞等待需要处理的事件发生
深入理解Java三种IO模式和Epoll模型插图4

5、总结

NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取Socket文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效遍历,将事件处理交给了操作系统内核(操作系统的终端程序),大大提高了效率。

6、Epoll函数详解

1
int epoll_create(int size);

创建一个epoll实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用。参数size代表可能会容纳size个描述符,但size不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。

1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

使用文件描述符epfd引用epoll实例,对目标文件描述符fs执行op操作。参数epfd表示epoll对应的文件描述符,参数fd表示socket对应的文件描述符。

1)参数op有以下几个值

  • EPOLL_CTL_ADD:注册新的fd到epfd中,并关联事件event;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null;

2)参数event是一个结构体

1
2
3
4
5
6
7
8
9
struct epoll_event {
      __uint32_t   events;      /* Epoll events */      epoll_data_t data;        /* User data variable */  };
   
  typedef union epoll_data {
      void        *ptr;
      int          fd;
      __uint32_t   u32;
      __uint64_t   u64;
  } epoll_data_t;

3)events的可选值

events有很多可选值,这里只举例最常见的几个:

  • EPOLLIN :表示对应的文件描述符是可读的;
  • EPOLLOUT:表示对应的文件描述符是可写的;
  • EPOLLERR:表示对应的文件描述符发生了错误;

成功则返回0,失败返回-1

1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待文件描述符epfd上的事件。epfd就是Epoll对应的文件描述符,events表示调用者所有可用事件的集合,maxevents表示最大等到多少个事件就返回,timeout是超时时间。

4)I/O多路复用底层

I/O多路复用底层主要用Linux内核函数(select 、poll、epoll)来实现。

selectpollepoll(jdk1.5及以上)
操作方式遍历遍历回调
底层实现数组链表哈希表
O效率每次调用都进行线性遍历,时间复杂度O(n)每次调用都进行线性遍历,时间复杂度O(n)事件通知方式,每当有IO事件放生,系统注册的回调函数就会被调用,事件复杂度O(1)
最大连接有上限无上限无上限

三、AIO模型

异步非阻塞模型,由操作系统完成后回调通知服务端程序启动线程去处理,一般适用于连接数比较多且连接时间比较长的应用

1、应用场景

AIO方式适用于连接数 多且连接比较长(重操作)的架构,JDK1.7开始支持

1)服务器代码

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannel;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
  
public class AIOServer {
  
    public static void main(String[] args) throws  Exception {
       final AsynchronousServerSocketChannel serverChannel =
                AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
  
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
                try {
                    System.out.println("2--" + Thread.currentThread().getName());
                    //再此接收客户端连接,如果不写这行代码后面的客户端连接不上服务端
                    serverChannel.accept(attachment,this);
                    System.out.print(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
  
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            System.out.println("3--"+Thread.currentThread().getName());
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, result));
                            socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
                        }
  
                        @Override
                        public void failed(Throwable exc, ByteBuffer buffer) {
                            exc.printStackTrace();
                        }
                    });
  
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
  
            @Override
            public void failed(Throwable exc, Object attachment) {
  
            }
        });
        System.out.println("1--"+Thread.currentThread().getName());
        Thread.sleep(Integer.MAX_VALUE);
    }
}

2)客户端代码

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
  
public class AIOClient {
  
    public static void main(String... args) throws Exception {
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
        socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
        ByteBuffer buffer = ByteBuffer.allocate(512);
        Integer len = socketChannel.read(buffer).get();
        if (len != -1) {
            System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
        }
    }
}

2、BIO、NIO、AIO对比

深入理解Java三种IO模式和Epoll模型插图6

3、为什么Netty使用NIO而不是AIO?

因为在Linux系统上,AIO的底层实现扔使用Epoll模型,没有很好的使用AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易再次进行深度优化,Linux上AIO还不够成熟。Netty是异步非阻塞框架,Netty在NIO上做了很多异步封装。

发表评论

欢迎阅读『深入理解Java三种IO模式和Epoll模型|Java、JDK、开源组件|Nick Tan-梓潼Blog』