一、简单介绍
通道(Channel)是java.nio的第二个创建概念。Channel用于在缓冲区和位于通道另一侧的实体(通常是一个文件或者是一个套接字)之间有效的传输数据。只不过Channel本身不能直接访问数据,Channel只能和Buffer进行交互。
1.NIO的通道和流的区别
- 通道可以同时进行读写,但是流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读取数据,也可以写数据到缓冲区;
2.BIO中的stream是单向的,例如InputStream对象只能进行对数据的读取操作,而NIO中通道Channel是双向的,可以进行读操作,也可以进行写操作。
3.Channel在NIO中是一个接口
public interface Channel extends Closeable
二、常用的Channel实现类
- FileChannel: 用于读取、写入、映射和操作文件通道。
- DatagramChannel: 通过UDP读写网络中的数据通道。
- SocketChannel: 通过TCP读取网络中的数据
- ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建SocketChannel。
【ServerSocketChannel 类似 ServerSocket , SocketChannel 类似 Socket】
三、FileChannel类
1.FileChannel的常用方法
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) 强制将所有对此通道的文件更新写入到存储设备中
2.FileChannel的实际案例
①:需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello world!” 写入到 data.txt 中.
public class NIOTest {
public static void main(String[] args) {
bufferTest();
}
public static void bufferTest(){
try {
// 1、字节输出流通向目标文件
FileOutputStream fos = new FileOutputStream("D:\\data01.txt");
// 2、得到字节输出流对应的通道Channel
FileChannel channel = fos.getChannel();
// 3、分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello world!".getBytes());
// 4、把缓冲区切换成写出模式
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("写数据到文件中!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
②:需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道),将data01.txt 中的数据读入到程序,并显示在控制台屏幕
public class NIOTest {
public static void main(String[] args) throws Exception {
bufferTest();
}
public static void bufferTest() throws Exception {
// 1、定义一个文件字节输入流与源文件接通
FileInputStream is = new FileInputStream("D:\\data01.txt");
// 2、需要得到文件字节输入流的文件通道
FileChannel channel = is.getChannel();
// 3、定义一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4、读取数据到缓冲区
channel.read(buffer);
buffer.flip();
// 5、读取出缓冲区中的数据并输出即可
String rs = new String(buffer.array(),0,buffer.remaining());
System.out.println(rs);
}
}
四、FileChannel类的创建方式
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。
支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
获取通道的其他方式是使用 Files 类的静态方法
newByteChannel() 获取字节通道。或者通过通道的静态
方法 open() 打开并返回指定通道
五、SocketChannel
SocketChannel是对传统的java socket API进行了改进,主要支持了非阻塞的读写,同时改进了传统的单向流API,Channel同时支持读和写(其实就是加了个中间层Buffer)
SocketChannel的基本使用
①:打开SocketChannel
创建SocketChannel的方式有两种分别是
方式一:
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("www.baidu.com",80));
方式二:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.baidu.com",80));
②:关闭SocketChannel
socketChannel.close();
③:链接校验
socketChannel.isOpen(); //检测SocketChannel是否处于Open状态
socketChannel.isConnected(); // 检测SocketChannel是否已经被链接
socketChannel.isConnectionPending();//测试SocketChannel是否正在进行链接
socketChannel.finishConnect();//校验正在进行套接字的SocketChannel是否已经完成链接
④:SocketChannel的非阻塞模式
将 SocketChannel 设置为非阻塞模式之后,就可以在异步模式下调用connect(), read() 和write()了。
socketChannel.configureBlocking(false);
connect()
如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。像这样:
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("www.baidu.com",80));
//校验正在进行套接字的SocketChannel是否已经完成链接
while(! socketChannel.finishConnect() ){
}
}
write()
非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.baidu.com", 80));
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()) {
socketChannel.write(buf);
}
}
read()
非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。
使用 SocketChannel 读写
从 SocketChannel 读取
public static void main(String[] argv) throws Exception {
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
SocketChannel sChannel = SocketChannel.open();
sChannel.configureBlocking(false);
sChannel.connect(new InetSocketAddress("hostName", 12345));
int numBytesRead = sChannel.read(buf);
if (numBytesRead == -1) {
sChannel.close();
} else {
buf.flip();
}
}
写入 SocketChannel
public static void main(String[] argv) throws Exception {
SocketChannel sChannel = SocketChannel.open();
sChannel.configureBlocking(false);
sChannel.connect(new InetSocketAddress("hostName", 12345));
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
buf.put((byte) 0xFF);
buf.flip();
int numBytesWritten = sChannel.write(buf);
}
六、ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。
ServerSocketChannel的基本使用
①:打开 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
②:关闭 ServerSocketChannel
通过调用ServerSocketChannel.close() 方法来关闭ServerSocketChannel.
serverSocketChannel.close();
③:监听新进来的连接
通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
当然,也可以在while循环中使用除了true以外的其它退出准则。
④:非阻塞模式
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null.
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}
七、Scatter/Gather分散和聚集
分散(Scatter) 从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据 “分散(Scatter)” 到多个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,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。
聚集(gather) 写入Channel是指在写操作时将多个buffer的数据写入到同一个channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
public static void main(String[] argv) throws Exception {
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
}
buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。