Java NIO:IO与NIO的区别及java NIO经典实例

一、概念

     NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

二、NIO和IO的主要区别

下表总结了Java IO和NIO之间的主要区别:

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

 

 

 

 

 

1、面向流与面向缓冲

     Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

2、阻塞与非阻塞IO

     Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3、选择器(Selectors)

     Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

三、NIO和IO如何影响应用程序的设计

无论您选择IO或NIO工具箱,可能会影响您应用程序设计的以下几个方面:

1.对NIO或IO类的API调用。
2.数据处理。
3.用来处理数据的线程数。

1、API调用

当然,使用NIO的API调用时看起来与使用IO时有所不同,但这并不意外,因为并不是仅从一个InputStream逐字节读取,而是数据必须先读入缓冲区再处理。

2、数据处理

使用纯粹的NIO设计相较IO设计,数据处理也受到影响。

在IO设计中,我们从InputStream或 Reader逐字节读取数据。假设你正在处理一基于行的文本数据流,例如:

Name: Anna 
Age: 25
Email: [email protected] 
Phone: 1234567890 

该文本行的流可以这样处理:

 

InputStream input = ... ; // get the InputStream from the client socket   

BufferedReader reader = new BufferedReader(new InputStreamReader(input));   

String nameLine   = reader.readLine(); 
String ageLine    = reader.readLine(); 
String emailLine  = reader.readLine(); 
String phoneLine  = reader.readLine(); 

 

     请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道肯定文本行就已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄等。 正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则:

而一个NIO的实现会有所不同,下面是一个简单的例子:

ByteBuffer buffer = ByteBuffer.allocate(48); 
int bytesRead = inChannel.read(buffer); 

注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。假设第一次 read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name:An”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存,在此之前,对数据的任何处理毫无意义。所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:

 

ByteBuffer buffer = ByteBuffer.allocate(48);   

int bytesRead = inChannel.read(buffer);   

while(! bufferFull(bytesRead) ) {   
       bytesRead = inChannel.read(buffer);   
} 

 

bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满了。

bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。

如果缓冲区已满,它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”:

四、总结

NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:

Java NIO: 单线程管理多个连接

如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:

Java IO: 一个典型的IO服务器设计- 一个连接通过一个线程处理.

 

java NIO经典实例

Loader.java

 

package net.chatroom.server;

public class Loader {

    public static void main(String[] args) {
        Deamon deamon = new Deamon(9999);
        new Thread(deamon).start();
    }

}

 

 

Util.java

 

package net.chatroom.server;

import java.nio.charset.Charset;
import java.util.HashSet;

public class Util {

    public static Charset charset = Charset.forName("UTF-8");
    
    // 相当于自定义协议格式,与客户端协商好
    public static String USER_CONTENT_SPILIT = "#@#";
    
    // 用来记录在线人数,以及昵称
    public static HashSet users = new HashSet();
    public static String USER_EXIST = "system message: user exist, please change a name";
}

 

 

Deamon.java

 

package net.chatroom.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

public class Deamon implements Runnable {

    private boolean flag = true;
    

