二、Java NIO通信基础

Java NIO属于第三种模型--IO多路复用模型,主要有三个核心组件组成:

  • Channel 通道:在NIO中,同一个网络连接使用一个通道表示,所有的NIO的IO操作都是从这个通道开始的。一个通道类似于OIO中的两个流的结合体,既可以从通道中读取,也可以向通道中写入。
  • Buffer缓冲区:应用程序与通道的主要交互操作就是数据的读写,通道的读取和写入就是将数据与缓冲区的读取和写入。
  • Selector选择器:(什么是IO多路复用?----指的是一个线程/进程可以同时监视多个文件描述符,一旦其中一个或者多个文件描述符可写或者可读,系统内核就通知该线程/进程)Selector是一个事件的查询器,通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。

一、NIO Buffer类及其属性

       NIO的Buffer本质上是一个内存块,可读也可写。Java中NIO的Buffer类是一个抽象类,其内部是一个内存块(数组)。它提供了一组更加有效的方法来进行写入和读取的交替访问。

Buffer类在其内部有一个byte[]数组内存块作为内存缓冲区。为了记录读写的状态和位置,有以下

1.几个重要的属性:

 // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
  • capacity表示内部容量的大小,Buffer类的capacity一旦初始化就不能再改变了。
  • position表示当前的位置(下一个要被读和写的位置)。与缓冲区的读写模式有关,在读写模式切换时会进行调整。写入时:最初position为0,每当一个数据写入,position会向后移动一个位置,最大可谓limit-1。当position到达limit时,则表示缓冲区已满,无法继续写入。读取时:position重置为0,读取数据后position相应向后移动,当position达到limit是表示无数据可读。 
  • limit表示读写的最大上限。与缓冲区的读写模式有关,写模式:刚写入时limit会被置为capacity值。读模式:flip()方法调用后,limit的值会被置为写模式的position值,position被置为0。表示可以从0开始读取,读取数据大小为limit。
  • mark相当于一个暂存属性,暂时保存position的值,方便后面重复使用position值。调用mark()方法来是指mark=position,在调用reset()可以让position恢复到mark标记的位置。

2.几个重要的方法:

  • allocate()创建缓冲区。
  • put()写入到缓冲区。写入数据时,position根据写入的数据量向后移动。
  • flip()翻转,向缓冲区写入数据后并不是可以直接读取数据,需要调用该方法,limit被置为原position值,position被置为0,表示从头开始读取。
  • get()从缓冲区读取。
  • rewind()倒带,缓冲区的数据是否可以重复读呢,调用该方法position置为0,limit不变,之前的mark被清理。
  • mark()和reset(),mark先记录当前读取的position值,继续向后读取,当调用reset方法时,postion将被置为刚才保存在mark的旧的position值。读取数据也将从mark值标记的位置开始读取。
  • clear()清空缓冲区,调用该方法缓冲区切换为写入模式,position置为0,limit被置为capacity值。

3.使用Buffer类的基本步骤

  • 使用创建子类实例对象的allocate()方法,创建Buffer类的实例对象。
  • 调用put()方法,将数据写入到缓冲区。
  • 写入完成后,在开始读取数据之前,调用buffer.flip()方法,将缓冲区变为读模式。
  • 调用get()方法,从缓冲区获取数据。
  • 读取完成后调用clear()或者compact()方法,将缓冲区转换为写入模式。

一个简单的示例:

public class IntBufferDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(5);
        System.out.println("初始化时:"+buffer);
        buffer.put(3);
        buffer.put(2);
        buffer.mark();
        buffer.put(4);
        System.out.println("mark调用后,reset方法之前:"+buffer+"\n值为"+Arrays.toString(buffer.array()));
        buffer.reset();
        buffer.put(5);
        System.out.println("reset方法调用后第三个值被替换"+Arrays.toString(buffer.array()));
        buffer.put(6);
        System.out.println("调用flip方法前:"+buffer);
        buffer.flip();
        System.out.println("调用flip方法后:"+buffer);
        for (int i = 0; i <buffer.limit() ; i++) {
            System.out.println(buffer.get());
            System.out.println("读取第"+(i+1)+"个数据后:"+buffer);
        }
        buffer.clear();
        System.out.println("清空缓存后:"+buffer);
    }
}

结果如下:

初始化时:java.nio.HeapIntBuffer[pos=0 lim=5 cap=5]
mark调用后,reset方法之前:java.nio.HeapIntBuffer[pos=3 lim=5 cap=5]
值为[3, 2, 4, 0, 0]
reset方法调用后第三个值被替换[3, 2, 5, 0, 0]
调用flip方法前:java.nio.HeapIntBuffer[pos=4 lim=5 cap=5]
调用flip方法后:java.nio.HeapIntBuffer[pos=0 lim=4 cap=5]
3
读取第1个数据后:java.nio.HeapIntBuffer[pos=1 lim=4 cap=5]
2
读取第2个数据后:java.nio.HeapIntBuffer[pos=2 lim=4 cap=5]
5
读取第3个数据后:java.nio.HeapIntBuffer[pos=3 lim=4 cap=5]
6
读取第4个数据后:java.nio.HeapIntBuffer[pos=4 lim=4 cap=5]
清空缓存后:java.nio.HeapIntBuffer[pos=0 lim=5 cap=5]

 

