Java网络编程(BIO和NIO)

BIO、NIO

本文参考自《Netty权威指南》、《Netty实战》,主要对JDK的BIO、NIO和JDK1.7 最新提供的NIO 2.0的使用进行详细说明。

1、传统的同步阻塞式I/O编程

2、基于NIO的非阻塞编程

3、基于NIO2.0异步非阻塞(AIO)编程

4、为什么使用NIO编程

5、为什么选择Netty

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

一、传统的BIO编程

1.1BIO通信模型图

Java网络编程(BIO和NIO)_第1张图片

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

Java网络编程(BIO和NIO)_第2张图片

​ 该模型最大的问题缺乏弹性伸缩能力,当客户端并发量增加后,服务端的线程个数和客户端并发访问数一致,线程对于JVM是宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题。

    public final static int targetPort=9001;
    public static void main(String[] args) throws IOException {
        //实现服务器和客户端通信
        //1、创建服务器端通信的ServerSocket对象,用以监听指定端口上的连接请求
        ServerSocket serverSocket=new ServerSocket(targetPort);
        //2、对accept()用于侦听并接收此ServerSocket的连接,方法的调用将被阻塞,直到一个连接的建立
           for (; ; ) {
            //阻塞式接收客户端套接字
            final Socket socket = serverSocket.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    OutputStream out = null;

                    try {
                        out = socket.getOutputStream();
                        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(out));
                        bw.write("hell0");
                        bw.newLine();
                        bw.flush();
                        socket.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    } finally {
                        try {
                            out.close();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }

                }
                }).start();

            }

    }

​ 对此我们发线,BIO主要问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。显然无法满足高性能、高并发接入的场景。

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

1.2 伪异步I/O编程

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,对其模型做了优化:通过一个线程池来处理多个客户端的请求接入,线程池的最大线程数可以远大于客户端数。通过线程池来灵活调配线程资源。

伪装异步通信模型

Java网络编程(BIO和NIO)_第3张图片

将客户端的Socket封装成为一个Task(实现Runnable接口)提交到线程池进行处理。通过设置线程池的max Thread、阻塞队列来调控

异步任务执行逻辑

