对于同步和非同步,阻塞和非阻塞,BIO,NIO的概念的回顾

同步和异步

同步和异步其实是指CPU时间片的利用,主要看请求发起方对消息结果的获取是主动发起的,还是被动通知的,如下图所示。如果是请求方主动发起的,一直在等待应答结果(同步阻塞),或者可以先去处理其他事情,但要不断轮询查看发起的请求是否有应答结果(同步非阻塞),因为不管如何都要发起方主动获取消息结果,所以形式上还是同步操作。如果是由服务方通知的,也就是请求方发出请求后,要么一直等待通知(异步阻塞),要么先去干自己的事(异步非阻塞)。当事情处理完成后,服务方会主动通知请求方,它的请求已经完成,这就是异步。异步通知的方式一般通过状态改变、消息通知或者回调函数来完成,大多数时候采用的都是回调函数。

 

阻塞和非阻塞

阻塞和非阻塞在计算机的世界里,通常指针对I/O的操作,如网络I/O和磁盘I/O等。那么什么是阻塞和非阻塞呢?简单地说,就是我们调用了一个函数后,在等待这个函数返回结果之前,当前的线程是处于挂起状态还是运行状态。如果是挂起状态,就意味着当前线程什么都不能干,就等着获取结果,这就是同步阻塞;如果仍然是运行状态,就意味着当前线程是可以继续处理其他任务的,但要时不时地看一下是否有结果了,这就是同步非阻塞。具体如下图所示

 

阻塞I/O模型

 

非阻塞I/O模型

多路复用I/O模型

从BIO到NIO的演进

面向流与面向缓冲Java NIO和BIO之间第一个最大的区别是,BIO是面向流的,NIO是面向缓冲区的。Java BIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程的灵活性。但是,还需要检查该缓冲区是否包含所有需要处理的数据。而且,要确保当更多的数据读入缓冲区时,不能覆盖缓冲区里尚未处理的数据。

例子分析

阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来(或超时)时才会返回;同样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接成功后,服务端都会启动一个线程去处理该客户端的请求。阻塞I/O的通信模型示意如下图所示。

缺点。(1)当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间。(2)阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。在这种情况下非阻塞I/O就有了它的应用前景。

BIO代码分析:

package com.io.demo;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Objects;

public class BioServer {

    private String ip;

    private int port;

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

