Java NIO学习笔记 - BIO编程

Java I/O演进

在JDK1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种——请求——应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O(AIO)能力。当并发访问量增大,响应时间延迟增大之后,采用Java BIO并发的服务端软件只有通过硬件的不断扩容来满足高并发和低延时,它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。

正是由于Java传统BIO的拙劣表现,才使得Java支持非阻塞I/O的呼的呼声日渐高涨,最终,JDK1.4版本提供了新的NIO类库(new input/output),Java终于也可以支持非阻塞I/O了。

Java I/O 发展简史

从JDK1.0到JDK1.3,Java的I/O类库都非常原始,很多UNIX网络编程中的概念或者接口在I/O类库中都没有体现,例如:Pipe,Channel,Buffer和Selector等。2002年发布JDK1.4时,NIO以JSR-51的身份正式随JDK发布。NIO主要的类和接口如下:

  • 进行异步I/O操作的缓冲区ByteBuffer等;
  • 进行异步I/O操作的管道Pipe;
  • 进行各种I/O操作(异步或者同步)的Channel,包括ServerSocketChannel和SocketChannel;
  • 多种字符集的编码能力和解码能力;
  • 实现非阻塞I/O操作的多路复用器Selector;
  • 基于流行的Perl实现的正则表达式类库;
  • 文件通道FileChannel;

新的NIO类库的提供,极大地促进了基于Java的异步非阻塞编程的发展和应用,但是,它依然有不完善的地方,特别是对文件系统的处理能力仍显不足,主要问题如下:

  • 没有统一的文件属性(例如读写权限)
  • API能力比较弱,例如目录的级联创建和递归遍历,往往需要自己实现。
  • 底层存储系统的一些高级API无法使用。
  • 所有的文件操作都是同步阻塞调用,不支持异步文件读写操作。

2011年7月28日,JDK1.7正式发布。它的一个比较大的亮点就是将原来的NIO类库进行了升级,被称为NIO2.0。NIO2.0由JSR-203演进而来,它主要提供了如下三个方面的改进:

  • 提供能够批量获取文件属性的API,这些API具有平台无关性,不与特性的文件系统相耦合,另外它还提供了标准文件系统的SPI,供各个服务提供商扩展实现
  • 提供AIO功能,支持基于文件的异步I/O操作和针对网络套接字的异步操作
  • 完成JSR-51定义的通道功能,包括对配置和多播数据报的支持等。

传统BIO编程

网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务器监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口; Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

BIO通信模型

BIO的服务端通信模型:

Java NIO学习笔记 - BIO编程_第1张图片
bio-model.png

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型。

该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出,创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

BIO 简单示例

Server 端

同步阻塞IO的TimeServer:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TimeServer {
    public static void main(String[] args) throws IOException{
        int port = 8080;
        if(args != null && args.length > 0){
            try{
                port = Integer.valueOf(args[0]);
            }catch (NumberFormatException e){
                //采用默认值
            }
        }
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("The time server is start in port : " + port);
            Socket socket = null;
            while(true) {
                socket = server.accept();    //接收客户端连接请求,没有的时候就阻塞
                System.out.println("收到来自客户端的请求");
                new Thread(new TimeServerHandler(socket)).start();
            }
        } finally{
            if(server != null) {
                System.out.println("The time server close");
                server.close();
            }
        }
    }
}

TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080,20行通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。23-26行通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。启动TimeServer,通过JvisualVM打印线程堆栈,我们可以发现主程序确实阻塞在accept操作上

当有新的客户端接入的时候,执行代码25行,以Socket为参数构造TimeServerHandler对象,TimeServerHandler是一个Runnable,使用它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。下面我们继续分析TimeServerHandler的代码。

同步阻塞IO的TimeServerHandler:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TimeServerHandler implements Runnable {
    private Socket socket;
    public TimeServerHandler(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(),true);
            String currentTime = null;
            String body = null;
            while(true) {
                body = in.readLine();        //从客户端读
                if(body == null) break;
                System.out.println("The time server receive order: " + body);
                currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
                        new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
                out.print(currentTime);    //向客户端写
            }
        } catch (Exception e) {
            e.printStackTrace();
            if(in != null){
                try {
                    in.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
            if(out != null) {
                out.close();
            }
            if(this.socket != null) {
                try {
                    this.socket.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}

25行通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到了非空值,则对内容进行判断,如果请求消息为查询时间的指令”QUERY TIME ORDER”则获取当前最新的系统时间,通过PrintWriter的println函数发送给客户端,最后退出循环。代码35-52行释放输入流、输出流、和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。

Client端

客户端通过Socket创建,发送查询时间服务器的”QUERY TIME ORDER”指令,然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。

同步阻塞IO的TimeClient:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        if(args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);
            } catch (NumberFormatException e) {
                //采用默认值
            }
        }
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket("127.0.0.1",port);    //创建socket
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(),true);
            out.println("QUERY TIME ORDER"); //向服务端写出QUERY TIME ORDER
            System.out.println("Send order 2 server succeed.");
            String resp = in.readLine(); //从服务端读入
            System.out.println("Now is : " +  resp);
        } catch (Exception e) {
            e.printStackTrace();
            //不需要处理
        } finally {
            if(out != null ){
                out.close();
            }
            if(in != null){
                try {
                    in.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

第23行客户端通过PrintWriter向服务端发送”QUERY TIME ORDER”指令,然后通过BufferedReader的readLine读取响应并打印。

BIO潜在的问题

BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能,高并发接入的场景。

为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”,下一节会讲到。

你可能感兴趣的:(Java NIO学习笔记 - BIO编程)