java高级开发 - NIO

一、知识回顾

java IO模型有几种?

BIO 即(blocking)同步阻塞IO、NIO 同步非阻塞(non-blocking)IO、AIO 异步非阻塞IO(jdk7推出,可以说是NIO 2.0版)

1.理解什么是阻塞、非阻塞、同步、异步

举个简单的例子,我想喝热水,在家拿水壶烧水。

 一开始我比较笨,拿个水壶装上水放到炉子上等着水烧开(同步),在烧水过程中不做任何其他事情(阻塞);

 然后我发现这也太无聊了,还是在等着水烧开(这时候还是同步)的时候干点别的吧,打开手机,玩一把游戏,中途时不时地去看一下水是否烧开了(非阻塞);

 想着这样还是效率低下,然后我去买了个带响铃的水壶,只要水一烧开就发出响声通知我(异步),这下省心多了;

 下次用这个带响铃的来烧水(异步), 我可以继续等着水开而不做其他事情(阻塞) 或者直接自己一边玩去了,水壶响了再去处理(非阻塞); 

同步/异步关注的是消息如何通知,在上面的例子就是两种不同的通知方式:同步通知方式是人一直等消息,异步通知方式是由铃声报警器发送消息来通知人,人无需一直去查看消息。阻塞/非阻塞关注的是等待消息通知时的状态,阻塞的时候人的状态不变一直等着,非阻塞的时候人可以变成其他非等待状态,如玩游戏、看书等。

2.阻塞对服务端有什么影响

资源浪费、处理能力低下。

来一段BIO的socket通信代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * socket server 端
 *
 * @author gaojc
 * @date 2019/8/20 21:58
 */
