与NIO的第一次亲密接触

前言

随着互联网的不断发展,用户群体越发庞大。
从互联网初入中国,国内的上网用户不过数万,而上网也只能简单的进行邮件,浏览新闻。到现在几乎每个人都可以通过互联网进行社交,娱乐,学习。中间不但有社会经济的发展,java技术也随之不断发展,究竟是科学技术的发展促使了社会的发展,还是社会经济的发展促使了科学技术?这个问题我想大家应该都知道一句话:科学技术是第一生产力。
随着用户全体的庞大,对于网络通信的要求越发高,BIO越发不适应需求,于是NIO应运而生。
NIO(New IO)又被业内称之为:Non Block IO,即非阻塞IO

一、传统的BIO编程

1、BIO通信模型图

在这里插入图片描述
BIO通信的服务器,对于每一个客户端的连接,都会使用一个独立的Acceptor线程,来对客户端进行连接。
即,他收到客户端请求后,会为每一个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,最后线程销毁。这就是典型的一请求一应答的通信模型。
线程是JVM非常宝贵的资源,当线程数过大后,随着并发访问数的继续增大,系统将会产生堆栈移除,创建线程失败等事故。最终导致服务器宕机或僵死,无法对外提供服务。

2、BIO的Server源码分析

package com.xyp.iodemo.nio.server;


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-28 18:13
 */
public class TimeServer {
    public static Logger XLogUtils = Logger.getLogger("TimeServer");

    public static void main(String [] args) throws IOException {
        Integer port=20022;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }
        ServerSocket serverSocket=null;
        try {
            serverSocket=new ServerSocket(port);
            XLogUtils.info("The TimeServer is start in port:"+port);

            Socket socket=null;
            while (true){
                socket=serverSocket.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }

        }finally {
            if(serverSocket!=null){
                XLogUtils.info("The Time is Close");
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                serverSocket=null;
            }
            XLogUtils.info("END>>>>");
        }
    }
}

TimeServer是一个很简单的类,它负责创建ServerSocket,然后对客户端的请求进行接受,这里值得注意的一点就是,这里的

serverSocket.accept()

是阻塞的,即sever等待客户端请求接入,如果接入就创建线程处理请求,创建完毕后,继续等待下一个客户端的接入。

然后我们再来一起看一下具体针对线程的处理。

package com.xyp.iodemo.nio.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-28 19:13
 */
public class TimeServerHandler implements Runnable {
    private  Socket socket;
    public static Logger logger = Logger.getLogger("TimeServerHandler");

    public TimeServerHandler(Socket socket){
        this.socket=socket;
        logger.info("TimeServerHandler 构造函数");
    }

    @Override
    public void run() {
        BufferedReader in =null;
        PrintWriter out=null;
        try {
            in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out=new PrintWriter(socket.getOutputStream(),true);
            String currentTime=null;
            String body=null;
            logger.info("TimeServerHandler run");
            while (true){
                logger.info("TimeServerHandler wait input");
                body=in.readLine();
                logger.info("TimeServerHandler wait finish:"+String.valueOf(body));
                if(body== null){
                    break;
                }
                logger.info("The time server receive order : "+body);
                currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?new java.util.Date(
                        System.currentTimeMillis()
                ).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(socket!=null){
                try {
                    socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }finally {
                    socket=null;
                }
            }
        }
    }
}

3、Client的源码分析

package com.xyp.iodemo.bio.client;

import java.io.*;
import java.net.Socket;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-01 18:21
 */
public class TimeClient {
    public static Logger logger = Logger.getLogger("TimeClient");

    public static void main(String [] args) throws IOException {
        Integer port=20022;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }

        Socket socket=null;
        BufferedReader in =null;
        PrintWriter out = null;
        try {
            socket = new Socket("127.0.0.1", port);
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));//用户获取用户输入

            OutputStream os = socket.getOutputStream();//用于向服务器输出
            System.out.println("请输入要发送的文字:");
            String input;
            //等待cons
            while ((input= reader.readLine()) != null) {
                input = input+"\n";//手动加上回车
                os.write(input.getBytes("utf-8"));
                logger.info("向服务的写入:"+input.toString());
            }

        }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 e2){
                    e2.printStackTrace();
                }
                socket=null;
            }
        }

    }
}

等待控制台的输入,按下回车键,就会向服务器写入字节。

4、运行结果

(1)启动Server的console

四月 08, 2019 4:25:41 下午 com.xyp.iodemo.nio.server.TimeServer main
信息: The TimeServer is start in port:20022

(2)启动Client的console,在其中输入了 111后,按下回车

请输入要发送的文字:
111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.bio.client.TimeClient main
信息: 向服务的写入:111

(3)服务器响应输出。输出是分步的
当client启动后,输出

四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler
信息: TimeServerHandler 构造函数
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler run
四月 08, 2019 4:27:30 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait input

当客户端按下回车键后

