JAVA NIO学习三:NIO 的非阻塞式网络通讯

紧接着上一章,咱们继续来研究NIO,上一章中咱们讲了NIO 中最多见的操做即文件通道的操做,但实际上NIO的主要用途仍是在于网络通讯,那么这个时候就会涉及到选择器,这一章咱们就会对其进行讲解操做。java

1、阻塞和非阻塞服务器

传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其余任务。所以,在完成网络通讯进行 IO 操做时,因为线程会阻塞,因此服务器端必须为每一个客户端都提供一个独立的线程进行处理,当服务器端须要处理大量客户端时,性能急剧降低。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程能够进行其余任务。线程一般将非阻塞 IO 的空闲时间用于在其余通道上执行 IO 操做,因此单独的线程能够管理多个输入和输出通道。所以, NIO 可让服务器端使用一个或有限几个线程来同时处理链接到服务器端的全部客户端。网络

下面我看个例子来使用NIO 演示下阻塞式,即不采用选择器状况下:并发

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import org.junit.Test;

/*
 * 1、使用 NIO 完成网络通讯的三个核心:
 * 
 * 1. 通道(Channel):负责链接
 *         
 *        java.nio.channels.Channel 接口:
 *             |--SelectableChannel
 *                 |--SocketChannel
 *                 |--ServerSocketChannel
 *                 |--DatagramChannel
 * 
 *                 |--Pipe.SinkChannel
 *                 |--Pipe.SourceChannel
 * 
 * 2. 缓冲区(Buffer):负责数据的存取
 * 
 * 3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 情况
 * 
 */
public class TestBlockingNIO {

