一、网络编程IO模型

202 阅读9分钟

一、IO模型

1、什么是IO模型

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

2、BIO(Blocking IO)通信模型

1)快速入门

概念: BIO模型(同步阻塞IO模型),一个客户端连接对应一个处理线程,适用于连接数目比较小的架构。

处理步骤: 客户端发送请求,接收器acceptor每接收一个请求,就创建一个线程,处理完后,再通过输出流返回到客户端,然后销毁线程

缺陷: 一个客户端请求对应一个线程,客户端请求和服务器线程为1:1的关系,当请求过多的时候,线程就越来越多,服务端压力过大,可能导致JVM内存被大量线程占用,堆栈溢出,此外,BIO模型中read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,造成资源浪费

2)多线程版本BIO模型代码示例

//BIO模型服务端代码
public class SocketServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9000);
        while (true) {
            System.out.println("等待连接 ...");
            //阻塞方法
            Socket socket = serverSocket.accept();
            System.out.println("有客户端连接了 ...");
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    //业务处理逻辑
    private static void handler(Socket socket) throws IOException {
        byte[] bytes = new byte[1024];
        System.out.println("准备read ...");
        //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
        int read = socket.getInputStream().read(bytes);
        System.out.println("read完毕 ...");
        if (read != -1) {
            System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
        }
        //向客户端发送数据
        socket.getOutputStream().write("HelloClient".getBytes());
        socket.getOutputStream().flush();
    }
}
//BIO模型客户端代码
public class SocketClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 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();
    }
}

3、NIO(Non Blocking IO)通信模型

1)快速入门

概念: NIO模型(同步非阻塞IO模型),一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,适用于连接数目多且连接时间短的架构,JDK1.4及以上支持

核心对象: Buffer(缓冲区)、Channel(通道)、Selector(选择器)

Buffer: 在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,底层是数组

Channel: 通道,类似于流,可以读写数据,每个channel对应一个buffer缓冲区,所有的数据都是通过buffer对象来处理。写入数据时,不是直接将字节写入通道中,而是将数据写入包含一个或者多个字节的缓冲区中。读取数据时,也不是直接从通道中读取字节,而是将数据从通道中读入缓冲区,再从缓冲区获取字节。

selector: 选择器,负责管理与客户端建立的多个连接,负责监听注册到上面的一些事件(新连接接入、当前连接可读消息或可写消息),一旦事件被其监听到,就会调用对应的事件处理器来完成对事件的响应

2)IO多路复用

IO多路复用底层一般用的Linux API(select、poll、epoll)来实现,他们的区别如下:

select poll epoll(JDK1.5及以上)
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线性遍历 每次调用都进行线性遍历 事件通知方式,每当有IO事件就绪,就会触法事件通知
最大连接 有上限 无上限 无上限

3)单线程版本NIO代码示例

