JavaIO(四)-NIO通道详解

什么是通道?

通道是访问 I/O 服务的导管。可以在通道上传输“源缓冲区”与“目的缓冲区”要交互的数据,NIO 技术中的数据要放在缓 区中进行管理,再使用通道将缓冲区中的数据传输到目的地,那么Channel就是缓冲区和缓冲区之间的传输管道。

public interface Channel extends Closeable {
    public boolean isOpen();
    public void close() throws IOException;
}

最顶层的Channel接口只有两个API,对所有通道来说只有两种共同的操作:检查一个通道是否打开(isOpen())和关闭一个打开的通道(close())。

I/O 可以分为广义的两大类别:File I/O 和 Stream I/O。那么相应地也有两种类型的通道,它们是文件(file)通道和套接字(socket)通道。

文件通道:FileChannel

 

套接字通道:SocketChannel,ServerSocketChannel。还有一个DatagramChannel,这是一个通过 UDP 读写网络中的数据的通道。

 

通道的创建:

通道可以以多种方式创建。Socket 通道有可以直接创建。如

SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
DatagramChannel datagramChannel = DatagramChannel.open();

但是一个FileChannel 对象却只能通过一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream对象上调用 getChannel( )方法来获取,并不能直接创建一个 FileChannel 对象。

FileChannel channel = new FileInputStream("E:\\test.txt").getChannel();
RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel( );

通道可以是单向(unidirectional)或者双向的(bidirectional)。对应上边的类图,实现WritableByteChannel 那就是可写通道,实现ReadableByteChannel,就是一个可读通道,单独实现其中一个的都是单向通道,只能在一个方向上进行数据传输,而两个都实现就是双向通道,可以双向传输数据。

ReadableByteChannel只定义了read()方法用于数据读取。

public int read(ByteBuffer dst) throws IOException;

WritableByteChannel 只定义了write()方法用于数据写入。

public int write(ByteBuffer src) throws IOException;

ByteChannel 接口的通道会同时实现 ReadableByteChannel 和 WritableByteChannel 两个接口,所以此类通道是双向的。没有实际方法但是可以方便下游类实现双向通道,所以这意味着FileChannel 、SocketChannel 和DatagramChannel 通道对象都是双向的。但是可读可写不意味着可以同时读写,这个在IO基础里已经说过,一根总线同一时间只能读或者写。

FileChannel :

一个FileChannel 对象却只能通过一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream对象上调用 getChannel( )方法来获取,并不能直接创建一个 FileChannel 对象。

从 FileInputStream 对象的getChannel( )方法获取的 FileChannel 对象是只读的,不过从接口声明的角度来看却是双向的,因为FileChannel 实现 ByteChannel 接口。在这样一个通道上调用 write( )方法将抛出未经检查的NonWritableChannelException 异常,因为 FileInputStream 对象总是以 read-only 的权限打开文件,所以定义归定义,实际的使用又是另一回事了。

FileChannel 总是阻塞式的,因此不能被置于非阻塞模式,而且FileChannel对象是线程安全的,并非说一个文件只能建立一个通道,而是建立多个通道之后同一时间只能有一个线程对这个文件进行修改,如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。以下面一个例子来引发一些思考。

public class FileNioTest {
    public static void main(String[] args) throws IOException {
        FileChannel channel = new FileInputStream("E:\\test.txt").getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        StringBuffer content = new StringBuffer();
        while (channel.read(byteBuffer) != -1) {
            byteBuffer.flip();
            CharBuffer charBuffer = Charset.defaultCharset().decode(byteBuffer);
            content.append(charBuffer.toString());
            byteBuffer.clear();
        }
        System.out.println(content);
        channel.close();
    }
}

先看输出

 

现在看好像是没问题的,那么如果我把缓冲区改小,比如4,那结果就变成了

 

乱码了,首先defaultCharset默认是UTF-8,根据我们学过的编码知识,UTF-8默认汉字占用3个字节,字母数字占用1个字节,我们以4个字节大小的缓冲区去读取,自然就乱码了,而且FileChannel 只支持字节缓冲区。关于编码不清楚的可以看一下二进制与编码

怎么解决这个问题?就跟我们一开始一样把缓冲区加大,加大到足够包含你所有内容的大小,但是这显然是不明智的,一个文件可以很大,但是内存就那么点,如果是非直接缓冲区,那么可使用范围就更小了。那么还有另外一种方式,我们以行为单位进行读取总行了吧,一个行的大小总归不会很大,但是这样又从新带来一个问题,你需要判断换行符 即 '\r' 或者 '\n',而且通道进行缓冲区读取都是保证文件结束或者缓冲区填满,那么我们这个换行也就不好判断了,我们每拿到一个字节都要进行一下判断,所以方法可行但是复杂。