public class BIOServer {
    public static int port = 8880;

    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);

            System.out.println("启动服务端,监听" + port + "端口");
            while (true) {
                try {
                    Socket accept = serverSocket.accept();
                    InputStream inputStream = accept.getInputStream();
                    BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

                    String line = null;
                    while ((line = bf.readLine()) != null) {
                        System.out.println("服务端收到消息:" + line);
                    }
                    accept.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

/**
 * Socket  client 端
 * 写1个socket通信,客户端向服务端发送数据,服务端打印出来
 *
 * @author gaojc
 * @date 2019/8/20 22:01
 */
public class BIOClient implements Runnable {
    String ip;
    int port;

    public BIOClient(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }

    public static void main(String[] args) {
        BIOClient bioClient = new BIOClient("127.0.0.1", 8880);
        new Thread(bioClient).start();
        System.out.println("启动客户端,连接" + bioClient.port + "端口");
    }

    @Override
    public void run() {
        try {
            while (true) {
                Socket socket = new Socket(ip, port);
                Scanner scanner = new Scanner(System.in);
                System.out.println("客户端开始输入:");
                String nextLine = scanner.nextLine();

                socket.getOutputStream().write((nextLine).getBytes("UTF-8"));

                socket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端程序 通过while (true) 循环,阻塞的监听来自客户端的连接,阻塞的接收客户端发来的数据并输出。

为了不让接收客户端发来的数据这里阻塞,把服务端程序改造一下,使用多线程:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * socket server 端
 *
 * @author gaojc
 * @date 2019/8/20 21:58
 */
public class BIOServer {
    public static int port = 8880;

    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);

            System.out.println("启动服务端,监听" + port + "端口");
            while (true) {
                try {
                    Socket socket = serverSocket.accept();

                    //为每个连接 的读取数据 开 新的线程
                    new Thread(new ServerThread(socket)).start();

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

    }

    //把接收客户端数据的地方 搞多线程
    static class ServerThread implements Runnable {
        Socket socket;

        public ServerThread(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

                String line = null;
                while ((line = bf.readLine()) != null) {
                    System.out.println("服务端收到消息:" + line);
                }
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

如果多线程并发请求过大,这样创建线程过多,线程栈需要系统内存,可能会造成OOM;

线程处理逻辑的代码还需要创建对象,消耗堆内存;

而且多线程使得CPU频繁切换,严重影响性能。

有没办法改进这些弊端?有,想到了线程池,那么来继续改造一下代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * socket server 端
 *
 * @author gaojc
 * @date 2019/8/20 21:58
 */
public class BIOServer {
    public static int port = 8880;

    public static void main(String[] args) {
        //复用线程,大大减少线程数量
        int threadNum = 2;

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(threadNum, threadNum,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>());

        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);

            System.out.println("启动服务端,监听" + port + "端口");
            while (true) {
                try {
                    Socket socket = serverSocket.accept();

                    //用线程池 读取每个连接的数据
                    threadPool.execute(new ServerThread(socket));

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        threadPool.shutdown();
    }

    //把接收客户端数据的地方 搞多线程
    static class ServerThread implements Runnable {
        Socket socket;

        public ServerThread(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

                String line = null;
                while ((line = bf.readLine()) != null) {
                    System.out.println("服务端收到消息:" + line);
                }
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

服务端用线程池 来接收客户端的数据,不用为每次连接都创建线程,减少内存开销,节省线程创建的成本

那么这样还是有问题,阻塞对线程池的影响:

阻塞等待接收客户端的数据时,占用线程,而线程池的线程是有限的,并发量大的时候,会导致没有足够的线程来处理请求,请求的响应时间长,甚至拒绝服务。

那么如果能不阻塞,在没有接收到数据的时候去干别的事情,不就好了?

二、NIO 华丽登场

这时候 就需要用到NIO了,NIO可以以 non - blocking模式工作,可以用极少量线程处理大量IO连接。

NIO的工作原理图:

java高级开发 - NIO_第1张图片

来改造socket服务端的代码,使用NIO

package com.gaojc.springboot.demo.servicefind.IO;

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * NIO socket server 端
 *
 * @author gaojc
 * @date 2019/8/22 21:58
 */
public class NIOSocketServer {
    public static int port = 8880;
    public static int threadNum = 2;

    public static void main(String[] args) {
        ByteBuffer readBuf = ByteBuffer.allocateDirect(1024);

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(threadNum, threadNum,
                10L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(100));
        //NIO多路选择器
        Selector selector;
        ServerSocketChannel serverSocketChannel = null;

        //连接计数
        int connectionCount = 0;

        try {
            //1.创建一个选择器
            selector = Selector.open();
            //2.打开服务器通道
            serverSocketChannel = ServerSocketChannel.open();

            //3、服务器通道绑定地址
            serverSocketChannel.bind(new InetSocketAddress(port));
            //4.设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //5.把服务器通道注册到selector选择器上,监听阻塞事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

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

            //selector循环监听
            while (true) {
                //1.等待 就绪的事件
                int selectCount = selector.select();
                //没有就绪的事件就 continue
                if (selectCount == 0) {
                    continue;
                }

                //2.得到就绪的事件
                Set selectionKeys = selector.selectedKeys();
                //3.遍历结果
                Iterator iterator = selectionKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //如果是阻塞的状态
                    if (selectionKey.isAcceptable()) {
                        //获取服务器通道
                        ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel sc = null;
                        try {
                            //1.调用服务器端的accpet方法获取客户端通道
                            sc = ssc.accept();
                            //2.设置为非阻塞
                            sc.configureBlocking(false);
                            //3.将客户端通道注册到selector中
                            sc.register(selector, SelectionKey.OP_READ, ++connectionCount);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    //如果是可读的状态了
                    if (selectionKey.isReadable()) {
                        //1.用线程池 读取通道的数据
                        threadPool.execute(new ServerThread(readBuf, selectionKey));
                        //2.取消注册,防止线程池处理不及时,重复选择
                        selectionKey.cancel();
                    }
                    //4.处理完了,一定要从迭代里remove
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        threadPool.shutdown();
    }

    //把接收客户端数据的地方 搞多线程
    static class ServerThread implements Runnable {
        ByteBuffer readBuf;
        SelectionKey selectionKey;

        public ServerThread(ByteBuffer readBuf, SelectionKey selectionKey) {
            this.readBuf = readBuf;
            this.selectionKey = selectionKey;
        }

        @Override
        public void run() {
            try {
                //1、先清空缓冲区,防止有上一次的读数据
                this.readBuf.clear();
                //2、获取客户端通道
                SocketChannel sc = (SocketChannel) selectionKey.channel();
                //3、看客户端是否有输入
                int count = sc.read(this.readBuf);
                if (count == -1) {//如果没有数据
                    selectionKey.cancel();
                    sc.close();
                }
                //4、把buffer转为读模式
                this.readBuf.flip();
                //5、根据缓冲区的数据长度创建对应大小的byte数组,接收缓冲区的数据
                byte[] data = new byte[this.readBuf.remaining()];
                //6、将数据从这个缓冲区传输到目标字节数组
                this.readBuf.get(data);
                //7、将byte数组内容打印出来
                String result = new String(data);
                System.out.println("接受到client的数据是:" + result);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

这样就使用了非阻塞的IO,使用了selector选择器,做到IO连接多路复用。

 

你可能感兴趣的:(java技术)