public class TimeServerHandler implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        BufferedReader br=null;
        PrintWriter out=null;
        try{
            br=new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out=new PrintWriter(socket.getOutputStream(),true);
            String currentTime=null;
            String body=null;
            for(;;){
                body=br.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) {
            if(br!=null){
                try {
                    br.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }

            if(out!=null){
                out.close();
                out=null;
            }
            if(this.socket!=null){
                try {
                    this.socket.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                this.socket=null;
            }
        }
    }
}

客户端

public class TimeClient {
    public static void main(String[] args) throws IOException {
        Socket socket=new Socket("127.0.0.1",8080);
//        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//        bw.write("QUERY TIME ORDER");
//        bw.newLine();
//        bw.flush();
        PrintWriter pw=new PrintWriter(socket.getOutputStream(),true);
        pw.println("QUERY TIME ORDER");

        BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String s = br.readLine();
        System.out.println("Now is :"+s);
    }
}

服务端接收连接,启动线程驱动

public class TimeServer {
    public static void main(String[] args) {
        int port=8080;
        ServerSocket server=null;
        try{
            server=new ServerSocket(port);
            Socket socket=null;
            //创建I/O任务线程池
            TimeServerHandlerExecutePool singleExecutor=new TimeServerHandlerExecutePool(50,10000);
            for(;;){
                socket=server.accept();
                singleExecutor.execute(new TimeServerHandler(socket));
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

线程池

public class TimeServerHandlerExecutePool {
    private ExecutorService executor;

    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize){
        executor=new ThreadPoolExecutor(Runtime.getRuntime()
                .availableProcessors(),maxPoolSize,120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize));
    }

    public void execute(Runnable task){
        executor.execute(task);
    }
}

Java网络编程(BIO和NIO)_第4张图片

Java网络编程(BIO和NIO)_第5张图片

伪异步I/O弊端分析
  • Socket的输入流进行读取操作时,会一直阻塞,直到:
    • 有数据可读
    • 可用数据读取完毕
    • 发生控制着或I/O异常
  • 调用OutputStream的write方法写输出流时,也会被阻塞,直到所有要发送的字节全部写入完毕,或者方式一场

二、NIO编程

​ 与Socket类和ServerSocket类相对应,NIO提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种通道都支持阻塞和 非阻塞两种模式。对于阻塞模式,性能和可靠性不好。但非阻塞模式恰好相反。

一般,低负载、低开发的应用程序选择BIO降低编程复杂度;对于高负载、高并发的网络应用,使用NIO的非阻塞模式进行开发

2.1 NIO类库

​ NIO是JDK1.4引入。弥补了原来BIO的不足。

1、缓冲区Buffer

Java网络编程(BIO和NIO)_第6张图片

​ Buffer继承关系

​ Buffer是一个对象,包含一些要写入或者读出的数据。NIO引入了Buffer对象,为了区别于BIO。在面向流的I/O中,可以将数据直接写入或将数据读到Stream对象中。

​ NIO库中,所有数据都是用缓冲区处理的,在读取数据时,直接读到缓冲区;写数据时,写入到缓冲区。任何时候访问NIO的数据,都是通过缓冲区进行操作

​ 缓冲区本质是一个数组,通常是一个字节数组,也可使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置等信息

​ 如上图,Java的所有基本数据类型都对应一种缓冲区,每一个Buffer类都是Buffer接口的一个子实例。大多数标准I/O使用ByteBuffer,所以ByteBuffer具有一般缓冲区操作之外的特有操作,方便网络读写。

2、Channel通道

​ 网络数据通过Channel读取和写入。通道和流不同之处在于通道是双向的,而流是单向的,通道可以用于读、写或者二者同时进行(Channel具有全双工)。

​ Channel也分两大类:用于网络读写的SelectableChannel和用于文件读写的FileChannel

Java网络编程(BIO和NIO)_第7张图片

3、多路复用器Selector

​ 多路复用器提供选择已经就绪的任务的能力。简单来说,Selector会不断地沦陷注册在其上的Channel(一个多路复用器Selector可以同时轮询多个Channel),如果某个Channel上面发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey获取就绪Channel的集合,进行后序的I/O操作。

NIO服务端序列图

Java网络编程(BIO和NIO)_第8张图片

public class PlainNioServer {
    public void server(int port) throws IOException {
        //1、开启ServerSocketChannel,用于监听客户端的连接,是所有客户端连接的父管道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        //2、设置连接为非阻塞,绑定监听端口
        serverChannel.configureBlocking(false);
        ServerSocket ssocket = serverChannel.socket();
        //将服务器绑定到相应的端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress(port);
        ssocket.bind(inetSocketAddress);

        //3、打开Selector处理Channel
        Selector selector = Selector.open();
        //4、将ServerSocket注册到Selector以接受连接
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        final ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes());
        for(;;){
            try{
                selector.select();
            }catch (IOException e){
                e.printStackTrace();
                break;
            }
//          5、获取所有接收事件的SelectionKey实例(即就绪的Channel集合)
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = readyKeys.iterator();
//          6、多路复用器 轮询 准备就绪的Key
            while(iterator.hasNext()){
                SelectionKey key=iterator.next();
                iterator.remove();
                try{
                    //检查事件是否是一个新的已经就绪可以被接受的连接
                    if(key.isAcceptable()){
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        //多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
                        SocketChannel client = server.accept();
                        //设置客户端链路为非阻塞
                        client.configureBlocking(false);
                        //将新接入的客户端连接 注册到 多路复用器上,监听读写操作
                        client.register(selector, SelectionKey.OP_WRITE |
                                SelectionKey.OP_READ,wrap.duplicate());
                        System.out.println("Accepted connection from"+client);
                    }
//                  检查套接字是否已经准备好写数据
                    if(key.isWritable()){
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        //将数据写到已连接的客户端
                        while (buffer.hasRemaining()) {
                            if (client.write(buffer) == 0) {
                                break;
                            } }
                        client.close();
                    }
                }catch (IOException e){
                    key.cancel();
                    try{
                        key.channel().close();
                    }catch (Exception ce){
                        ce.printStackTrace();
                    }
                }
            }

        }
    }
}


你可能感兴趣的:(计算机网络,网络,nio)