netty(一) java NIO

关于java,其网络通信方面性能可以说在慢慢挖掘,而且是现今被挖掘出来有效率最高的一块了,不止2017年,最近这两年开始,你会发现,java很多组件都采用了NIO的方式,因为其性能确实可以带来很大提高,从阿里dubbo的广泛应用netty,到tomcat8以后的NIO通讯,作为一个老程序员,我们发现一定要深入研究java的NIO了,它现在成了java各种应用级开发的核心功能。

在jdk1.4之前,java是没有NIO的,所有的socket都采用了同步阻塞模式,这以请求一应答的模式简化了java开发,但在性能和可靠性上却存在着巨大瓶颈。在jdk1.4中增加了个java.nio包,提供了很多进行异步I/O开发的API和类库,主要的类和接口如下:

  • 进行异步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。主要提供了以下三个方面的改进。
  • 提供能够批量获取文件属性的API,这些API具有平台无关性,不与特性的文件系统相耦合。另外它还提供了标准文件系统的SPI,供各个服务提供商扩展实现;
  • 提供AIO功能,支持基于文件的异步i/O操作和针对网络套接字的异步操作;

传统的BIO编程是可以

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

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
下面我们就以经典的时间服务器(TimeServer)为例,通过代码分析来回顾和熟悉BIO编程。

BIO通信模型图

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

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

同步阻塞式I/O创建的TimerServer源码分析

public class TimerServer {
    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();
                new Thread(new TimeServerHandler(socket)).start();
            }
        }finally {
            if (server != null) {
                System.out.println("The time server close");
                server.close();
                server = null;
            }
        }
    }
}
TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080.通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。之后通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。
当有新的线程接入的时候,执行代码new Thread,以Socket为参数构造TimeServerHandler对象,TimeServerHandler是一个Runnable,使用它为构造函数的参数创建一个新的客户端线程处理Socket链路。下面再看下TimeServerHandler的代码:
public class TimeServerHandler implements Runnable {

    private Socket socket;

    public TimeServerHandler() {

    }

    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 Date(System.currentTimeMillis()).toString() : "BAD ORDER";
                out.println(currentTime);
            }
        } catch (Exception e) {
            
        }finally{
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (out != null) {
                out.close();
                out = null;
            }
            if (this.socket != null) {
                try {
                    this.socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}


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

下面我们会介绍同步阻塞的客户端代码,然后分别运行服务端和客户端,看下程序的运行结果。

同步阻塞式IO创建的TimeClient源码分析

客户端通过Socket创建,发送查询时间服务器的“QUERY TIME ORDER”命令,然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。
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);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("QUERY TIME ORDER");
            System.out.println("Send order 2 server succeed.");
            String resp = in.readLine();
            System.out.println("Now is:" + resp);
        } catch (Exception e) {

        } finally {
            if (out != null) {
                out.close();
                out = null;
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                in = null;
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                socket = null;
            }
        }
    }
}
分别执行服务端和客户端,执行结果如下。
客户端执行结果如下:

到此为止,同步阻塞IO开发的时间服务器程序已经讲解完毕。我们发现,BIO主要的问题在于每当有一个新的客户端接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器领域,往往需要面对成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。

为了改进一线程一连接的模型,后来又演进出了一种通过线程池或者消息队列实现一个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞IO,所以被成为“伪异步”。下篇博文我们就对伪异步代码进行分析,看看伪异步是否能够满足我们对高性能、高并发接入的诉求。


没有更多推荐了,返回首页