Java NIO Channel

Channel概述

Channel是一个通道,可以通过它读取与写入数据,它就像水管一样,网络数据通过Channel读取与写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动,而通道可用于读、写或者同时用于读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。

NIO中通过Channel封装了对数据源的操作,通过Channel我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,Channel与文件描述符或者socket是一一对应的。Channel用于在字节缓冲区和位于通道另一侧的实体之间有效地传输数据。

与缓冲区不同,通道API主要是由接口指定。不同操作系统上通道的实现会有根本性差异,所以通道API仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许以一种受控且可移植的方式来访问底层I/O服务。

Channel是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做对比,通道就像是流。所有数据都通过Buffer对象来处理。你永远不会将字节直接写入通道中,相反,你是将数据写入包含一个或多个字节的缓冲区中。同样,您不会直接从通道中读取字节,而是将数据从通道读取缓冲区,再从缓冲区获取这个字节。

Java NIO的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

正如上面所说,从通道中读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:
在这里插入图片描述

Channel实现

下面是Java NIO中最重要的Channel的实现:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

(1)FileChannel从文件中读写数据。
(2)DatagramChannel能通过UDP读写网络中的数据。
(3)SocketChannel能通过TCP读写网络中的数据。
(4)ServerSocketChannel可以监听新进来的TCP连接,像web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

正如你所看到的,这些通道涵盖了UDP和TCP网络IO,以及文件IO。

FileChannel

FileChannel类可以实现常用的read,write以及scatter/gather操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们熟悉的文件操作。

方法描述
int read(ByteBuffer dst)从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts)将Channel中的数据“分散”到ByteBuffer[]
int write(ByteBuffer src)将ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs)将ByteBuffer[]中的数据“聚集”到Channel
long position()返回此通道的文件位置
FileChannel position(long p)设置此通道的文件位置
long size()返回此通道的文件的当前大小
FileChannel truncate(long s)将此通道的文件截取为给定大小
void force(boolean metaData)强制将所有对此通道的文件更新写入到存储设备中

FileChannel读取数据到Buffer中的实例:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {

    public static void main(String[] args) throws IOException {
        RandomAccessFile file = new RandomAccessFile("d:\\qrxqrx\\01.txt","rw");
        FileChannel inChannel = file.getChannel();
        ByteBuffer buf = ByteBuffer.allocate(1024);
        int read = inChannel.read(buf);
        while (read != - 1) {
            System.out.println("读取了:"+read);
            buf.flip();
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            System.out.println();
            buf.clear();
            read = inChannel.read(buf);
        }
        file.close();
        System.out.println("结束了");
    }

}

Buffer通常的操作:

  • 将数据写入到缓冲区。
  • 调用buffer.flip()反转读写模式。
  • 从缓冲区中读取数据。
  • 调用buffer.clear()或buffer.compact()清除缓冲区内容。

FileChannel操作详解

打开FileChannel

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例:

RandomAccessFile file = new RandomAccessFile("d:\\01.txt","rw");
FileChannel inChannel = file.getChannel();

从FileChannel读取数据

调用多个read()方法之一从FileChannel中读取数据。如:

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

首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。

向FileChannel写数据

使用FileChannel.write()方法向FileChannel写数据,该方法的参数是一个Buffer。

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelDemo {

    public static void main(String[] args) throws IOException {
        RandomAccessFile file = new RandomAccessFile("d:\\qrxqrx\\01.txt", "rw");
        FileChannel channel = file.getChannel();
        String newData = "new String to write to file ... " + System.currentTimeMillis();
        ByteBuffer buf = ByteBuffer.allocate(48);
        buf.clear();
        buf.put(newData.getBytes());
        buf.flip();
        while (buf.hasRemaining()) {
            channel.write(buf);
        }
        channel.close(); 
    }

}

注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

关闭FileChannel

用完FileChannel后必须将其关闭。如:

channel.close();

FileChannel的position方法

有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取FileChannel的当前位置。也可以通过调用position(long pos)方法设置FileChannel的当前位置。

long pos = channel.position();
channel.position(pos+123);

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1(文件结束标志)。
如果将位置设置在文件标识符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

FileChannel的size方法

FileChannel的size()方法将返回该实例所关联文件的大小。如:

long filesize = channel.size();

FileChannel的truncate方法

可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将从指定长度后面的部分将被删除。如:

channel.truncate(1024);

这个例子截取文件的前1024个字节。

FileChannel的force方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。

