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能较好的处理动态消息。