    //客户端
    @Test
    public void client() throws IOException{
        //1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
        
        //2. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        //3. 读取本地文件,并发送到服务端
        while(inChannel.read(buf) != -1){
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        
        //4. 关闭通道
        inChannel.close();
        sChannel.close();
    }
    
    //服务端
    @Test
    public void server() throws IOException{
        //1. 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        
        //2. 绑定链接
        ssChannel.bind(new InetSocketAddress(9898));
        
        //3. 获取客户端链接的通道
        SocketChannel sChannel = ssChannel.accept();
        
        //4. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        //5. 接收客户端的数据,并保存到本地
        while(sChannel.read(buf) != -1){
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        
        //6. 关闭通道
        sChannel.close();
        outChannel.close();
        ssChannel.close();
        
    }
    
}

那么解决上面的方法,之前没有选择器的时候,对于阻塞状况,咱们能够采用下面的方法:(发送完成,自动本身关闭告知已发送完成)性能

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import org.junit.Test;

public class TestBlockingNIO2 {
    
    //客户端
    @Test
    public void client() throws IOException{
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
        
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        while(inChannel.read(buf) != -1){
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        
        sChannel.shutdownOutput(); //接收服务端的反馈
        int len = 0;
        while((len = sChannel.read(buf)) != -1){
            buf.flip();
            System.out.println(new String(buf.array(), 0, len));
            buf.clear();
        }
        
        inChannel.close();
        sChannel.close();
    }
    
    //服务端
    @Test
    public void server() throws IOException{
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        
        FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        
        ssChannel.bind(new InetSocketAddress(9898));
        
        SocketChannel sChannel = ssChannel.accept();
        
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        while(sChannel.read(buf) != -1){
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        
        //发送反馈给客户端
        buf.put("服务端接收数据成功".getBytes());
        buf.flip();
        sChannel.write(buf);
        
        sChannel.close();
        outChannel.close();
        ssChannel.close();
    }

}

2、选择器(Selector)ui

选择器(Selector) 是 SelectableChannle 对象的多路复用器, Selector 能够同时监控多个 SelectableChannel 的 IO 情况,也就是说,利用 Selector可以使一个单独的线程管理多个 Channel。 Selector 是非阻塞 IO 的核心。
SelectableChannle 的结构以下图:spa

 选择器(Selector)的应用
.net

建立 Selector :经过调用 Selector.open() 方法建立一个 Selector
线程

向选择器注册通道: SelectableChannel.register(Selector sel, int ops)
3d

选择器(Selector)的应用

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,须要经过第二个参数 ops 指定。
能够监听的事件类型(可以使用 SelectionKey 的四个常量表示):
 读 : SelectionKey.OP_READ (1)
 写 : SelectionKey.OP_WRITE (4)
 链接 : SelectionKey.OP_CONNECT (8)
 接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则能够使用“位或”操做符链接

例: 

SelectionKey 

SelectionKey: 表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。 选择键包含两个表示为整数值的操做集。操做集的每一位都表示该键的通道所支持的一类可选择操做 。

方 法 描 述
int interestOps() 获取感兴趣事件集合
int readyOps() 获取通道已经准备就绪的操做的集合
SelectableChannel channel() 获取注册通道
Selector selector() 返回选择器
boolean isReadable() 检测 Channal 中读事件是否就绪
boolean isWritable() 检测 Channal 中写事件是否就绪
boolean isConnectable() 检测 Channel 中链接是否就绪
boolean isAcceptable() 检测 Channel 中接收是否就绪

Selector 的经常使用方法

方 法 描 述
Set<SelectionKey> keys() 全部的 SelectionKey 集合。表明注册在该Selector上的Channel
selectedKeys() 被选择的 SelectionKey 集合。返回此Selector的已选择键集
int select() 监控全部注册的Channel,当它们中间有须要处理的 IO 操做时,
该方法返回,并将对应得的 SelectionKey 加入被选择的
SelectionKey 集合中,该方法返回这些 Channel 的数量。
int select(long timeout) 能够设置超时时长的 select() 操做
int selectNow() 执行一个当即返回的 select() 操做,该方法不会阻塞线程
Selector wakeup() 使一个还未返回的 select() 方法当即返回
void close() 关闭该选择器

SocketChannel 

Java NIO中的SocketChannel是一个链接到TCP网络套接字的通道。
操做步骤:
 打开 SocketChannel
 读写数据
 关闭 SocketChannel

Java NIO中的 ServerSocketChannel 是一个能够监听新进来的TCP链接的通道,就像标准IO中的ServerSocket同样
代码样例;

package com.atguigu.nio;

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

import org.junit.Test;

/*
 * 1、使用 NIO 完成网络通讯的三个核心:
 * 
 * 1. 通道(Channel):负责链接
 *         
 *        java.nio.channels.Channel 接口:
 *             |--SelectableChannel
 *                 |--SocketChannel
 *                 |--ServerSocketChannel
 *                 |--DatagramChannel
 * 
 *                 |--Pipe.SinkChannel
 *                 |--Pipe.SourceChannel
 * 
 * 2. 缓冲区(Buffer):负责数据的存取
 * 
 * 3. 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 情况
 * 
 */
public class TestNonBlockingNIO {
    
    //客户端
    @Test
    public void client() throws IOException{
        //1. 获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
        
        //2. 切换非阻塞模式
        sChannel.configureBlocking(false);
        
        //3. 分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        //4. 发送数据给服务端
        Scanner scan = new Scanner(System.in);
        
        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        
        //5. 关闭通道
        sChannel.close();
    }

    //服务端
    @Test
    public void server() throws IOException{
        //1. 获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        
        //2. 切换非阻塞模式
        ssChannel.configureBlocking(false);
        
        //3. 绑定链接
        ssChannel.bind(new InetSocketAddress(9898));
        
        //4. 获取选择器
        Selector selector = Selector.open();
        
        //5. 将通道注册到选择器上, 而且指定“监听接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        //6. 轮询式的获取选择器上已经“准备就绪”的事件
        while(selector.select() > 0){
            
            //7. 获取当前选择器中全部注册的“选择键(已就绪的监听事件)”
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            
            while(it.hasNext()){
                //8. 获取准备“就绪”的是事件
                SelectionKey sk = it.next();
                
                //9. 判断具体是什么事件准备就绪
                if(sk.isAcceptable()){
                    //10. 若“接收就绪”,获取客户端链接
                    SocketChannel sChannel = ssChannel.accept();
                    
                    //11. 切换非阻塞模式
                    sChannel.configureBlocking(false);
                    
                    //12. 将该通道注册到选择器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    //13. 获取当前选择器上“读就绪”状态的通道
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    
                    //14. 读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    
                    int len = 0;
                    while((len = sChannel.read(buf)) > 0 ){
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, len));
                        buf.clear();
                    }
                }
                
                //15. 取消选择键 SelectionKey
                it.remove();
            }
        }
    }
}

DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道。
 操做步骤:
 打开 DatagramChannel
 接收/发送数据

样例;

package com.atguigu.nio;

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

import org.junit.Test;

public class TestNonBlockingNIO2 {
    
    @Test
    public void send() throws IOException{
        DatagramChannel dc = DatagramChannel.open();
        
        dc.configureBlocking(false);
        
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        Scanner scan = new Scanner(System.in);
        
        while(scan.hasNext()){
            String str = scan.next();
            buf.put((new Date().toString() + ":\n" + str).getBytes());
            buf.flip();
            dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
            buf.clear();
        }
        
        dc.close();
    }
    
    @Test
    public void receive() throws IOException{
        DatagramChannel dc = DatagramChannel.open();
        
        dc.configureBlocking(false);
        
        dc.bind(new InetSocketAddress(9898));
        
        Selector selector = Selector.open();
        
        dc.register(selector, SelectionKey.OP_READ);
        
        while(selector.select() > 0){
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            
            while(it.hasNext()){
                SelectionKey sk = it.next();
                
                if(sk.isReadable()){
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    
                    dc.receive(buf);
                    buf.flip();
                    System.out.println(new String(buf.array(), 0, buf.limit()));
                    buf.clear();
                }
            }
            
            it.remove();
        }
    }

}

管道 (Pipe)

Java NIO 管道是2个线程之间的单向数据链接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

向管道写数据

 

从管道读取数据

从读取管道的数据,须要访问source通道。

调用source通道的read()方法来读取数据

代码样例:

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

import org.junit.Test;

public class TestPipe {

    @Test
    public void test1() throws IOException{
        //1. 获取管道
        Pipe pipe = Pipe.open();
        
        //2. 将缓冲区中的数据写入管道
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        Pipe.SinkChannel sinkChannel = pipe.sink();
        buf.put("经过单向管道发送数据".getBytes());
        buf.flip();
        sinkChannel.write(buf);
        
        //3. 读取缓冲区中的数据
        Pipe.SourceChannel sourceChannel = pipe.source();
        buf.flip();
        int len = sourceChannel.read(buf);
        System.out.println(new String(buf.array(), 0, len));
        
        sourceChannel.close();
        sinkChannel.close();
    }
    
}

 

参考资料:

《尚硅谷》视频

相关文章
相关标签/搜索