force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写道磁盘上。

FileChannel的transferTo和transferFrom方法

通道之间的数据传输:
如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另外一个channel。

(1)transferFrom()方法:
FileChannel的transferFrom方法可以将数据从源通道传输到FileChannel中。如:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

public class FileChannelWrite {

    public static void main(String[] args) throws IOException {
        RandomAccessFile file1 = new RandomAccessFile("d:\\qrxqrx\\01.txt", "rw");
        FileChannel fromChannel = file1.getChannel();

        RandomAccessFile file2 = new RandomAccessFile("d:\\qrxqrx\\02.txt", "rw");
        FileChannel toChannel = file2.getChannel();

        long position = 0;
        long count = fromChannel.size();
        toChannel.transferFrom(fromChannel,position,count);

        file1.close();
        file2.close();
        System.out.println("over!");
    }

}

方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于count个字节,则所传输的字节数要小于请求的字节数。此外要注意,在SocketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。

(2)transferTo()方法
transferTo()方法将数据从FileChannel传输到其他的channel中。如:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

public class FileChannelWrite2 {

    public static void main(String[] args) throws IOException {
        RandomAccessFile file1 = new RandomAccessFile("d:\\qrxqrx\\01.txt", "rw");
        FileChannel fromChannel = file1.getChannel();

        RandomAccessFile file2 = new RandomAccessFile("d:\\qrxqrx\\02.txt", "rw");
        FileChannel toChannel = file2.getChannel();

        long position = 0;
        long count = fromChannel.size();
        fromChannel.transferTo(position,count,toChannel);

        file1.close();
        file2.close();
        System.out.println("over!");
    }

}

SocketChannel

(1)新的Socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。再也没有必要为每个socket连接使用一个线程,也避免了管理大量线程所需的上下文切换的开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了,并且只有很少甚至可能没有性能损失。所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择。

(2)请注意DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。

(3)socket和socket通道之间的关系。通道是一个连接I/O服务导管并提供与该服务交互的方法。就某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用。

全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现在都有getChannel方法。

(4)要把一个socket通道置于非阻塞模式,我们要依赖所有socket通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。

设置或重新设置一个通道的阻塞模式是简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用isBlocking()方法来判断某个socket通道当前处于那种模式。

非阻塞模式socket通常被认为是服务端模式使用的,因为它们使同时管理很多socket通道变得非常容易。但是,在客户端使用一个或几个非阻塞模式的socket通道也是有益处的,例如,借助非阻塞socket通道,GUI程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。

偶尔地,我们也需要防止socket通道的阻塞模式被更改。API中有一个blockingLock()方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。

ServerSocketChannel

ServerSocketChannel是一个基于通道的socket监听器。它同我们熟悉的java.net.ServerSocket执行相同的任务,不过它增加了通道的语义,因此能够在非阻塞模式下运行。

由于ServerSocketChannel没有bind()方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。我们也是使用对等ServerSocket的API来根据需要设置其他的socket选项。

同java.net.ServerSocket一样,ServerSocketChannel也有accept()方法。一旦创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept()。如果您选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。如果您选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。

换句话说,ServerSocketChannel的accept()方法会返回SocketChannel类型对象,SocketChannel可以在非阻塞模式下运行。

其他Socket的accept()方法会阻塞返回一个socket对象。如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册ServerSocketChannel对象以实现新连接到达时自动通知的功能。

以下代码演示了如何使用一个非阻塞的accept()方法:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class FileChannelAccept {

    public static void main(String[] argv) throws IOException, InterruptedException {
        int port = 8888;
        ByteBuffer buf = ByteBuffer.wrap("Hello java nio.".getBytes());

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ServerSocket socket = ssc.socket();
        socket.bind(new InetSocketAddress(port));

        //设置非阻塞模式
        ssc.configureBlocking(false);

        while (true) {
            System.out.println("waiting for connections");
            SocketChannel sc = ssc.accept();
            if (sc == null) { //没有连接传入
                System.out.println("null");
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from: "+sc.socket().getRemoteSocketAddress());
                buf.rewind(); //指针0
                sc.write(buf);
                sc.close();
            }
        }

    }

}

SocketChannel

SocketChannel介绍

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。

SocketChannel是一种面向流连接sockets套接字的可选择通道。从这里可以看出:

  • SocketChannel是用来连接Socket套接字。
  • SocketChannel主要用途用来处理网络I/O的通道。
  • SocketChannel是基于TCP连接传输。
  • SocketChannel实现了可选择通道,可以被多路复用的。