所以NIO中的FileChannel并不是一个好的文件读取器,遇到这种需要读取到用户空间并且需要进行Buffer类型转换的情况,可能BIO会更适合,BIO也并非一无是处,刚开始我们就说了,反正FileChannel也是阻塞的。但是我们好像忘了Channel的本质,他是一个桥梁,一个连接两个缓冲区的管道,如果我们需要从一个缓冲区,传输到另个一个缓冲区,这个意义可能就大的多。

public static void main(String[] args) throws IOException {
    FileChannel inChannel = new FileInputStream("E:\\test.txt").getChannel();
    FileChannel outChannel = new FileOutputStream("E:\\testcopy.txt").getChannel();
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    while (inChannel.read(byteBuffer) != -1) {
        byteBuffer.flip();
        outChannel.write(byteBuffer);
        byteBuffer.clear();
    }
    inChannel.close();
    outChannel.close();
}

写入结果:

 

下面使我们的重中之重SocketChannel和ServerSocketChannel:

在Linux服务器下,我们运行这样一行命令

man socket

我们会看到这样一行

 

操作系统本身就为我们提供了非阻塞方式的socket的,所谓"天予不取,反受其咎",那我们就要充分利用起来。

新的 socket 通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失,这就是传说中的多路复用。

开启非阻塞模式需要调用通道的configureBlocking()方法,并设置为false,因为默认为true。

channel.configureBlocking(false);

ServerSocketChannel和SocketChannel的使用依然逃不开我们前面的那张图,建立通道,绑定端口,监听端口,接收连接,发送数据,关闭通道。

public class SocketNioServerTest {
    private static List<SocketChannel> clients = new LinkedList<>();
    public static void main(String[] args) {
        ServerSocketChannel channel = null;
        try {
            channel = ServerSocketChannel.open();
            channel.configureBlocking(false);
            channel.bind(new InetSocketAddress(9090));
            while (channel.isOpen()) {
                TimeUnit.SECONDS.sleep(5);
                SocketChannel client = channel.accept();
                if (null != client) {
                    System.out.println("a new socket client connection, port : "
                            + client.socket().getPort());
                    client.configureBlocking(false);
                    clients.add(client);
                } else {
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    for (SocketChannel c : clients) {
                        try {
                            if (Objects.isNull(c)
                                    || !c.isOpen()
                                    || !c.isConnected()) {
                                clients.remove(c);
                                continue;
                            }
                            if (c.read(buffer)  == 0) {
                                System.out.println("no data");
                                continue;
                            }
                            if (c.read(buffer) != -1) {
                                buffer.flip();
                                CharBuffer charBuffer = Charset.defaultCharset().decode(buffer);
                                System.out.println(charBuffer.toString());
                                buffer.clear();
                            }
                        }catch (Exception e) {
                            System.out.println("nio client exception " + e.getMessage());
                            clients.remove(c);
                        }
                    }
                }
            }
        } catch (Exception e) {
            System.out.println("nio server exception " + e.getMessage());
        } finally {
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    System.out.println("nio server exception " + e.getMessage());
                }
            }
        }
    }
}

我们启动之后先不启动客户端,控制台输出如下,说明我们accept并没有被阻塞,accept非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept( )会立即返回 null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。

 

为了更好的了解后续表现,然后把这个输出删了,启动客户端。发送一个“123”,继续等待,然后断开连接输出如下

 

 

首先我们接收到一个连接,并且客户端向服务端发送了一个数据,之后服务端打印了这个数据。此时客户端和服务端仍然处于连接状态,针对所有SocketChannel进行轮询,没有数据也会立即返回0,所以打印 "no data",我们关闭连接,服务端报异常,但是程序并没有关闭,静静等待下一个连接的到来。因为我们不会阻塞,所以也支持客户端连接同一个服务器,同时发送数据。

我们对比BIO,首选没有用到多线程,但是依然实现了多客户端连接和多连接接收数据,这是因为我们一个线程对所有的连接进行了管理。

 

原先我们受限于线程大量占用资源的情况下没法大量接收和处理连接,现在我们只需要一个线程就可以管理多个连接和数据输入,那么如果你真正跑了上面的代码,试用了各种情况,你就会发现一个问题,慢,很慢,几个连接尚且如此,当大量连接到来,我们每一次都需要对所有的连接进行轮询,哪怕有一万个连接,不管有没有数据,都需要取一下看看,这并不是我们所希望看到的。而操作系统才是最善解人意的,跟非阻塞一样,同样为我们提供了解决方案,那就是Selector。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值