NIO初识

转载自:http://blog.youkuaiyun.com/ns_code/article/details/15460405
    http://blog.youkuaiyun.com/ns_code/article/details/15378417
    http://blog.youkuaiyun.com/ns_code/article/details/15545057

  
  Java NIO (New IO) 是一个可供选择的 Java API (从Java 1.4引入),它可以替代标准的java IO API。它提供了一种与标准IO不同的工作方式。
  在标准的IO Socket编程中,是通过字符流和字节流进行操作,它不能前后移动流中的数据;而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,需要时可以在缓冲区中前后移动所保存的数据。

  在标准IO的Socket编程中,套接字的某些操作可能会造成阻塞:accept()方法的调用可能会因为等待一个客户端连接而阻塞,read()方法也可能会因为没有数据可读而阻塞,write()方法在数据没有完全写入时也可能会发生阻塞,阻塞发生时,该线程被挂起,什么也干不了。NIO则具有非阻塞的特性,可以通过对channel的阻塞行为的配置,实现非阻塞式的信道。在非阻塞情况下,线程在等待连接,写数据等(标准IO中的阻塞操作)的同时,也可以做其他事情,这便实现了线程的异步操作。

  非阻塞式网络IO的特点:1)把整个过程切换成小的任务,通过任务间协作完成;2)由一个专门的线程来处理所有的 IO 事件,并负责分发;3)事件驱动机制:事件到的时候触发,而不是同步的去监视事件;4)线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换。


  
  NIO包(java.nio.*)引入了四个关键的抽象数据类型:
  1、 Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
  2、 Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
  3、 Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。
  4、 Selector:它将多元异步I/O操作集中到一个或多个线程中。

  
  考虑一个即时消息服务器,可能有上千个客户端同时连接到服务器,但是在任何时刻只有非常少量的消息需要读取和分发,这就需要一种方法能阻塞等待,直到有一个信道可以进行I/O操作。NIO的Selector选择器就实现了这样的功能,一个 Selector实例可以同时检查一组信道的I/O状态,它就类似一个观察者,只要把需要探知的SocketChannel告诉Selector,接着做别的事情,当有事件(比如,连接打开、数据到达等)发生时,它会通知我们,传回一组SelectionKey,读取这些Key,就会获得刚刚注册过的SocketChannel,然后从这个 Channel中读取数据,接着可以处理这些数据。
  Selector内部原理实际是在做一个对所注册的Channel的轮询访问,不断的轮询,一旦轮询到一个Channel有所注册的事情发生,比如数据来了,它就会读取Channel中的数据,并对其进行处理。一般在一个单独的线程中,通过调用select()方法,就能检查多个信道是否准备好进行I/O操作,由于非阻塞I/O的异步特性,在检查的同时,也可以执行其他任务。

  基于NIO的TCP通信Demo之服务端:

public class NIOTCPServer implements Runnable {

    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private volatile boolean stop = false;

    public static void main(String[] args) throws Exception {
        new Thread(new NIOTCPServer()).start();
    }

    public NIOTCPServer() {
        try {
            selector = Selector.open();  // 创建一个选择器
            serverSocketChannel = ServerSocketChannel.open(); // 创建一个信道
            serverSocketChannel.configureBlocking(false);// 配置信道为非阻塞模式

            // 将该信道绑定到指定端口
            serverSocketChannel.socket()
                    .bind(new InetSocketAddress(9000));

            // 将选择器注册到信道
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("set SelectionKey.OP_ACCEPT on " + serverSocketChannel);

            System.out.println("Server is start...");

            /*
             * telnet 127.0.0.1 9000
             */
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                //无论有无读写事件发生,selector每隔1s都唤醒一次
                //也可以使用无参的select方法,一直阻塞到有事件发生
                int selected = selector.select(3000);
                System.out.println("Selected=" + selected
                        + ", selector.selectedKeys().size()=" + selector.selectedKeys().size());
                // 获取准备好的信道所关联的Key集合的iterator实例
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                handleSelectionKeys(keyIterator);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (!key.isValid()) {
            return;
        }
        //处理新接入的请求消息
        if (key.isAcceptable()) {
            handleAccept(key);
        }
        if (key.isReadable()) {
            handleRead(key);
        }
    }

    private void doWrite(SocketChannel sc, String msg) throws IOException {
        byte[] bytes = msg.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);//将字节数组复制到缓冲区
        writeBuffer.flip();
        sc.write(writeBuffer);
    }

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