//NIO模型服务端代码
public class NIOServer {
    private static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);
    public static void main(String[] args) throws IOException {
        //todo 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //todo 必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        ssc.configureBlocking(false);
        //todo 绑定连接
        ssc.socket().bind(new InetSocketAddress(9000));
        //todo 创建一个选择器selector
        Selector selector = Selector.open();
        //todo 把ServerSocketChannel注册到selector上,并指定监听接收事件accept
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            LOGGER.info("等待事件发生。。");
            //todo 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
            selector.select();
            LOGGER.info("有事件发生了。。");
            //todo 有客户端请求,被轮询监听到
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                //todo 删除本次已处理的key,防止下次select重复处理
                it.remove();
                handle(key);
            }
        }
    }

    private static void handle(SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            LOGGER.info("有客户端连接事件发生了。。");
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            //todo NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
            //todo 处理完连接请求不会继续等待客户端的数据发送
            SocketChannel sc = ssc.accept();
            sc.configureBlocking(false);
            //todo 通过Selector监听Channel时对读事件感兴趣
            sc.register(key.selector(), SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            LOGGER.info("有客户端数据可读事件发生了。。");
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //todo NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
            int len = sc.read(buffer);
            if (len != -1) {
                System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
            }
            ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
            sc.write(bufferToWrite);
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        } else if (key.isWritable()) {
            SocketChannel sc = (SocketChannel) key.channel();
            LOGGER.info("write事件");
            //todo NIO事件触发是水平触发
            //todo 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
            //todo 在有数据往外写的时候再注册写事件
            key.interestOps(SelectionKey.OP_READ);
        }
    }
}
//NIO模型客户端代码
public class NioClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(NioClient.class);
    public static void main(String[] args) throws IOException {
        //todo 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        //todo 设置通道为非阻塞
        channel.configureBlocking(false);
        //todo 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
        //todo 用channel.finishConnect() 才能完成连接
        channel.connect(new InetSocketAddress("127.0.0.1", 9000));
        //todo 获得一个通道管理器
        Selector selector = Selector.open();
        //todo 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
        //todo 轮询访问selector
        while (true) {
            selector.select();
            //todo 获得selector中选中的项的迭代器
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                //todo 删除已选的key,以防重复处理
                it.remove();
                //todo 连接事件发生
                if (key.isConnectable()) {
                    SocketChannel ch = (SocketChannel) key.channel();
                    //todo 如果正在连接,则完成连接
                    if (ch.isConnectionPending()) {
                        ch.finishConnect();
                    }
                    //todo 设置成非阻塞
                    ch.configureBlocking(false);
                    //todo 在这里可以给服务端发送信息哦
                    ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
                    channel.write(buffer);
                    //todo 在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    channel.register(selector, SelectionKey.OP_READ);                                            // 获得了可读的事件
                } else if (key.isReadable()) {
                    //todo 和服务端的read方法一样
                    //todo 服务器可读取消息:得到事件发生的Socket通道
                    SocketChannel ch = (SocketChannel) key.channel();
                    //todo 创建读取的缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = ch.read(buffer);
                    if (len != -1) {
                        System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
                    }
                }
            }
        }
    }
}

4)NIO服务端程序详细分析(客户端程序类似)

1、创建ServerSocketChannel实例并绑定端口号
2、创建Selector实例
3、将ServerSocketChannel实例注册到Selector上并监听accept(连接)事件
4、selector通过select()方法监听channel事件,当客户端连接时,selector监听到连接事件,获取到ServerSocketChannel注册时绑定的selectionKey 
5、根据selectionKey获取监听accept(连接)事件并通过channel()方法可以获取绑定的ServerSocketChannel 
6、ServerSocketChannel通过accept()方法得到SocketChannel
7、将SocketChannel注册到Selector上并监听read(读)事件
8、注册后返回一个SelectionKey,会和该SocketChannel关联
9、selector继续通过select()方法监听事件,当客户端发送数据给服务端,selector监听到read(读)事件,获取到SocketChannel注册时绑定的selectionKey 
10、根据selectionKey获取监听read(读)事件并通过channel()方法可以获取绑定的SocketChannel 
11、将socketChannel里的数据读取出来
12、用socketChannel将服务端数据写回客户端

5)总结

  • NIO模型的selector就像一个大总管,负责监听各种IO事件,然后转交给后端线程去处理
  • NIO相对于BIO非阻塞主要体现在:BIO的后端线程需要阻塞等待客户端写数据(比如read方法),如果客户端不写数据线程就要阻塞,NIO把等待客户端操作的事情交给了大总管selector,selector负责轮询所有已注册的客户端,发现有事件发生了才转交给后端线程处理,后端线程不需要做任何阻塞等待,直接处理客户端事件的数据即可,处理完马上结束,或返回线程池供其他客户端事件继续使用。

Redis就是典型的NIO线程模型,selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果写回客户端

4、AIO(NIO 2.0)

1)快速入门

概念: AIO模型(异步非阻塞IO模型),由操作系统完成后回调通知服务端程序启动线程去处理,适用于连接数目多且连接时间较长的架构,JDK1.7及以上支持

2)单线程版本AIO代码示例

//AIO模型服务端代码
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 {
                    // 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
                    serverChannel.accept(attachment, this);
                    System.out.println(socketChannel.getRemoteAddress());
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            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) {
                exc.printStackTrace();
            }
        });
        Thread.sleep(Integer.MAX_VALUE);
    }
}
//AIO模型客户端代码
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));
        }
    }
}

5、BIO、NIO、AIO异同

BIO NIO AIO
IO模型 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
编程难度 简单 复杂 复杂
可靠性
吞吐量