四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait finish:111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: The time server receive order : 111
四月 08, 2019 4:27:40 下午 com.xyp.iodemo.nio.server.TimeServerHandler run
信息: TimeServerHandler wait input

二、NIO基础概念

1、缓冲区Buffer

Buffer是一个对象,他包含了一些要写入的或者要读出的数据。
在NIO中加入Buffer对象,体现了新库与原始IO的一个重大区别,即,在面流的IO中,可以直接将数据写入或者直接将数据读到steam对象中。
Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

2、通道Channel

Channel是一个通道,可以通过它进行数据的读取和写入。如同自来水管道,网络数据在channel中可以进行双向的流通,这点不同于steam,steam是单向的,即steam要么是InputSteam,要么是OutputSteam。
而通道可以用于读、写、或同时进行读写。

3、多路复用器 Selector

多路复用器,提供平选择已就绪任务的能力。
简单来说,selector会不断轮询注册在其上的Channel,如果某个Channel上有新的Tcp接入,或者有发生读写事件,这个Channel就会处于就绪状态,可以被Selector轮询出来,然后通过selectedKey获取就绪的Channel集合,以便进行后续的IO操作。

selector.selectedKeys() 

一个多路复用器可以同时轮询多个Channel。

4、NIO的Server的时序图

在这里插入图片描述

5、NIO的Server源码分析

(1)TimeServer

此类的作用主要是声明端口,以及启动线程

package com.xyp.iodemo.nio.server;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 11:05
 */
public class TimeServer {
    public static void main(String [] args){
        Integer port=8085;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }
        MultiplexerTimeServer timeServer=new MultiplexerTimeServer(port);
        new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
    }
}
(2)MultiplexerTimeServer
package com.xyp.iodemo.nio.server;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 11:11
 */
public class MultiplexerTimeServer implements Runnable{
    public static Logger logger = Logger.getLogger("MultiplexerTimeServer");
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile Boolean stop;