    // 服务端信道已经准备好了接收新的客户端连接
    public void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel socketChannel = ssc.accept();
        if (socketChannel == null) {
            return;
        }
        socketChannel.configureBlocking(false);
        socketChannel.socket().setTcpNoDelay(true);
        socketChannel.socket().setKeepAlive(true);

        // 将选择器注册到连接到的客户端信道,并指定该信道key值的属性为OP_READ
        socketChannel.register(key.selector(), SelectionKey.OP_READ);
        System.out.println("accept channel:" + socketChannel);
        System.out.println("set SelectionKey.OP_READ on " + socketChannel);
    }

    // 客户端信道已经准备好了从信道中读取数据到缓冲区
    public void handleRead(SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();

        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        long readBytes = socketChannel.read(readBuffer);

        // 如果read()方法返回-1,说明客户端关闭了连接,可以安全地关闭
        if (readBytes < 0) {
            key.cancel();
            socketChannel.close();
        }
        else if (readBytes > 0) {
            readBuffer.flip();//将缓冲区当前的limit设置为position,position设置为0,用于读取
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);//将缓冲区的可读字节数组复制到bytes
            String msgBody = new String(bytes, "UTF-8");
            System.out.println("Server receive: " + msgBody);
            doWrite(socketChannel, "Server have received msg: " + msgBody);

            // 如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            System.out.println("set SelectionKey.OP_READ | SelectionKey.OP_WRITE on " + socketChannel);
        }

    }

    // 客户端信道已经准备好了将数据从缓冲区写入信道
    public void handleWrite(SelectionKey key) throws IOException {
        // 获取与该信道关联的缓冲区,里面有之前读取到的数据
        ByteBuffer buf = (ByteBuffer) key.attachment();
        // 重置缓冲区,准备将数据写入信道
        buf.flip();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        // 将数据写入到信道中
        socketChannel.write(buf);
        if (!buf.hasRemaining()) {
            // 如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读
            key.interestOps(SelectionKey.OP_READ);
            System.out.println("set  SelectionKey.OP_READ");
        }
        // 为读入更多的数据腾出空间
        buf.compact();
    }


    private void handleSelectionKeys(Iterator<SelectionKey> keyIter) throws IOException {
        SelectionKey key;
        while (keyIter.hasNext()) { // 循环取得集合中的每个键值
            key = keyIter.next();
            keyIter.remove();
            try {
                handleInput(key);
            } catch (IOException e) {
                if (key != null) {
                    key.cancel();
                    if (key.channel() != null) {
                        key.channel().close();
                    }
                }
            }
        }
    }
}

  Selector类的select()方法会阻塞等待,直到有信道准备好了IO操作,或等待超时,或另一个线程唤醒了它(调用了该选择器的wakeup()方法)。select()方法返回的是自上次调用它之后,有多少通道变为就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
  在用Iterator迭代selectedKeys()方法返回的SelectionKey集合时,每次迭代末尾注意调用remove()方法,以备下次该通道变成就绪时,Selector可以再次将其放入已选择键集中。如果不移除每个处理过的键,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它。所以说,select()方法每次返回的publicSelectedKeys都是一个Set实例。

  
  基于NIO的TCP通信Demo之服务端:

public class NIOTCPClient implements Runnable {
    private Selector selector;
    private SocketChannel socketChannel;

