简述 BIO 、NIO 模型

发布于:2024-05-07 ⋅ 阅读:(20) ⋅ 点赞:(0)
  • BIO : 同步阻塞I/O(Block IO)

         服务器实现模式为每一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,此处可以通过线程池机制进行优化。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * 多人聊天室 - 服务端
 */
public class BioServer {
    static List<Socket> clientList = new ArrayList<>();

    public static void main(String[] args) throws Exception {

        int port = 8080;

        ServerSocket serverSocket = new ServerSocket(port);

        while (true) {
            Socket client = serverSocket.accept();

            System.out.println("客户端: " + client.getPort() + " 连接成功!");

            clientList.add(client);

            forwardProcess(client);
        }
    }

    /**
     * 转发处理
     */
    public static void forwardProcess(Socket socket) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    forwardMsg(socket);
                }
            }
        }).start();
    }

    /**
     * 转发消息
     */
    public static void forwardMsg(Socket socket) {

        try {
            String msg = readMsg(socket);

            System.out.println(msg);

            for (Socket client : clientList) {
                if (client != socket) {
                    writeMsg(client, msg);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 写入消息
     */
    public static void writeMsg(Socket socket, String msg) throws IOException {
        PrintStream ps = new PrintStream(socket.getOutputStream());
        ps.println(msg);
        ps.flush();
    }

    /**
     * 读取消息
     */
    public static String readMsg(Socket socket) throws IOException {
        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String msg;
        if ((msg = br.readLine()) != null) {
            msg = socket.getPort() + " 说: " + msg;
        }
        return msg;
    }

}

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class BilClient {

    public static void main(String[] args) throws IOException {

        String ip = "127.0.0.1";
        int port = 8080;

        Socket client = new Socket(ip, port);

        readProcess(client);

        OutputStream os = client.getOutputStream();
        PrintStream ps = new PrintStream(os);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String input = scanner.nextLine();
            ps.println(input);
            ps.flush();
        }
    }

    /**
     * 读取处理
     */
    public static void readProcess(Socket socket) {
        new Thread(() -> {
            while (true) {
                try {
                    System.out.println(readMsg(socket));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

    /**
     * 读取消息
     */
    public static String readMsg(Socket socket) throws IOException {
        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String msg;
        if ((msg = br.readLine()) != null) {
        }
        return msg;
    }
}

  • ​NIO: 同步非阻塞式IO,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。
  • Channel,翻译过来就是“通道”,就是数据传输的管道,类似于“流”,但是与“流”又有着区别。
    • 既可以从Channel中读取数据,又可以写数据到Channel,但流的读写通常是单向的——输入输出流
    • 通道可以异步读写
    • 通道中的数据总是先读取到buffer(缓冲区),或者总是需要从一个buffer写入,不能直接访问数据
    • 非阻塞特性:Channel在设计上采用了非阻塞的特性,它不会像传统的流一样在读写操作上阻塞线程,而是立即返回结果,告诉调用者当前的状态。这使得程序可以在等待数据准备的过程中同时进行其他操作,实现了非阻塞IO。
    • 事件通知机制:Channel通常搭配选择器(Selector)来使用,选择器能够检测多个Channel的就绪状态,如是否可读、可写等,并通过事件通知(例如轮询或回调)及时地通知程序哪些Channel处于就绪状态,从而可以进行相应的读写操作。这种机制支持程序实现异步IO模型。
    • 操作系统底层支持:Channel的异步读写也依赖于操作系统底层的异步IO支持。Java NIO中的Channel实际上是对操作系统底层异步IO的封装和抽象,利用了操作系统提供的异步IO机制来实现其自身的异步读写功能。
  • Buffer是一个对象,里面是要写入或者读出的数据,在java.nio库中,所有的数据都是用缓冲区处理的。
    • 在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是直接写到缓冲区中,任何时候访问Channel中的数据,都是通过缓冲区进行操作的。
    • 缓冲区实质上是一个数组,通常是一个字节数组ByteBuffer,当然也有其他类型的:
  • Selector被称为选择器,Selector会不断地轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就被判定处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取到就绪Channel的集合,进行后续的I/O操作。
    • 一个多路复用器Selector可以同时轮询多个Channel,JDK使用了epoll()代替了传统的select实现,所以并没有最大连接句柄的限制,这意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Set;

/**
 * NIO 聊天室 服务端
 */
public class NioServer {

    private Integer port;

    ByteBuffer readBuffer = ByteBuffer.allocate(1024);

    ByteBuffer writerBuffer = ByteBuffer.allocate(1024);

    private Charset charset = Charset.forName("UTF-8");

    public NioServer(Integer port) {
        this.port = port;
    }

    public static void main(String[] args) {
        NioServer nioServer = new NioServer(8080);
        nioServer.start();
    }

    private void start() {

        try {

            // 开启socket
            ServerSocketChannel server = ServerSocketChannel.open();

            // 设置非阻塞
            server.configureBlocking(false);

            // 绑定端口
            server.socket().bind(new InetSocketAddress(port));

            // 开启通道, 得到 Selector (选择器)
            Selector selector = Selector.open();

            //  注册 selector 监听事件
            server.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("启动服务器, 监听端口:" + port + "...");

            while (true) {

                // 阻塞监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入 集合内部并返回事件数量
                selector.select();

                // 返回存有SelectionKey的集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                for (SelectionKey selectionKey : selectionKeys) {
                    handle(selectionKey, selector);
                }

                //  处理后清理 selectionKeys
                selectionKeys.clear();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    /**
     * 事件处理
     */
    private void handle(SelectionKey key, Selector selector) throws IOException {

        //  SelectionKey 常用方法
        //isAcceptable()	是否是连接继续事件
        //isConnectable()	是否是连接就绪事件
        //isReadable()	是否是读就绪事件
        //isWritable()	是否是写就绪事件

        // SelectionKey 常用事件
        //SelectionKey.OP_ACCEPT	接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
        //SelectionKey.OP_CONNECT	连接就绪事件,表示客户端与服务器的连接已经建立成功
        //SelectionKey.OP_READ	读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
        //SelectionKey.OP_WRITE	写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)

        //处理连接
        if (key.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();

            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
            System.out.println(client.socket().getPort() + " 已建立连接 ...");
        }
        //  读取消息
        else if (key.isReadable()) {

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

            String msg = client.socket().getPort() + " 说: " + readMsg(client);

            System.out.println(msg);

            forwardMsg(msg, client, selector);

        }
    }

    /**
     * 读取通道消息
     */
    private String readMsg(SocketChannel client) throws IOException {
        readBuffer.clear();
        while (client.read(readBuffer) > 0) ;
        readBuffer.flip();
        return String.valueOf(charset.decode(readBuffer));
    }

    /**
     * 转发
     */
    private void forwardMsg(String msg, SocketChannel client, Selector selector) throws IOException {
        for (SelectionKey key : selector.keys()) {
            Channel connectedClient = key.channel();
            if (connectedClient instanceof ServerSocketChannel) {
                continue;
            }

            if (key.isValid() && !client.equals(connectedClient)) {
                writerBuffer.clear();
                writerBuffer.put(charset.encode(msg));
                writerBuffer.flip();
                while (writerBuffer.hasRemaining())
                    ((SocketChannel) connectedClient).write(writerBuffer);
            }
        }
    }
}


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;
import java.util.Set;

/**
 * NIO 聊天室 客户端
 */
public class NioClient {

    private String ip;

    private Integer port;

    private ByteBuffer writerBuffer = ByteBuffer.allocate(1024);

    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);

    private Charset charset = Charset.forName("UTF-8");

    public NioClient(String ip, Integer port) {
        this.ip = ip;
        this.port = port;
    }

    public static void main(String[] args) {
        NioClient nioClient = new NioClient("127.0.0.1", 8080);
        nioClient.start();
    }


    public void start() {

        try {

            // 开启通道
            SocketChannel client = SocketChannel.open();

            // 设置非阻塞
            client.configureBlocking(false);

            Selector selector = Selector.open();
            client.register(selector, SelectionKey.OP_CONNECT);
            client.connect(new InetSocketAddress(ip, port));

            while (true) {
                selector.select();

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

                for (SelectionKey selectionKey : selectionKeys) {
                    handle(selectionKey, selector);
                }

                selectionKeys.clear();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private void handle(SelectionKey key, Selector selector) throws IOException {
        // 处理连接事件
        if (key.isConnectable()) {

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

            if (client.isConnectionPending()) {
                client.finishConnect();

                // 处理用户输入
                new Thread(() -> {
                    Scanner scanner = new Scanner(System.in);
                    while (true) {
                        String msg = scanner.nextLine();

                        writerBuffer.clear();
                        writerBuffer.put(charset.encode(msg));
                        writerBuffer.flip();

                        while (writerBuffer.hasRemaining()) {
                            try {
                                client.write(writerBuffer);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        }

                    }
                }).start();

            }
            client.register(selector, SelectionKey.OP_READ);
        }
        // 读取消息信息
        else if (key.isReadable()) {

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

            String s = readMsg(client);
            System.out.println(s);

        }
    }

    private String readMsg(SocketChannel client) throws IOException {
        readBuffer.clear();
        while (client.read(readBuffer) > 0) ;
        readBuffer.flip();
        return String.valueOf(charset.decode(readBuffer));
    }
}