SocketChannel特征:

(1)对于已经存在的socket不能创建SocketChannel。
(2)SocketChannel中提供的open接口创建的Channel并没有网络连接,需要使用connect接口连接到指定地址。
(3)未进行连接的SocketChannel执行I/O操作时,会抛出NotYetConnectedException。
(4)SocketChannel支持两种I/O模式:阻塞式和非阻塞式。
(5)SocketChannel支持异步关闭。如果SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将返回-1表示没有读取任何数据;如果SocketChannel在一个线程上write阻塞,另一个线程对该SocketChannel调用shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException。
(6)SocketChannel支持设定参数:

  • SO_SNDBUF 套接字发送缓冲区大小
  • SO_RCVBUF 套接字接收缓冲区大小
  • SO_KEEPALIVE 保活链接
  • SO_REUSEADDR 复用地址
  • SO_LINGER 有数据传输时延缓关闭Channel(只有在非阻塞模式下有用)
  • TCP_NODELAY 禁用Nagle算法
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class SocketChannelDemo {

    public static void main(String[] args) throws IOException {

        //创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));

//        SocketChannel socketChannel = SocketChannel.open();
//        socketChannel.connect(new InetSocketAddress("www.baidu.com",80));

        //连接校验
        socketChannel.isOpen(); //  测试SocketChannel是否为open状态
        socketChannel.isConnected(); //测试SocketChannel是否已经被连接
        socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接
        socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接

        //读写模式
        socketChannel.configureBlocking(false);

        //设置和获取参数
        socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE);
        

        //读写
        ByteBuffer buf = ByteBuffer.allocate(48);
        socketChannel.read(buf);
        socketChannel.close();
        System.out.println("read over!");
    }

}

DatagramChannel

正如SocketChannel对应Socket,ServerSocketChannel对应ServerSocket,每一个DatagramChannel对象也有一个关联的DatagramSocket对象。正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)。DatagramChannel是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的地址及不依赖其他数据报的数据负载。与面向流的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都包含有关于它来自何处的信息(源地址)。

基本使用:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class DatagramChannelDemo {

    public static void main(String[] args) throws IOException {
        //打开DatagramChannel
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.socket().bind(new InetSocketAddress(8888));
        //接收数据
        ByteBuffer buf = ByteBuffer.allocate(64);
        buf.clear();
        SocketAddress socketAddress = datagramChannel.receive(buf);

        //发送数据
        ByteBuffer buffer = ByteBuffer.wrap("data...".getBytes());
        datagramChannel.send(buffer,new InetSocketAddress("127.0.0.1",8888));

        //连接
        datagramChannel.connect(new InetSocketAddress("127.0.0.1",8888));
        int readSize = datagramChannel.read(buffer);
        datagramChannel.write(buffer);
    }

}

示例:
发送端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;

public class DatagramChannelDemo1 {

    //发送
    public static void main(String[] args) throws IOException, InterruptedException {
        //打开DatagramChannel
        DatagramChannel datagramChannel = DatagramChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        //发送
        while (true) {
            ByteBuffer buffer = ByteBuffer.wrap("send data".getBytes(StandardCharsets.UTF_8));
            datagramChannel.send(buffer,address);
            System.out.println("finished!");
            Thread.sleep(1000);
        }
    }

}

接收端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;

public class DatagramChannelDemo2 {

    //接收
    public static void main(String[] args) throws IOException {
        //打开
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.bind(new InetSocketAddress("127.0.0.1",8888));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            buffer.clear();
            SocketAddress receive = datagramChannel.receive(buffer);
            buffer.flip();
            System.out.println(receive.toString());
            System.out.println(Charset.forName("UTF-8").decode(buffer));
        }
    }

}

Scatter/Gather

Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作。

分散(scatter):从Channel中读取是指读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散scatter”到多个buffer中。

聚集(gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel将多个Buffer中的数据“聚集(gather)”后发送到Channel。

scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。

Scattering Reads

Scattering Reads是指数据从一个channel读取到多个buffer中。如图:
在这里插入图片描述

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.read(bufferArray);

注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧跟着向另一个buffer中写。

Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充。Scattering Reads才能正常工作。

Gathering Writes

Gathering Writes是指数据从多个buffer写入到同一个channel。如图:
在这里插入图片描述

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.write(bufferArray);

buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值