    public void startListen() throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(ip, port));

        while (true) {
            // 阻塞
            Socket socket = serverSocket.accept();
            new RequestHandler(socket).start();
        }
    }

    private class RequestHandler extends Thread {
        private final Socket socket;


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

        @Override
        public void run() {
            InputStream in = null;
            InputStreamReader reader = null;
            BufferedReader br = null;
            OutputStream out = null;
            OutputStreamWriter writer = null;
            BufferedWriter bw = null;
            try {
                //输入流用于接收客户端传输过来的数据
                in = socket.getInputStream();
                reader = new InputStreamReader(in);
                br = new BufferedReader(reader);
                String line;
                while ((line = br.readLine()) != null) {
                    System.out.println(line);
                    //按照HTTP协议,请求头和请求体之间为一空行分隔
                    if ("".equals(line)) {
                        break;
                    }
                }
                //输出流用于向客户端发送响应消息,需遵从HTTP协议格式
                out = socket.getOutputStream();
                writer = new OutputStreamWriter(out);
                bw = new BufferedWriter(writer);
                bw.write(getResponseText());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //必须在请求读取和响应写入都处理完毕之后才可以调用close方法,将输入流关闭也会导致输出流不可用
                try {
                    Objects.requireNonNull(bw).close();
                    writer.close();
                    out.close();
                    br.close();
                    reader.close();
                    in.close();
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private String getResponseText() {
        StringBuffer sb = new StringBuffer();
        sb.append("HTTP/1.1 200 OK\n");
        sb.append("Content-Type: text/html; charset=UTF-8\n");
        sb.append("\n");
        sb.append("<html>");
        sb.append("  <head>");
        sb.append("    <title>");
        sb.append("      NIO Http Server");
        sb.append("    </title>");
        sb.append("  </head>");
        sb.append("  <body>");
        sb.append("    <h1>Hello World!</h1>");
        sb.append("  </body>");
        sb.append("</html>");

        return sb.toString();
    }

    public static void main(String[] args) {
        BioServer server = new BioServer("127.0.0.1", 8081);
        try {
            server.startListen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

Java NIO的工作原理。(1)由一个专门的线程来处理所有的I/O事件,并负责分发。(2)事件驱动机制:事件到的时候触发,而不是同步地去监视事件。(3)线程通信:线程之间通过wait、notify等方式通信。保证每次上下文切换都是有意义的,减少无谓的线程切换。下面是笔者理解的Java NIO反应堆的工作原理图。

(注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码和发送响应。)

NIO代码分析

package com.io.demo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;

public class NioServer {
    private String ip;

    private int port;

    private Selector selector;

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

    public void startListen() throws IOException {
        selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        serverChannel.bind(new InetSocketAddress(ip, port));

        while (true) {
            //不能使用select方法,该方法会阻塞,如果在阻塞过程中channel状态就绪,会因此处阻塞而无法执行。
            //所以,如果调用阻塞方法,下面对channel状态的处理得另起一个常驻线程
            int result = selector.selectNow();
            if (result == 0) {
                continue;
            }
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                if (key.isAcceptable()) {
                    accept(key);
                } else if (key.isReadable()) {
                    read(key);
                } else if (key.isWritable()) {
                    write(key);
                } else {
                    System.out.println("Unknow selector type");
                }

                //一定要调用remove方法将已经处理过的SelectionKey清除掉,否则会造成后面的请求无法接受
                it.remove();
            }
        }
    }

    private void accept(SelectionKey key) throws IOException {
        System.out.println("Receive connection");
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        SocketChannel channel = serverSocketChannel.accept();

        if (channel != null) {
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
        }
        System.out.println("Connection end");
    }

    private void read(SelectionKey key) throws IOException {
        System.out.println("Start read");
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(64);
        boolean hasContent = false;

        //这里的判断条件不能是不等于-1,因为channel一直都在,只是在数据被读完后里面为空,返回的长度是0.用-1判断会无限循环无法退出
        while (channel.read(buffer) > 0) {
            buffer.flip(); //切换为读模式
            CharBuffer cb = Charset.forName("UTF-8").decode(buffer);
            System.out.print(cb.toString());
            buffer.clear();
            hasContent = true;
        }

        if (hasContent) {
            //设置interestOps,用于写响应
            key.interestOps(SelectionKey.OP_WRITE);
        } else {
            channel.close();
        }
        System.out.println("Read end");
    }

    private void write(SelectionKey key) throws IOException {
        System.out.println("Start write");
        SocketChannel channel = (SocketChannel) key.channel();

        String resText = getResponseText();
        ByteBuffer buffer = ByteBuffer.wrap(resText.getBytes());

        //此处不可使用channel.write(buffer) != -1来判断,因为在两端都不关闭的情况下,会一直返回0,导致该循环无法退出
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }
        channel.close();
        System.out.println("End write");
    }

    private String getResponseText() {
        StringBuffer sb = new StringBuffer();
        sb.append("HTTP/1.1 200 OK\n");
        sb.append("Content-Type: text/html; charset=UTF-8\n");
        sb.append("\n");
        sb.append("<html>");
        sb.append("  <head>");
        sb.append("    <title>");
        sb.append("      NIO Http Server");
        sb.append("    </title>");
        sb.append("  </head>");
        sb.append("  <body>");
        sb.append("    <h1>Hello World!</h1>");
        sb.append("  </body>");
        sb.append("</html>");

        return sb.toString();
    }

    public static void main(String[] args) {
        NioServer server = new NioServer("127.0.0.1", 8080);
        try {
            server.startListen();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章