    public MultiplexerTimeServer(Integer port){
        stop=false;
        try {
            //创建多路复用器
            selector=Selector.open();
            //打开ServerSocketChannel,用于监听客户端连接,他是所有的客户端连接的父管道
            serverSocketChannel=ServerSocketChannel.open();
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //绑定监听端口
            serverSocketChannel.socket().bind(new InetSocketAddress(port),1024);
            //将serverSocketChannel注册到多路复用器,监听ACCEPT事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            logger.info("The Time server is start in port: " +port);
        }catch (IOException e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop(){
        this.stop=true;
    }

    @Override
    public void run() {
        while (!stop){
            try {
                //设置休眠时间为1s,无论是否有读写事件发生,selector每1s都被唤醒一次
                selector.select(1000);
//                logger.info("selector被唤醒>>>>>>>");
                //多路复用器无线循环获取准备就绪的Key
                Set<SelectionKey> keys=selector.selectedKeys();
//                logger.info("keys len>>>>>>>"+keys.size());

                Iterator<SelectionKey> it=keys.iterator();
                SelectionKey key=null;

                while (it.hasNext()){
                    key=it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (Exception e){
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Throwable e){
                e.printStackTrace();
            }
        }

        if(selector!=null){
            try {
                selector.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }


    private void handleInput(SelectionKey key) throws IOException{
        if(key.isValid()){
            if(key.isAcceptable()){
                //多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP的三次握手
                //建立物理连接
                ServerSocketChannel ssc= (ServerSocketChannel) key.channel();
                logger.info("准备监听客户端连接");
                SelectableChannel sc=ssc.accept();
                logger.info("设置监听完毕");
                //设置客户端链路,为非阻塞模式
                sc.configureBlocking(false);
                sc.register(selector,SelectionKey.OP_READ);
            }
            if(key.isReadable()){
                logger.info("key.isReadable");

                SocketChannel sc= (SocketChannel) key.channel();
                ByteBuffer byteBuffe=ByteBuffer.allocate(1024);
                //异步读取客户端请求消息到缓存区
                logger.info("异步读取客户端请求消息到缓存区>>"+byteBuffe.toString());
                int readBytes=sc.read(byteBuffe);
                if(readBytes>0){
                    //将缓冲区的当前limit设置为postion,position设置为0,用于后续对缓冲区的读取操作
                    byteBuffe.flip();
                    byte [] bytes=new byte[byteBuffe.remaining()];
                    byteBuffe.get(bytes);
                    //对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文
                    //将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排
                    String body=new String(bytes,"UTF-8");

                    logger.info("The Time server receive order : "+body);

                    String currentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?
                            new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
                    //调用异步write接口,将消息异步发送到客户端
                    doWrite(sc,currentTime);
                }else if(readBytes<0){
                    key.channel();
                    sc.close();
                }else {

                }
            }
        }
    }

    private void doWrite(SocketChannel channel,String response) throws IOException {
        if(response!=null&&response.trim().length()>0){
            byte [] bytes=response.getBytes();
            ByteBuffer writeBuffer=ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}

6、NIO的Client

1、NIO的Client的时序图

在这里插入图片描述

2、Client源码分析

(1)TimeClient
主要作用是声明端口,启动线程。

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 16:59
 */
public class TimeClient {
    public static void main(String [] args){
        Integer port=8085;
        if(args!=null&&args.length>0){
            try {
                port=Integer.valueOf(args[0]);
            }catch (Exception e){

            }
        }
        TimeHandle timeServer=new TimeHandle("127.0.0.1",port);
        new Thread(timeServer,"NIO-TimeHandle-001").start();
    }
}

(2)TimeHandle


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-02 17:01
 */
public class TimeHandle implements Runnable{
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile Boolean stop;
    public static Logger logger = Logger.getLogger("TimeHandle");

    public TimeHandle(String host,int port){
        this.host=host==null?"127.0.0.1":host;
        this.port=port;
        this.stop=false;
        try {
            selector=Selector.open();
            socketChannel=SocketChannel.open();
            socketChannel.configureBlocking(false);
        }catch (Exception e){
            e.printStackTrace();
            System.exit(1);
        }
        logger.info("TimeHandle构造完毕");
    }

    @Override
    public void run() {
        try {
            logger.info("run >> 开始连接");
            doConnect();
            logger.info("run >> 连接完毕");
        }catch (Exception e){
            e.printStackTrace();
            System.exit(1);
        }
        while (!stop){
            try {
                selector.select(1000);
//                logger.info("selector被唤醒>>>>>>>");
                Set<SelectionKey> selectionKeys=selector.selectedKeys();
//                logger.info("keys len>>>>>>>"+selectionKeys.size());
                Iterator<SelectionKey> it=selectionKeys.iterator();
                SelectionKey key=null;
                while (it.hasNext()){
                    key=it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    }catch (Exception e){
                        if(key!=null){
                            key.cancel();
                            if(key.channel()!=null){
                                key.channel().close();
                            }
                        }
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
                System.exit(1);
            }
        }

        if(selector!=null){
            try {
                selector.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

    }


    private void handleInput(SelectionKey key) throws IOException {
//        logger.info("handleInput>>");
        if(key.isValid()){
//            logger.info("key.isValid>>");

            SocketChannel sc= (SocketChannel) key.channel();
            //测试此键的通道是否已完成其套接字连接操作。
            if(key.isConnectable()){
                logger.info("sc.isConnected>>");
                if(sc.finishConnect()){
                    logger.info("sc.finishConnect>>");

                    sc.register(selector,SelectionKey.OP_READ);
                    doWrite(sc);
                }else {
                    logger.info("Link fail exit...");
                    System.exit(1);
                }

                if(key.isReadable()){
                    ByteBuffer readBuffer=ByteBuffer.allocate(1024);
                    int readBytes=sc.read(readBuffer);
                    if(readBytes>0){
                        readBuffer.flip();
                        byte [] bytes=new byte[readBuffer.remaining()];
                        readBuffer.get(bytes);
                        String body=new String(bytes,"UTF-8");
                        System.out.println("Now is : "+body);
                        this.stop=true;
                    }else if(readBytes<0){
                        key.cancel();
                        sc.close();
                    }else {
                        //ignore
                    }
                }
            }
        }



    }


    private void doConnect() throws IOException {
        logger.info("连接>>"+host+">>"+port);
        if(socketChannel.connect(new InetSocketAddress(host,port))){
            socketChannel.register(selector,SelectionKey.OP_READ);
            doWrite(socketChannel);
        }else {
            socketChannel.register(selector,SelectionKey.OP_CONNECT);
        }
    }


    private void doWrite(SocketChannel channel) throws IOException {
        logger.info("doWrite>>>>");
        byte [] bytes="Hi Service O_O".getBytes();
        ByteBuffer writeBuffer=ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        channel.write(writeBuffer);
        if(!writeBuffer.hasRemaining()){
            logger.info("Send order 2 server succeed.");
        }
    }
}
3、运行结果

(1)启动服务器

四月 08, 2019 5:09:25 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer
信息: The Time server is start in port: 8085

(2)启动客户端

四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle
信息: TimeHandle构造完毕
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle run
信息: run >> 开始连接
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doConnect
信息: 连接>>127.0.0.1>>8085
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle run
信息: run >> 连接完毕
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle handleInput
信息: sc.isConnected>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle handleInput
信息: sc.finishConnect>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doWrite
信息: doWrite>>>>
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.client.TimeHandle doWrite
信息: Send order 2 server succeed.

(3)服务器响应

四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 准备监听客户端连接
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 设置监听完毕
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: key.isReadable
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: 异步读取客户端请求消息到缓存区>>java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
四月 08, 2019 5:10:19 下午 com.xyp.iodemo.nio.server.MultiplexerTimeServer handleInput
信息: The Time server receive order : Hi Service O_O

以上就是NIO的运行流程。

参考文档:
《Netty权威指南》

猜你喜欢

转载自blog.csdn.net/m13797378901/article/details/88977996