1.BIO(blocking I/O)
BIO是一个传统的IO,是一个阻塞的IO,当然都是这么说,那么堵塞在哪里呢,我们通过代码示例给大家解说
我先用程序模拟一个客户端连接服务端程序,建立一个socket连接来监听客户端,然后监听到了以后用getInputStream进行获取流然后死循环监听客户端的请求数据。
/**
* 用BIO方式让客户端连接程序,监听服务端
*
* @Author df
* @Date 2020/4/12 15:24
* @Version 1.0
*/
public class BioServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("BIOServer has started,Listening on port" + serverSocket.getLocalSocketAddress());
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
// BIO阻塞原因:getInputStream阻塞一直占用当前线程资源,不让当前线程做其他事情
Scanner input = new Scanner(clientSocket.getInputStream());
//针对每个socket,不断的进行数据交互
while (true) {
String request = input.nextLine();
if ("quit".equals(request)) {
break;
}
System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
String response = "From BIOServer Hello " + request + ".\n";
clientSocket.getOutputStream().write(response.getBytes());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
咱们来启动一下main方法,启动完成,监听本地的8888端口
接下来就用cmd命令telnet来连接服务,输入如下命令回车,如下第二张图就连接上了
输入字符敲回车,服务端都能收到了
然后打开第二个cmd窗口,同样输入telnet localhost 8888然后回车,你发现控制台没有再打印出第二个连接信息了,输入任何字符也都没有输出了,那么为什么呢?
当第一个线程请求以后,socket建立连接打印信息,一直循环等待,等第二个线程需要进来的时候就不能进行访问了,因为一直被第一个线程getInputStream堵塞着。也就是说阻塞一直占用当前线程资源,不让当前线程做其他事情。
那么这样阻塞也不是办法啊,那我难道只能进行一次连接么,这样肯定是不行的,那需要解决这个问题啊!
既然BIO通过获取getInputStream进行阻塞,那么可以不可以用多线程的方式解决这个问题呢,让客户端能够多个连接呢。也给大家准备了线程池改良版的代码示例
/**
* 线程池版改良BIO阻塞问题
*
* @Author df
* @Date 2020/4/12 16:09
* @Version 1.0
*/
public class BioServerThreadPool {
Map<Socket, String> map;
public static void main(String[] args) {
// 创建3个线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
RequestHandler requestHeader = new RequestHandler();
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("BIO thread Server has started,Listening on port" + serverSocket.getLocalSocketAddress());
while (true) {
Socket clientSocket = serverSocket.accept();
// 线程提交
executor.submit(new ClientHandler(clientSocket, requestHeader));
System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
}
} catch (
IOException e) {
e.printStackTrace();
}
}
/**
* @Author df
* @Date 2020/4/12 20:52
* @Version 1.0
*/
public class ClientHandler implements Runnable {
private static RequestHandler requestHandler;
private static Socket clientSocket;
public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
this.requestHandler = requestHandler;
this.clientSocket = clientSocket;
}
@Override
public void run() {
try (Scanner input = new Scanner(clientSocket.getInputStream())) {
while (true) {
String request = input.nextLine();
if ("quit".equals(request)) {
break;
}
System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
String response = "From BIOServer Hello " + request + ".\n";
clientSocket.getOutputStream().write(response.getBytes());
}
} catch (Exception e) {
}
}
}
public class RequestHandler {
public String handle(String request) {
return "From Server Hello " + request + ".\n";
}
}
咱们来启动一下,打开cmd同样输入telnet localhost 8888然后回车,发现可以连接,并且也正常通信的
那么再打开一个cmd,输入telnet localhost 8888然后回车,看是否能连接呢?发现第二个也能连接并且正常通信
那么打开第三个呢,能否连接呢?发现也是能连接上的,并且正常通信
那么第四个呢?发现可以连接,但是无法进行任何输出操作,为什么?
这时候就把请求操作都放到等待队列里了,所以你能有连接操作,但是已经被其他三个线程阻塞掉了,不能再有线程处理它了,那么这个时候咱们在其他三个已经连接成功的输入quit回车将其连接退出,又会发生什么呢?你会发现第四个线程立马把之前阻塞时候进行输入的字符全部输出,并且可以进行正常的通信了
其实tomcat中就是用这种BIO的方式进行请求连接,防止阻塞用线程池方式,默认线程池设置200个。所以多线程是解决阻塞的一种方式。
但是啊,在高并发场景下我有10000个人请求,如果选用线程池,就算开1000个线程池,9999个放入了等待队列里,而且等待队列也不是源源不断的存储的,再说了让用户等待这种设计是不行的,而且多线程切换的开销也大,所以这个是jdk的BIO留下的坑,那么jdk会解决,所以JDK1.4以后出现了NIO。
那么再说NIO之前一定要知道为什么IO是阻塞的呢?所以就说一下IO的阻塞流程!
1.1 BIO阻塞的过程!
其实IO是否阻塞和java代码没有直接关系,IO的本质(针对java而言):应用程序和操作系统内核进行数据交互。
为什么这么说呢?假如我们要读取硬盘的gupao.txt文件一定是要inputStream读取,但是我们的java程序一定能直接读取磁盘么,答案是否的也是没有权限的,需要操作系统帮助我们读取,我们java程序是进程也可以叫做用户空间,用户空间是程序员可以操作的,可以直接对硬盘操作的就是内核空间。下图在进行讲解一下
以读操作为例说一下过程:
- java程序发出读操作,内核空间收到请求通过磁盘控制器转化对磁盘进行操作。
- 操作以后返回数据通过DMA方式将数据放入内核空间的缓冲区里面。
- Java read是从缓冲区拿数据进行返回的。
所以走到这里应该明白阻塞不阻塞的与我们程序没有关系,我们也不会无缘无故的阻塞,阻塞不阻塞和内核空间才有关系。
但是JDK设计者想好了该如何解决,因为以前的BIO是我发起了read请求以后一直等待内核空间读取放入缓冲然后用户空间才读取,内核空间没有操作完它就会一直等待,但是NIO就是我发起了read请求以后我就不管了我还可以做其他的事情,直到你把数据读取完返回给我,我再操作,这样就是非阻塞的了
把以上的叙述讲解完大家就应该理解了,接下来说NIO
2.NIO(non-blocking I/O)
那么这张图就是同步非阻塞也就是NIO,首先application就是用户空间,kernel就是内核空间。
1.用户通过read,在这里就是recvfrom(java也要调用c语言去读取的)去读取,内核空间接收到了进行操作,然后用户空间不需要等待直接可以去干别的事情,然后再来询问内核空间处理好了么,没有的话用户空间还可以干别的,直到在继续询问内核空间发现内核空间告诉它自己处理好了(内核空间把数据拷贝到缓冲区)那么进行返回,结束
那么NIO方式的代码示例我也准备好了,给大家演示一下
public class NIOServer {
public static void main(String[] args) {
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置不阻塞
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(9999));
System.out.println("NIO NIOServer has started ,listening on port:" + serverChannel.getLocalAddress());
Selector selector = Selector.open();
// 每个客户端来了之后,就把客户端注册到selector选择器上,默认状态就是ACCEPT
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer buffer = ByteBuffer.allocate(1024);
RequestHandler requestHandler = new RequestHandler();
// 轮询,服务端不断轮询,等待客户端的连接
while (true) {
int select = selector.select();
if (select == 0) {
continue;
}
// 如果selector有的话,那么就取出对应的channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 判断SelectionKey中的channel状态如何
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept();
// 客户端的channel来源打出来
System.out.println("Connection from " + clientChannel.getRemoteAddress());
// 将客户端的也设置为非阻塞
clientChannel.configureBlocking(false);
// 将channel的状态设置为read
clientChannel.register(selector, SelectionKey.OP_READ);
}
// 接下来轮询到的时候发现状态是readable
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 数据的交互是以buffer为中间桥梁的
channel.read(buffer);
// 用buffer取数据也用buffer返回数据
String request = new String(buffer.array()).trim();
buffer.clear();
System.out.println(String.format("From %s : %s", channel.getRemoteAddress(), request));
String response = requestHandler.handle(request);
channel.write(ByteBuffer.wrap(response.getBytes()));
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
你们自行用之前的方法测试把,打开5,7八个都不会阻塞
3.AIO(NIO.2) (Asynchronous I/O)
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端I/O请求都是由OS先完成了再通知服务器应用启动线程进行处理。
1.当应用空间要读取的时候发送给内核空间,内核空间不管有没有处理直接返回,然后等到内核空间处理完毕直接发送信号(deliver signal)把数据传送给用户空间。
4.多路复用IO
IO的多路复用是在NIO基础上处理的,它的阻塞方式则和NIO(循环向系统调用是否数据准备就绪)都是一致的,但它的连接方式进行了优化,它通过select/epoll进行调用系统内核,一个进程可以监控多个文件描述符,当有可读的连接则返回通知,这时再此请求调用读写操作。
1.每一次的连接读取也好写操作也好都先不进行连接,都先存储也可以说是登记到这边,不会直接分配线程去调度资源,直到真的要操作input或Out的时候我才去分配线程做这些操作呢。
1.有操作进行连接都会通过select进行注册,轮询监控发现有需要读取或者写操作再去分配线程,那么之后的操作可以使用同步非阻塞或者异步非阻塞或者阻塞IO都是可以的。
以上就是全部内容,以上笔记记录学习来源咕泡教育-Jack老师的公开课学习整理!