    private ServerSocketChannel serverChannel = null;
    private Selector selector = null;
    /**
     * 记录进来的所有的客户端连接
     * */
    private List clientChannels = null;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public Deamon(int port) {
        try {
            serverChannel = ServerSocketChannel.open();
            serverChannel.socket().bind(new InetSocketAddress(port));
            selector = Selector.open();
            serverChannel.configureBlocking(false);
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            this.clientChannels = new ArrayList();
            System.out.println("Server is listening now...");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
//        System.out.println("server listening..");
        while (this.flag) {
            int num = 0;
            try {
                //此处select()阻塞了线程
                num = selector.select();
            } catch (IOException e) {
                System.out.println("Error while select channel:" + e);
            }
            if (num > 0) {
                Iterator it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();
                    if (key.isAcceptable()) {
                        // 监听到有新的连接则再注册读操作
                        this.clientChannels.add(Dealer.accept(selector,
                                serverChannel));
                    } else if (key.isReadable()) {
                        // 监听到读操作
                        try {
                            Dealer.read(selector, key, clientChannels);
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println("server to close..");
        if (this.serverChannel != null && this.serverChannel.isOpen()) {
            try {
                this.serverChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (this.selector != null && this.selector.isOpen()) {
            try {
                this.selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

 

 

Dealer.java

 

package net.chatroom.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.rmi.server.Skeleton;
import java.util.List;
import java.util.Scanner;

public class Dealer {

    public static SocketChannel accept(Selector selector,
            ServerSocketChannel serverChannel) {
        SocketChannel channel = null;
        try {
            channel = serverChannel.accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);

            channel.write(Util.charset.encode("Please input your name."));

        } catch (Exception e) {
            System.out.println("Error while configure socket channel :" + e);
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
        return channel;
    }

    public static void read(Selector selector, SelectionKey selectionkey,
            List clientChannels) throws IOException {
        SocketChannel socketClientChannel = (SocketChannel) selectionkey
                .channel();
        ByteBuffer buffer = ByteBuffer.allocateDirect(6 * 1024);
        StringBuilder content = new StringBuilder();
        int num = 0;
        try {
            // 将客户端发上来的消息读到buffer
            //循环将通道中数据读入buffer
            while (socketClientChannel.read(buffer) > 0) {
                buffer.flip();// 切换成读
                content.append(Util.charset.decode(buffer));
            }

            System.out.println("num:" + num);
            System.out.println("Server is listening from client :"
                    + socketClientChannel.getRemoteAddress() + " data rev is: "
                    + content);
        } catch (IOException e) {
            /**
             * 如果出现异常,则需要关闭连接。故把num设置为-1,用下面的关闭逻辑来关闭channel
             */
            num = -1;
        }

        if (num >= 0) {
            if (content.length() > 0) {
                String[] arrayContent = content.toString().split(
                        Util.USER_CONTENT_SPILIT);
                // 注册用户
                if (arrayContent != null && arrayContent.length == 1) {
                    String name = arrayContent[0];
                    if (Util.users.contains(name)) {
                        socketClientChannel.write(Util.charset
                                .encode(Util.USER_EXIST));
                    } else {
                        Util.users.add(name);
                        int onlineNum = clientChannels.size();
                        String message = "welcome " + name
                                + " to chat room! Online numbers:" + onlineNum;
                        BroadCast2(clientChannels, null, message);
                    }
                }
                // 注册完了,发送消息
                else if (arrayContent != null && arrayContent.length > 1) {
                    String name = arrayContent[0];
                    String message = content.substring(name.length()
                            + Util.USER_CONTENT_SPILIT.length());
                    message = name + " say: " + message;
                    if (Util.users.contains(name)) {
                        // 不回发给发送此内容的客户端
                        BroadCast2(clientChannels, socketClientChannel, message);
                    }
                }

                // /**
                // * 把读到的数据原封不动的下发给客户端
                // */
                // int length = clientChannels.size();
                // for (int index = 0; index < length; index++) {
                // // 循环所有的客户端连接,下发数据
                // buffer.flip();
                // try {
                // // 将buffer里的数据再下发给客户端的通道
                // clientChannels.get(index).write(buffer);
                // } catch (IOException e) {
                // e.printStackTrace();
                // }
                // }
            }
        } else {
            /**
             * 如果未读到数据,对方关闭了SocketChannel 所以服务器这边也要关闭
             */
            try {
                socketClientChannel.close();
                int length = clientChannels.size();
                for (int index = 0; index < length; index++) {
                    if (clientChannels.get(index).equals(socketClientChannel)) {
                        // 移除当前接受的通道
                        clientChannels.remove(index);
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    // TODO 要是能检测下线,就不用这么统计了
    public static int OnlineNum(Selector selector) {
        int res = 0;
        for (SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();

            if (targetchannel instanceof SocketChannel) {
                res++;
            }
        }
        return res;
    }

    public void BroadCast(Selector selector, SocketChannel except,
            String content) throws IOException {
        // 广播数据到所有的SocketChannel中
        for (SelectionKey key : selector.keys()) {
            Channel targetchannel = key.channel();
            // 如果except不为空,不回发给发送此内容的客户端
            if (targetchannel instanceof SocketChannel
                    && targetchannel != except) {
                SocketChannel dest = (SocketChannel) targetchannel;
                dest.write(Util.charset.encode(content));
            }
        }
    }

    public static void BroadCast2(List socketChannels,
            SocketChannel except, String content) throws IOException {
        for (SocketChannel socketChannel : socketChannels) {
            if (!socketChannel.equals(except)) {
                // 除了自己,其它通道都通知
                socketChannel.write(Util.charset.encode(content));
            }
        }
    }

}

 

 

客户端:

 

Loader.java

 

package net.chatroom.client;

import java.util.Scanner;

import net.chatroom.server.Util;

public class Loader {

    public static void main(String[] args) {
        String name = "";
        Deamon deamon = new Deamon("127.0.0.1", 9999);
        new Thread(deamon).start();

        // 在主线程中 从键盘读取数据输入到服务器端
        Scanner scan = new Scanner(System.in);
        while (scan.hasNextLine()) {
            String line = scan.nextLine();
            if ("".equals(line))
                continue; // 不允许发空消息
            if ("".equals(name)) {
                name = line;
                line = name + Util.USER_CONTENT_SPILIT;
            } else {
                line = name + Util.USER_CONTENT_SPILIT + line;
            }
            deamon.chancelToWrite(Util.charset.encode(line));// sc既能写也能读,这边是写
        }
    }
}

 

 

Deamon.java

 

package net.chatroom.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

import net.chatroom.server.Util;

public class Deamon implements Runnable {
    /**
     * 选择器,用于监听注册在上面的SocketChannel的状态
     */
    private Selector selector = null;

    /**
     * SocketChannel 用户发送和接受数据的信道
     */
    private SocketChannel channel = null;

    /**
     * 运行标识。在线程里此标识为false的时候会推出线程
     * 该属性在ExitCommandListener里通过调用setFlag方法修改,用于通知线程用户要求退出的程序
     */
    private boolean flag = true;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public Deamon(String address, int port) {
        try {
            channel = SocketChannel.open(new InetSocketAddress(address, port));
            channel.configureBlocking(false);
            selector = Selector.open();
            // 客户端直接注册读和写操作
            channel.register(selector, SelectionKey.OP_READ
                    | SelectionKey.OP_WRITE);

            
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    
    public void chancelToWrite(ByteBuffer buffer){
        try {
            channel.write(buffer);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        System.out.println("client run..");
        while (this.flag) {
            /**
             * 如果可以继续执行,则在循环体内循环执行监听选择操作
             */
            int num = 0;
            try {
                /**
                 * 得到处于可读或者可写状态的SocketChannel对象的个数
                 */
                // 客户端的select()并不阻塞线程,是因为客户端一启动就是SelectionKey.OP_WRITE状态
//                 System.out.println("client select..");
                num = this.selector.select();

//                 System.out.println("client num:"+num);
            } catch (IOException e) {
                /**
                 * 如果出现异常,则此处应该加上日志打印,然后跳出循环,执行循环体下面的释放资源操作
                 */
                break;
            }

            if (num > 0) {
                /**
                 * 如果有多个SocketChannel处于可读或者可写状态,则轮询注册在Selector上面的SelectionKey
                 */
                Iterator keys = selector.selectedKeys()
                        .iterator();
                while (keys.hasNext()) {
                    SelectionKey key = keys.next();
                    /**
                     * 此步操作用于删除该SelectionKey的被选中状态
                     */
                    keys.remove();
                    if (key.isReadable()) {
                        System.out.println("client isReadable..");
                        /**
                         * 如果是读操作,则调用读操作的处理逻辑
                         */
                        try {
                            Dealer.read((SocketChannel) key.channel());
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    } else if (key.isWritable()) {
                        //客户端的写状态是一直就绪的
                        // System.out.println("client isWritable..");
                        /**
                         * 如果是写操作,则调用写操作的处理逻辑
                         */
//                        Dealer.write((SocketChannel) key.channel());
                    }
                }
            }
            
            /*取消关注,多用在多线程的时候
             * key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
             * 
             * 增加关注
             * key.interestOps(key.interestOps() | SelectionKey.OP_READ);
             * */

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        if (this.channel != null && this.channel.isOpen()) {
            /**
             * 关闭SocketChannel
             */
            try {
                this.channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (this.selector != null && this.selector.isOpen()) {
            /**
             * 关闭Selector选择器
             */
            try {
                this.selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

Dealer.java

 

package net.chatroom.client;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

import net.chatroom.server.Util;

public class Dealer {
    /**
     * 从SocketChannel中读取信息
     * 
     * @param channel
     * @throws IOException 
     */
    public static void read(SocketChannel channel) throws IOException {

        /**
         * 初始化缓冲区
         */
        ByteBuffer buffer = ByteBuffer.allocateDirect(6 * 1024);
        /**
         * 读到的字节数
         */
        int num = 0;
        String content = "";
        while ((num = channel.read(buffer)) > 0) {
            buffer.flip();
            content += Util.charset.decode(buffer);
        }
        //若系统发送通知名字已经存在,则需要换个昵称
        if(Util.USER_EXIST.equals(content)) {
//            name = "";
            System.out.println("name has exists.");
        }
        System.out.println(content);
    }

    /**
     * 想SocketChannel中写入数据
     * 
     * @param channel
     */
    public static void write(SocketChannel channel) {

//        /**
//         * 从消息队列中获取要发送的消息
//         */
//        String msg = MsgQueue.getInstance().get();
//        if (msg == null) {
//            /**
//             * 如果消息队列中没有要发送的消息,则返回。
//             */
//            return;
//        }
//        /**
//         * 初始化缓冲区
//         */
//        ByteBuffer buffer = ByteBuffer.allocateDirect(6 * 1024);
//
//        /**
//         * 把消息放到缓冲区中
//         */
//        buffer.put(msg.getBytes());
//
//        /**
//         * 重置缓冲区指针
//         */
//        buffer.flip();
//        try {
//            /**
//             * 把缓冲区中的数据写到SocketChannel里
//             */
//            channel.write(buffer);
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
    }
}

你可能感兴趣的:(Java NIO:IO与NIO的区别及java NIO经典实例)