    public static void main(String[] args) throws Exception {
        new Thread(new NIOTCPClient())
                .start();
//      new NIOTCPClient().work("127.0.0.1", 9898, "你好");
    }

    public NIOTCPClient() throws Exception {
        this.selector = Selector.open();
        this.socketChannel = SocketChannel.open();
        this.socketChannel.configureBlocking(false);
    }

    @Override
    public void run() {
        doConnect();

        while (true) {
            try {
                System.out.println("Client select()...");
                selector.select(); // 阻塞等待

                handleSelectionKeys(selector.selectedKeys());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
//        //多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
//        if (selector != null) {
//            try {
//                selector.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//        }
    }

    private void handleSelectionKeys(Set<SelectionKey> selectionKeys) throws IOException {
        Iterator<SelectionKey> keyIter = selectionKeys.iterator();
        SelectionKey key;
        while (keyIter.hasNext()) { // 循环取得集合中的每个键值
            key = keyIter.next();
            keyIter.remove();
            try {
                handleInput(key);
            } catch (IOException e) {
                if (key != null) {
                    key.cancel();
                    if (key.channel() != null) {
                        key.channel().close();
                    }
                }
            }
        }
    }

    private void doConnect() {
        try {
            //如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
            if (socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000))) {
                socketChannel.register(selector, SelectionKey.OP_READ);
                doWrite(socketChannel, "zero");
            }
            else {
                //syn已经发送,等待服务器的syn-ack消息,所以在注册SelectionKey.OP_CONNECT事件
                socketChannel.register(selector, SelectionKey.OP_CONNECT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            //是否连接成功
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                if (sc.finishConnect()) {// 连接成功,服务端已返回syn-ack消息
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    doWrite(socketChannel, "zero007");
                }
                else {
                    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 msgBody = new String(bytes, "UTF-8");
                    System.out.println("Client receive :" + msgBody);
                }
                else if (readBytes < 0) {
                    //对端链路关闭
                    key.cancel();
                    sc.close();
                }
                else {
                    //读到0字节,忽略
                }
            }
        }
    }

    private void doWrite(SocketChannel sc, String msg) throws IOException {
        byte[] bytes = msg.getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        writeBuffer.put(bytes);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if (!writeBuffer.hasRemaining()) {
            System.out.println("Client send :" + msg);
        }
    }

    public void work(String ip, int port, String message) throws Exception {
        // 创建一个信道,并设为非阻塞模式
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        // 向服务端发起连接
        if (!socketChannel.connect(new InetSocketAddress(ip, port))) {
            // 不断地轮询连接状态,直到完成连接
            while (!socketChannel.finishConnect()) {
                // 在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性
                // 这里为了演示该方法的使用,只是一直打印"----"
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println("。。。。。。");
            }
        }
        // 为了与后面打印的"."区别开来,这里输出换行符
        System.out.print("\n");
        // 分别实例化用来读写的缓冲区
        byte[] msgBytes = message.getBytes();
        ByteBuffer writeBuf = ByteBuffer.wrap(msgBytes);
        ByteBuffer readBuf = ByteBuffer.allocate(msgBytes.length);

        // 接收到的总的字节数
        int totalBytesRcvd = 0;
        // 每一次调用read()方法接收到的字节数
        int bytesRcvd;

        // 循环执行,直到接收到的字节数与发送的字符串的字节数相等
        while (totalBytesRcvd < msgBytes.length) {
            // 如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道
            if (writeBuf.hasRemaining()) {
                socketChannel.write(writeBuf);
            }
            // 如果read()接收到-1,表明服务端关闭,抛出异常
            if ((bytesRcvd = socketChannel.read(readBuf)) == -1) {
                throw new SocketException("Connection closed prematurely");
            }
            // 计算接收到的总字节数
            totalBytesRcvd += bytesRcvd;
            // 在等待通信完成的过程中,程序可以执行其他任务,以体现非阻塞IO的异步特性
            // 这里为了演示该方法的使用,同样只是一直打印"."
            System.out.print(".");
        }
        // 打印出接收到的数据
        System.out.println("Received: "
                + new String(readBuf.array(), 0, totalBytesRcvd));
        // 关闭信道
        socketChannel.close();
    }
}

