Java学习笔记13-BIO阻塞、NIO非阻塞网络编程

Java学习笔记13-BIO阻塞、NIO非阻塞网络编程

BIO阻塞式

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。

阻塞(blocking)IO:资源不可用时,IO请求一直阻塞,直到返回结果(有数据或超时)。

非阻塞(non-blocking)IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用。

同步(synchronous)IO:应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败。

异步(asynchronous)IO:应用发送或接收数据后立即返回,实际处理是异步执行的。

阻塞和非阻塞是获取资源的方式,同步和异步是程序如何处理资源的逻辑设计。

代码中使用的API:ServerSocket#accept、InputStream#read(我们这里用的BufferedReader#readLine)都是阻塞的API。操作系统底层API中,默认Socket操作都是Blocking型,send / recv等接口都是阻塞的。带来的问题:阻塞导致在处理网络I/O时,一个线程只能处理一个网络连接。

下面我们直接上代码感受下BIO服务端与客户端的网络通信~

BIO服务端测试代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: Wenx
 * @Description:
 * @Date: Created in 2019/11/18 21:02
 * @Modified By:
 */
public class BIOServer {
    public static void main(String[] args) throws IOException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 服务端在20480端口监听客户端请求的TCP连接
        ServerSocket server = new ServerSocket(20480);
        System.out.println("Socket服务端-启动");
        while (!server.isClosed()) {
            // 等待客户端的连接,如果没有获取连接则阻塞
            Socket client = server.accept();
            System.out.println("接收到客户端连接:" + client);
            // 为每个客户端连接开启一个线程
            executorService.execute(() -> {
                try {
                    // 获取Socket的输入流,用来接收从客户端发送过来的数据
                    BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
                    // 获取Socket的输出流,用来向客户端发送数据
                    PrintStream out = new PrintStream(client.getOutputStream(), false, "UTF-8");
                    String msg;
                    // 接收从客户端发送过来的数据
                    while ((msg = buf.readLine()) != null) {
                        if (msg.length() == 0) {
                            out.println("");
                            break;

                            // 响应结果 200
                            //OutputStream outputStream = client.getOutputStream();
                            //outputStream.write("HTTP/1.1 200 OK\r\n".getBytes());
                            //outputStream.write("Content-Length: 11\r\n\r\n".getBytes());
                            //outputStream.write("Hello World".getBytes());
                            //outputStream.flush();
                        }
                        System.out.println(msg);
                        // 将接收到的字符串前面加上“已收到”,发送到对应的客户端
                        out.println("已收到:" + msg);
                    }
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        System.out.println("客户端连接关闭:" + client);
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        server.close();
        System.out.println("Socket服务端-关闭");
    }
}

BIO客户端测试代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.SocketTimeoutException;

/**
 * @Author: Wenx
 * @Description:
 * @Date: Created in 2019/11/18 21:31
 * @Modified By:
 */
public class BIOClient {
    public static void main(String[] args) throws IOException {
        // 客户端请求与本机在20480端口建立TCP连接
        Socket client = new Socket("127.0.0.1", 20480);
        client.setSoTimeout(600000);
        System.out.println("Socket客户端-启动");
        // 获取Socket的输入流,用来接收从服务端发送过来的数据
        BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));
        // 获取Socket的输出流,用来发送数据到服务端
        PrintStream out = new PrintStream(client.getOutputStream(), false, "UTF-8");
        // 获取键盘输入
        BufferedReader input = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));

