BIO、NIO线程模型

BIO(Blocking IO)

BIO(Blocking IO):同步阻塞模型,一个客户端连接对应一个处理线程,即:一个线程处理一个客户端请求。
    单线程版本: 服务端在处理完第一个客户端的所有事件之前,无法为其他客户端提供服务。
    多线程版本:如果出现大量只连接不发数据的话,那么就会一直占用线程,浪费服务器资源。
    线程池版本:用线程池根本不能根本的解决问题。假如我们有500个线程组成一个线程池,我们用这500个线程去为客户端服务。那么,假设前500个客户端请求进来,被占满了这500个线程,并且这500个客户端连接只连接不发数据,那么我们的服务端不就崩掉了吗?因为我们服务端最多只能处理500个客户端连接,而你后面进来的,不管多少都会被我们的服务端拒之门外,所以用线程池也不能解决这个问题。

public class SocketServer {
    public static void main(String[] args) throws Exception {
        //创建了服务端,绑定到了9001端口
        ServerSocket serverSocket = new ServerSocket(9001);
        while (true) {
            System.out.println("等待连接..");
            //阻塞方法
            Socket clientSocket = serverSocket.accept(); //监听客户端的连接,有客户端的连接代码才会往下走,没有连接就会阻塞在这里
            System.out.println("有客户端连接了..");

                        //1.单线程版本
            //handler(clientSocket);

            //2.多线程版本
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(clientSocket);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            //3.线程池版本
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        handler(clientSocket);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    //handler方法是处理客户端事件的方法,比如客户端发来数据,在这里我们就做打印处理
    private static void handler(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));
            }
    }
}

NIO(Non Blocking IO)

NIO模型1.0版本(早期版本)

一个线程无限循环去list(存放着客户端连接)轮训,检查是否有读写请求,如果有则处理,如果没有跳过。这个版本如果连接数特别多的话,会有大量的无效遍历,假如有1000个客户端连接,只有其中100个有读写事件,那么还是会循环1000次,其中的900次循环都是无效的。

public class NioServer {

    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

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

        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似,即创建了服务端并绑定了9001端口
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9001));
        // 设置ServerSocketChannel为非阻塞
        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);
                // 如果有数据,把数据打印出来,整个过程是一个main线程在处理
                if (len > 0) {
                    System.out.println(Thread.currentThread().getName() + " 接收到消息:" + new String(byteBuffer.array()));
                } else if (len == -1) { // 如果客户端断开,把socket从集合中去掉
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

NIO模型2.0版本(启用多路复用器,jdk1.4以上)

客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。这个版本进行了很大程度的优化,当客户端连接以后,如果有读写事件,则会加入一个队列里,处理事件的线程会阻塞等待这个队列里有新的元素之后处理事件。

步骤:

  1. 创建ServerSocketChannel服务端
  2. 创建多路复用器Selector( 每个操作系统创建出来的是不一样的,Centos创建的是EPollSelectorProviderImpl,Windows创建的是WindowsSelectorImpl,其实就是Linux的Epoll实例EPollArrayWrapper)
  3. ServerSocketChannel将建立连接事件注册到Selector中(register方法往EPollArrayWrapper中添加元素)
  4. 处理事件
    1. 如果是建立连接事件,则把客户端的读写请求也注册到Selector中
    2. 如果是读写事件则按业务处理
public class NioSelectorServer {

    public static void main(String[] args) throws IOException {
        //指定感兴趣的操作类型为 OP_ACCEPT
        int OP_ACCEPT = 1 << 4;
        System.out.println(OP_ACCEPT);

        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9001));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        // 打开Selector处理Channel,即创建epoll,这里启用了多路复用器是与NIO1.0版本最大的区别
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣(把连接事件注册到多路复用器里面)
        SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);//通过 SelectionKey.OP_ACCEPT 来识别和处理接受连接事件。
        System.out.println("服务启动成功");

        while (true) {
            // 阻塞等待需要处理的事件发生 已注册事件发生后,会执行后面逻辑
            selector.select();

            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件(连接事件),则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    SelectionKey selKey = socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件(读事件),则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println(Thread.currentThread().getName() +  "接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

Redis线程模型

Redis就是典型的基于epoll的NIO线程模型(nginx也是),epoll实例收集所有事件(连接与读写事件),由一个服务端线程连续处理所有事件命令。

猜你喜欢

转载自blog.csdn.net/crazy_xieyi/article/details/131314611