Netty 之传统 BIO、NIO 编程

说明

系列文章:http://www.jianshu.com/p/594441fb9c9e

本文完全参考自《Netty权威指南(第2版)》,李林峰著。

传统 BIO 编程是什么样的?

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

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

BIO 通信模型

对于每个客户端,服务端都要新建一个线程。

当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数量呈1:1的关系,当线程数膨胀后,系统的性能将急剧下降。

示例

服务端在接收到字符串QUERY TIME ORDER后,返回当前日期给客户端。

TimeServer 源码

public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8080;
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("The time server is start in port : " + port);
            Socket socket = null;
            while (true) {
                socket = serverSocket.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }
        } finally {
            if (serverSocket != null) {
                System.out.println("The time server close");
                serverSocket.close();
                serverSocket = null;
            }
        }
    }
}
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 Date().toString() : "BAD ORDER";
                out.println(currentTime);
            }
        } catch (Exception e) {
            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;
            }
        }
    }
}

TimeClient 源码

public class TimeClient {
    public static void main(String[] args) throws Exception {
        int port = 8080;
        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 TO SERVER SUCCESSED");
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
        } catch (Exception e) {
            // TODO: handle exception
        } finally {
            if (out != null) {
                out.close();
                out = null;
            }
            if (in != null) {
                in.close();
                in = null;
            }
            if (socket != null) {
                socket.close();
                socket = null;
            }
        }
    }
}

输出结果

The time server is start in port : 8080
The time server receive order : QUERY TIME ORDER
The time server receive order : QUERY TIME ORDER

线程堆栈查看

通过jstack命令,查看当前堆栈信息:

"main" #1 prio=5 os_prio=31 tid=0x00007ffed1805000 nid=0x1c03 runnable [0x0000700007de7000]
   java.lang.Thread.State: RUNNABLE
    at java.net.PlainSocketImpl.socketAccept(Native Method)
    at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
    at java.net.ServerSocket.implAccept(ServerSocket.java:545)
    at java.net.ServerSocket.accept(ServerSocket.java:513)
    at bio.TimeServer.main(TimeServer.java:18)

我们可以看到,代码确实阻塞在accept操作上。

伪异步 I/O 编程

同步阻塞I/O当海量并发接入时,会导致线程耗尽。可以对线程模型进行优化:后端通过一个线程池来处理多个客户端的请求接入。

但是伪异步I/O也存在很多问题,因为它的本质仍然是同步阻塞。这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程会被长时间阻塞;在此期间,其他接入消息只能在消息队列中排队。

NIO 编程

首先问自己一个问题,什么才是NIO编程?对于NIO,官方的说法是:New I/O;但是更多的人喜欢称之为Non-block I/O(非阻塞I/O)。

BIO中的Socket类和ServerSocket类,对应于NIO中的SocketChannel和ServerSocketChannel两种不同的套接字通道实现,这两种套接字都支持阻塞和非阻塞两种模式。

  • 阻塞模式:使用非常简单,但是性能和可靠性都不好;
  • 非阻塞模式:使用复杂,性能和可靠性好。

NIO 类库简介

缓冲区 Buffer

在面向流的I/O中,可以直接读取或写入数据至Stream对象中;在NIO中,所有数据都是通过缓冲区处理的:读取数据时,直接读到缓冲区;写入数据时,写入到缓冲区。任何时候访问NIO中的数据,都是通过缓冲区进行的。

通道 Channel

传统的只有一个方向(InputStream或者OutputStream),而通道可以用于读、写或二者同时进行。

多路复用器 Selector

提供选择已经就绪的任务的能力。Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读写事件,这个Channel处于就绪状态,会被Selector轮询处理,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。


对于NIO的编程十分繁琐,就不作介绍,在接下来的文章中直接使用Netty开发。

推荐阅读更多精彩内容