        new Thread(() -> {
            String msg;
            // 接收从键盘发送过来的数据
            try {
                System.out.print("请输入信息:");
                while ((msg = input.readLine()) != null) {
                    out.println(msg);
                    if (msg.length() == 0) {
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        try {
            String echo;
            // 从服务器端接收数据有个时间限制(系统自设,也可以自己设置),超过了这个时间,便会抛出该异常
            while ((echo = buf.readLine()) != null) {
                if (echo.length() == 0) {
                    System.out.println("服务端连接关闭");
                    break;
                }
                System.out.println(echo);
                System.out.print("请输入信息:");
            }
        } catch (SocketTimeoutException e) {
            System.out.println("Time out, No response");
        }

        input.close();
        if (client != null) {
            // 如果构造函数建立起了连接,则关闭套接字,如果没有建立起连接,自然不用关闭
            client.close(); // 只关闭socket,其关联的输入输出流也会被关闭
            System.out.println("Socket客户端-关闭");
        }
    }
}
Http协议

Http协议-请求数据包解析

第一部分:请求行,请求类型,资源路径以及HTTP版本。

第二部分:请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。

第三部分:空行,请求头部后面的空行是必须的,请求头部和数据主体之间必须有换行。

第四部分:请求数据也叫主体,可以添加任意的数据。这个例子的请求数据为空。

GET / HTTP/1.1
Host: localhost:20010
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: __guid=111872281.1396970361135427000.1571659920085.445


Http协议-响应数据包解析

第一部分:状态行,HTTP版本、状态码、状态消息。

第二部分:响应报头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。

第三部分:空行,请求头部后面的空行是必须的,请求头部和数据主体之间必须有换行。

第四部分:响应正文,可以添加任意的数据。这个例子的响应正文为“Hello World”。

HTTP/1.1 200 OK
Content-Length: 11

Hello World

Http协议-响应状态码

1xx(临时响应):表示临时响应并需要请求者继续执行操作的状态代码。

2xx(成功):表示成功处理了请求的状态代码。

3xx(重定向):表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向。

4xx(请求错误):这些状态代码表示请求可能出错,妨碍了服务器的处理。

5xx(服务器错误):这些状态代码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。

NIO非阻塞式

NIO 是 Java 1.4 引入的 java.nio 包,提供了新的 Java IO操作非阻塞API。用意是替代 Java IO 和 Java Networking相关的API,NIO中有三个核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器),可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

Buffer缓冲区

缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以轻松地使用内存块。相比较直接的对数组的操作,Buffer API更加容易操作和管理。

使用Buffer进行数据写入与读取,需要进行如下四个步骤:

  1. 将数据写入缓冲区
  2. 调用buffer.flip(),转换为读取模式
  3. 缓冲区读取数据
  4. 调用buffer.clear()或buffer.compact()清除缓冲区

Buffer工作原理

Buffer三个重要属性:

capacity容量:作为一个内存块,Buffer具有一定的固定大小,也称为“容量”。

position位置:写入模式时代表写数据的位置。读取模式时代表读数据的位置。

limit限制:写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。

写入模式长度为8读取模式长度为8
位置0(有数据)position -->位置0(有数据)
位置1(有数据)位置1(有数据)
位置2(有数据)位置2(有数据)
position -->位置3(有数据)limit -->位置3(有数据)
位置4位置4
位置5位置5
位置6位置6
limit / capacity -->位置7capacity -->位置7

Buffer测试代码

import java.nio.ByteBuffer;

/**
 * @Author: Wenx
 * @Description:
 * @Date: Created in 2019/11/19 11:33
 * @Modified By:
 */
public class BufferDemo {
    public static void main(String[] args) {
        // 构建一个byte字节缓冲区,容量是4
        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
        //ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4); // 堆外
        // 默认写入模式,查看三个重要的指标
        System.out.println(String.format("初始化:position位置:%s, limit限制:%s capacity容量:%s,", byteBuffer.position(), byteBuffer.limit(), byteBuffer.capacity()));
        // 写入3字节的数据
        byteBuffer.put((byte) 1);
        byteBuffer.put((byte) 2);
        byteBuffer.put((byte) 3);
        // 再看数据
        System.out.println(String.format("写入3字节后,position位置:%s, limit限制:%s capacity容量:%s,", byteBuffer.position(), byteBuffer.limit(), byteBuffer.capacity()));

        // 转换为读取模式(不调用flip方法,也是可以读取数据的,但是position记录读取的位置不对)
        byteBuffer.flip();
        System.out.println(String.format("转换为读取模式后,position位置:%s, limit限制:%s capacity容量:%s,", byteBuffer.position(), byteBuffer.limit(), byteBuffer.capacity()));
        // 读取2字节的数据
        byte a = byteBuffer.get();
        System.out.println(a);
        byte b = byteBuffer.get();
        System.out.println(b);
        System.out.println(String.format("读取2字节数据后,position位置:%s, limit限制:%s capacity容量:%s,", byteBuffer.position(), byteBuffer.limit(), byteBuffer.capacity()));

        // 继续写入3字节,此时读模式下,limit=3,position=2.继续写入只能覆盖写入一条数据
        // clear()方法清除整个缓冲区。compact()方法仅清除已阅读的数据。转为写入模式
        byteBuffer.compact(); // buffer : 1 , 3
        System.out.println(String.format("清除已阅读的数据后,position位置:%s, limit限制:%s capacity容量:%s,", byteBuffer.position(), byteBuffer.limit(), byteBuffer.capacity()));
        // 写入3字节的数据
        byteBuffer.put((byte) 3);
        byteBuffer.put((byte) 4);
        byteBuffer.put((byte) 5);
        System.out.println(String.format("再写入3字节后最终的情况,position位置:%s, limit限制:%s capacity容量:%s,", byteBuffer.position(), byteBuffer.limit(), byteBuffer.capacity()));

        // rewind() 重置position为0
        // mark() 标记position的位置
        // reset() 重置position为上次mark()标记的位置
    }
}

ByteBuffer内存类型

ByteBuffer为性能关键性代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现。

堆外内存获取的方式:ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(noByteBuffer);

好处:

  1. 进行网络IO或者文件IO时比HeapByteBuffer少一次拷贝。(file/socket — OS memory — jvm heap)CG会移动对象内存,在写file或socket的过程中,JVM的实现中,会先把数据复制到堆外,再进行写入。
  2. GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator。

建议:

  1. 性能确实可观的时候才去使用;分配给大型、长寿命;(网络传输、文件读写场景)
  2. 通过虚拟机参数MaxDirectMemorySize现值大小,防止耗尽整个机器的内存。
Channel通道

BIO:代码 --byte[]数据写入–> outputStream -----网络-----> inputStream --read阻塞读取–> 代码

NIO:代码 --> Buffer --缓冲区数据写入通道–> channel -----网络-----> channel --通道数据写入缓冲区–> Buffer–> 代码

Channel的API涵盖了 TCP/UDP 网络和文件IO:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,和标准IO Stream操作的区别:在一个通道内进行读取和写入,而stream通常是单向的(input或output),可以非阻塞读取和写入通道,通道始终读取和写入缓冲区。

Selector选择器

Selector是一个Java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已准备好进行读取或写入。实现多个线程可以管理多个通道,从而管理多个网络连接。

一个线程使用Selector监听多个channel的不同事件:四个事件分别对应SelectionKey四个常量。

  1. Connect连接(SelectionKey.OP_CONNECT)
  2. Accept准备就绪(SelectionKey.OP_ACCEPT)
  3. Read读取(SelectionKey.OP_READ)
  4. Write写入(SelectionKey.OP_WRITE)

实现一个线程处理多个通道的核心概念理解:事件驱动机制。

非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来出发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)

NIO对比BIO

BIO:客户端 -----连接-----> Acceptor ----------> 线程-n read…write

  • 阻塞IO,线程等待时间长
  • 一个线程负责一个连接处理
  • 线程多且利用率低

NIO:客户端 -----连接-----> Selector事件通知机制 ----------> 线程-n read…write

  • 非阻塞IO,线程利用率更高
  • 一个线程处理多个连接事件
  • 性能更强大

如果你的程序需要支撑大量的连接,使用NIO是最好的方式。

Tomcat8中,已经完全去除BIO相关的网络处理代码,默认采用NIO进行网络处理。

NIO与多线程结合的改进方案

采用Reactor模式进行改进,参照Doug Lea的著名文章《Scalable IO in Java》

NIO服务端测试代码

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;

/**
 * @Author: Wenx
 * @Description:
 * @Date: Created in 2019/11/19 13:26
 * @Modified By:
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建Socket服务端
        ServerSocketChannel server = ServerSocketChannel.open();
        server.configureBlocking(false); // 设置为非阻塞模式
        // 2. 构建一个Selector选择器,并且将ServerSocketChannel的accept事件注册绑定到selector选择器上
        Selector selector = Selector.open();
        server.register(selector, SelectionKey.OP_ACCEPT);
        // 3. 绑定端口
        server.socket().bind(new InetSocketAddress(20480)); // 绑定端口
        System.out.println("Socket服务端-启动");

        ByteBuffer readBuff = ByteBuffer.allocate(1024);
        ByteBuffer writeBuff;

        while (true) {
            // 不再轮询通道,改用下面轮询事件的方式。select方法有阻塞效果,直到有事件通知才会有返回
            selector.select();
            // 获取事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍历查询结果e
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                // 被封装的查询结果
                SelectionKey key = iterator.next();
                // 关注 Accept 和 Read 两个事件
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    // 拿到客户端的连接通道,并且将SocketChannel的read事件注册绑定到selector选择器上
                    SocketChannel client = channel.accept(); // mainReactor 轮询accept
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("接收到客户端连接:" + client.getRemoteAddress());
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    try {
                        readBuff.clear();
                        while (client.isOpen() && client.read(readBuff) != -1) {
                            // 长连接情况下,需要手动判断数据有没有读取结束
                            // 此处做一个简单的判断:超过0字节就认为请求结束了
                            if (readBuff.position() > 0) break;
                        }
                        // 如果没数据了, 则不继续后面的处理
                        if (readBuff.position() == 0) continue;

                        readBuff.flip();
                        byte[] content = new byte[readBuff.limit()];
                        readBuff.get(content);
                        System.out.println("已收到:" + new String(content));
                        System.out.println("来自:" + client.getRemoteAddress());

                        // TODO 业务操作 数据库 接口调用等等

                        // 响应结果 200
                        String response = "HTTP/1.1 200 OK\r\n" +
                                "Content-Length: 11\r\n\r\n" +
                                "Hello World";
                        writeBuff = ByteBuffer.wrap(response.getBytes());
                        while (writeBuff.hasRemaining()) {
                            client.write(writeBuff);
                        }

                    } catch (IOException e) {
                        // e.printStackTrace();
                        key.cancel(); // 取消事件订阅
                    }
                }
                iterator.remove();
            }
            selector.selectNow();
        }
        //channel.close();
        //System.out.println("Socket服务端-关闭");
    }
}

NIO客户端测试代码

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * @Author: Wenx
 * @Description:
 * @Date: Created in 2019/11/19 13:26
 * @Modified By:
 */
public class NIOClient {
    public static void main(String[] args) throws IOException {
        // 客户端请求与本机在20480端口建立TCP连接
        SocketChannel client = SocketChannel.open();
        client.configureBlocking(false); // 设置为非阻塞模式
        client.connect(new InetSocketAddress("127.0.0.1", 20480));
        while (!client.finishConnect()) {
            // 没连接上,则一直等待
            Thread.yield();
        }
        System.out.println("Socket客户端-启动");

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入:");
        // 发送内容
        String msg = scanner.nextLine();
        ByteBuffer writeBuff = ByteBuffer.wrap(msg.getBytes());
        while (writeBuff.hasRemaining()) {
            client.write(writeBuff);
        }

        // 读取响应
        System.out.println("收到服务端响应:");
        ByteBuffer readBuff = ByteBuffer.allocate(1024);
        while (client.isOpen() && client.read(readBuff) != -1) {
            // 长连接情况下,需要手动判断数据有没有读取结束
            // 此处做一个简单的判断:超过0字节就认为请求结束了
            if (readBuff.position() > 0) break;
        }

        readBuff.flip();
        byte[] content = new byte[readBuff.limit()];
        readBuff.get(content);
        System.out.println("发送数据:" + new String(content));

        scanner.close();
        client.close();
    }
}

小结

NIO为开发者提供了功能丰富及强大的IO处理API,但是在应用于网络应用开发的过程中,直接使用 JDK提供的API,比较繁琐。而且想要将性能进行提升,光有NIO还不够,还需要将多线程技术与之结合起来。

因为网络编程本身的复杂性,以及 JDK API开发的使用难度较高,所以在开源社区中,涌出来很多对 JDK NIO进行封装、增强后的网络编程框架,例如:Netty、Mina等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值