  再次说明一下,上面的服务端程序,select()方法第一次能选择出来的准备好的信道都是服务端信道,其关联键值的属性都为OP_ACCEPT,其有效操作都为 accept,在执行handleAccept方法时,为取得连接的客户端信道也进行了注册,属性为OP_READ,这样下次轮询调用select()方法时,便会检查到对read操作感兴趣的客户端信道(当然也有可能有关联accept操作兴趣集的信道),从而调用handleRead方法,在该方法中又注册了OP_WRITE属性,那么第三次调用select()方法时,便会检测到对write操作感兴趣的客户端信道(当然也有可能有关联read操作兴趣集的信道)。
  服务器端在等待信道准备好的时候,线程没有阻塞,而是可以执行其他任务,客户端在等待连接和等待数据读写完成的时候,线程没有阻塞,也可以执行其他任务。

  几个需要注意的地方
  1、对于非阻塞 SocketChannel来说,一旦已经调用connect()方法发起连接,底层套接字可能既不是已经连接,也不是没有连接,而是正在连接。由于底层协议的工作机制,套接字可能会在这个状态一直保持下去,这时候就需要循环地调用finishConnect()方法来检查是否完成连接,在等待连接的同时,线程也可以做其他事情,这便实现了线程的异步操作。
  2、write()方法的非阻塞调用只会写出其能够发送的数据,而不会阻塞等待所有数据,而后一起发送,因此在调用write()方法将数据写入信道时,一般要用到while循环,如:

while(buf.hasRemaining()) {
    channel.write(buf);
}            

  3、任何对key(信道)所关联的兴趣操作集的改变,都只在下次调用了select()方法后才会生效。
  4、selectedKeys()方法返回的键集是可修改的,实际上在两次调用select()方法之间,都必须手动将其清空,否则,它就会在下次调用select()方法时仍然保留在集合中,而且可能会有无用的操作来调用它,换句话说,select()方法只会在已有的所选键集上添加键,它们不会创建新的建集。
  5、对于ServerSocketChannel来说,accept是唯一的有效操作,而对于SocketChannel来说,有效操作包括读、写和连接,另外,对于DatagramChannle,只有读写操作是有效的。

本设计项目聚焦于一款面向城市环保领域的移动应用开发,该应用以微信小程序为载体,结合SpringBoot后端框架与MySQL数据库系统构建。项目成果涵盖完整源代码、数据库结构文档、开题报告、毕业论文及功能演示视频。在信息化进程加速的背景下,传统数据管理模式逐步向数字化、系统化方向演进。本应用旨在通过技术手段提升垃圾分类管理工作的效率,实现对海量环保数据的快速处理与整合,从而优化管理流程,增强事务执行效能。 技术上,前端界面采用VUE框架配合layui样式库进行构建,小程序端基于uni-app框架实现跨平台兼容;后端服务选用Java语言下的SpringBoot框架搭建,数据存储则依托关系型数据库MySQL。系统为管理员提供了包括用户管理、内容分类(如环保视频、知识、新闻、垃圾信息等)、论坛维护、试题与测试管理、轮播图配置等在内的综合管理功能。普通用户可通过微信小程序完成注册登录,浏览各类环保资讯、查询垃圾归类信息,并参与在线知识问答活动。 在设计与实现层面,该应用注重界面简洁性与操作逻辑的一致性,在满足基础功能需求的同时,也考虑了数据安全性与系统稳定性的解决方案。通过模块化设计与规范化数据处理,系统不仅提升了管理工作的整体效率,也推动了信息管理的结构化与自动化水平。整体而言,本项目体现了现代软件开发技术在环保领域的实际应用,为垃圾分类的推广与管理提供了可行的技术支撑。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值