二、NIO Channel通道

Java中主要的四种Channel为:

  • FileChannel文件通道,用于文件的数据读写。
  • SocketChannel套接字通道,用于Socket套接字TCP连接的数据读写。
  • ServerSocketChannel服务器监听通道,允许我们监听TCP连接请求,为每个监听到的请求创建一个SocketChannel套接字通道。
  • DatagramChannel数据报通道,用于UDP协议的数据读写。

下面是一个使用FileChannel完成复制文件的简单案例代码:

public class FileChannelCopyDemo {
    private static final Logger log = LoggerFactory.getLogger(FileChannelCopyDemo.class);

    public static void main(String[] args) {
        nioCopyFile();
    }

    /**
     * 复制资源文件
     */
    private static void nioCopyFile() {
        long startTime = System.currentTimeMillis();
        File sourceFile = new File("");
        File destFile = new File("");
        try {
            boolean fileExsits = destFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            fileInputStream = new FileInputStream(sourceFile);
            fileOutputStream = new FileOutputStream(destFile);
            inChannel=fileInputStream.getChannel();
            outChannel=fileOutputStream.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //从输入通道读取到buffer
            //以下也可以用  outChannel.transferFrom(inChannel, pos, srcFile.length());替换
            while ((inChannel.read(buffer))!=-1){
                //第一次切换(buffer读取满了,需要写入到输出通道中),翻转buffer,变成读取模式
                buffer.flip();
                int outLength;
                while ((outLength=outChannel.write(buffer))!=0){
                    System.out.println("写入的字节数:"+outLength);
                }
                //第二次切换(数据写完,读取下一部分):清除buffer,变成写入模式
                buffer.clear();
            }
            //强制刷新到磁盘
            outChannel.force(true);
            long endTime = System.currentTimeMillis();
            log.debug("复制耗时毫秒数:" + (endTime - startTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fileInputStream.close();
                fileOutputStream.close();
                inChannel.close();
                outChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

三、NIO Selector选择器

选择器的使命就是完成IO的多路复用。一个通道代表一个连接通道,通过选择器可以同时监控多个通道的IO状况。选择器和t通道的关系,是监控和被监控的关系。选择器提供了一系列API能选择出所监控的通道那些拥有已经准备好的就绪的IO操作事件。

这里有一个很重要的概念--->SelectionKey选择键:简单来说,SelectionKey就是那些被选择器选中的IO事件,其中包含了Channel。主要有以下四种事件:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

Tips:注册到选择器的通道必须处于非阻塞模式下,否则将抛出IllegalBlockingModeExpection异常。并且服务器监听通道ServerSocketChannel仅仅支持Accept事件,而SocketChannel传输通道不支持Accept(接收到新链接)IO事件。

NIO处理的大致流程图如下:

  • ServerSocketChannel会被先注册到Selector上,后台线程会轮询感兴趣的IO事件,即SelectionKey.OP_ACCEPT 事件时,就会自动处理。
  • 当有客户端进行连接时,会被ServerSocketChannel监听到并生成SocketChannel,通过Register()方法将SocketChannel注册到Selector上,注册后返回一个SelectionKey。
  • Selector会调用select方法返回就绪的IO事件,得到SelectionKey并进行处理。

下面是一个简单的NIO代码:

服务端代码:

public class NIOServerDemo {
    public static void main(String[] args) throws IOException {
        //1.获取选择器
        Selector selector = Selector.open();
        //2.获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //4.绑定连接
        serverSocketChannel.socket().bind(new InetSocketAddress(8878));
        //5.将通道注册到选择器上,并注册的IO事件为:"接收新链接"
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("注册后的SelectionKey 数量=" + selector.keys().size()); // 1
        //6.选择感兴趣的IO就绪事件(选择键集合)
        //如果返回的>0, 就获取到相关的 selectionKey集合
        while (true) {
            if (selector.select(1000) == 0) {
                //无事件发生,返回继续等待
                continue;
            }
            //7.获取选择键集合
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                //8.获取单个的选择键并处理
                SelectionKey key = iterator.next();
                //9.判断key是什么事件,并处理
                if (key.isAcceptable()) {
                    //10.如果是新链接事件,就获取客户端新链接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept();
                    if (socketChannel == null) continue;
                    //11.客户端新链接切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //12.将客户端新链接注册到选择器上
                    SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接后 ,注册的SelectionKey 数量=" + selector.keys().size());
                } else if (key.isReadable()) {
                    //通过key 反向获取到对应channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    //此处连接关闭会造成死循环??Tips
                    if (channel.read(byteBuffer) > -1) {
                        Charset charset = Charset.forName("UTF-8");
                        String message = new String(byteBuffer.array(), 0, byteBuffer.limit(), charset);
                        System.out.println("从客户端收到消息:" + message);
                    }else{
                        System.out.println("远程连接关闭");
                        key.cancel();
                    }
                }

                iterator.remove();
            }
        }
    }
}

客户端可以用Telnet进行测试,下面是个简单的Java代码:

public class NIOClientDemo {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.socket().connect(new InetSocketAddress("127.0.0.1", 8878));
        socketChannel.configureBlocking(false);
        while (!socketChannel.finishConnect()) {
            //等待链接完成
        }
        System.out.println("成功连接服务器");
        //如果连接成功,就发送数据
        String str = "hello, world";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,将 buffer 数据写入 channel
        socketChannel.write(buffer);
        socketChannel.shutdownOutput();
        socketChannel.close();
    }
}

四、基于NIO的简单群聊系统

业务逻辑如下:服务端代码做转发功能,某一个客户端发送消息到服务端后,服务端将消息转发给所有在线的其他用户。

服务端代码如下:

public class GroupChatServer {
    //定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 6667;

    //初始化工作
    private GroupChatServer() {
        try {
            //得到选择器
            selector = Selector.open();
            //ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //绑定端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //设置非阻塞模式
            listenChannel.configureBlocking(false);
            //将该listenChannel 注册到selector
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //监听
    private void listen() {
        System.out.println("服务端已启动监听");
        try {
            //循环处理
            while (true) {
                int count = selector.select();
                if (count > 0) {//有事件处理
                    //遍历得到selectionKey 集合
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        //取出selectionKey
                        SelectionKey key = iterator.next();

                        //监听到accept
                        if (key.isAcceptable()) {
                            SocketChannel sc = listenChannel.accept();
                            sc.configureBlocking(false);
                            //将该 sc 注册到selector
                            sc.register(selector, SelectionKey.OP_READ);
                            //提示
                            System.out.println(sc.getRemoteAddress() + " 上线 ");
                        }
                        if (key.isReadable()) { //通道发送read事件,即通道是可读的状态
                            //处理读 (专门写方法..)
                            readData(key);
                        }
                        //当前的key 删除,防止重复处理
                        iterator.remove();
                    }
                } else {
                    System.out.println("等待....");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //发生异常处理....
            System.out.println("服务端发生异常");
        }
    }

    //读取客户端消息
    private void readData(SelectionKey key) {
        //取到关联的channel
        SocketChannel channel = null;
        try {
            //得到channel
            channel = (SocketChannel) key.channel();
            //创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            //根据count的值做处理
            if (count > 0) {
                //把缓存区的数据转成字符串
                String msg = new String(buffer.array());
                //输出该消息
                System.out.println("form 客户端: " + msg);
                //向其它的客户端转发消息(去掉自己)
                sendInfoToOtherClients(msg, channel);
            }
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了..");
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            } catch (IOException e2) {
                e2.printStackTrace();
            }
        }
    }

    //转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
        System.out.println("服务器转发消息");
        //遍历 所有注册到selector 上的 SocketChannel,并排除 self
        for (SelectionKey key : selector.keys()) {
            //通过 key  取出对应的 SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                SocketChannel dest = (SocketChannel) targetChannel;
                //将msg 存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer 的数据写入 通道
                dest.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        //创建服务器对象
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

客户端代码:

public class GroupChatClient {

    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //构造器, 完成初始化工作
    private GroupChatClient() throws IOException {

        selector = Selector.open();
        //连接服务器
        socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6667));
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //将channel 注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    }

    //向服务器发送消息
    private void sendInfo(String info) {
        info = username + " 说:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取从服务器端回复的消息
    private void readInfo() {
        try {
            int readChannels = selector.select();
            if(readChannels > 0) {//有可以用的通道
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        //得到相关的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        //得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        //读取
                        sc.read(buffer);
                        //把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                iterator.remove();
            } else {
                //System.out.println("没有可以用的通道...");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        //启动客户端
        GroupChatClient chatClient = new GroupChatClient();
        //启动一个线程读取服务器发送数据
        new Thread(chatClient::readInfo).start();
        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}

效果如下,

服务器端:

服务端启动监听 
/127.0.0.1:58965 上线 
/127.0.0.1:58977 上线 
form 客户端: 127.0.0.1:58965 说:你好 
服务器转发消息
form 客户端: 127.0.0.1:58977 说:你也好

客户端1:

127.0.0.1:58965 is ok...
你好
127.0.0.1:58977 说:你也好

客户端2:

127.0.0.1:58977 is ok...
127.0.0.1:58965 说:你好
你也好

 

posted @ 2019-12-05 23:27  寻找艾伦  阅读(16)  评论(0编辑  收藏  举报