1 概述
前面提到,Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java NIO的通道可以更加细化。例如,不同的网络传输协议,在Java中都有不同的NIO Channel实现。
这里不对Java NIO的全部通道类型进行过多描述,仅着重介绍其中最为重要的四种Channle实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
对于以上四种通道,说明如下:
(1)FileChannel:文件通道,用于文件的数据读写。
(2)SocketChannel:套接字通道,用于套接字TCP连接的数据读写。
(3)ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许监听TCP连接请求,位每个监听的请求创建一个SocketChannel通道。
(4)DatagramChannel:数据报通道,用于UDP的数据读写。
这四种通道涵盖了文件IO、TCP网络、UDP IO三类基础IO读写操作。
2 FileChannel
FileChannel是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数据,也可以把数据写入文件中。特别申明一下,FileChannel位阻塞模式,不能设置为非阻塞模式。
下面分别介绍FileChannel的获取、读取、写入、关闭四个操作。
2.1 获取FileChannel
public class Test_FileChannel {
final static String srcFile = "/Users/jay/Documents/files/nio.txt";
final static String outFile = "/Users/jay/Documents/files/nio1.txt";
public static void main(String[] args) throws IOException {
//创建一个文件输入流
FileInputStream fis = new FileInputStream(srcFile);
//获取文件流的通道
FileChannel inChannel = fis.getChannel();
//创建一个文件输出流
FileOutputStream fos = new FileOutputStream(outFile);
//获取文件流的通道
FileChannel outChannel = fos.getChannel();
System.out.println(outChannel.size());
}
}
2.2 读取FileChannle
在大部分引应用场景中,从通道读取数据都会调用通道的int read(ByteBuffer buf)方法,它把从通道读取的数据写入 ByteBuffer缓冲区,并且返回读取的数据量。
public class Test_FileChannel {
final static String srcFile = "/Users/jay/Documents/files/nio.txt";
final static String outFile = "/Users/jay/Documents/files/nio1.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile rw = new RandomAccessFile(srcFile, "rw");
//获取通道(可读可写)
FileChannel channel = rw.getChannel();
//获取一个字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(30);
int length = -1;
//调用通道read方法,读取数据并写入字节类型的缓冲区
while((length = channel.read(buffer)) != -1){
System.out.println(length);
}
}
}
2.3 写入FileChannel
把数据写入通道,在大部分引用场景中都会调用通道的write(ByteBuffer buf)方法,此方法的参数是一个ByteBuffer缓冲区示例,是待写数据的来源。
write(ByteBuffer buf)方法的作用是从ByteBuffer缓冲区中读取数据,然后写入通道自身,而返回值是写入成功的字节数。
public class Test_FileChannel {
final static String srcFile = "/Users/jay/Documents/files/nio.txt";
final static String outFile = "/Users/jay/Documents/files/nio1.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile rw = new RandomAccessFile(srcFile, "rw");
//获取通道(可读可写)
FileChannel channel = rw.getChannel();
//获取一个字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(30);
int length = -1;
//调用通道read方法,读取数据并写入字节类型的缓冲区
while((length = channel.read(buffer)) != -1){
System.out.println(length);
}
buffer.flip();
int outlength = 0;
while((outlength = channel.write(buffer)) != 0){
System.out.println("写入的字节数 = " + outlength);
}
}
}
2.4 关闭通道
当通道使用完后,必须将其关闭。调用close()方法 .
public class Test_FileChannel {
final static String srcFile = "/Users/jay/Documents/files/nio.txt";
final static String outFile = "/Users/jay/Documents/files/nio1.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile rw = null;
FileChannel channel = null;
try {
rw = new RandomAccessFile(srcFile, "rw");
//获取通道(可读可写)
channel = rw.getChannel();
//获取一个字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(30);
int length = -1;
//调用通道read方法,读取数据并写入字节类型的缓冲区
while((length = channel.read(buffer)) != -1){
System.out.println(length);
}
buffer.flip();
int outlength = 0;
while((outlength = channel.write(buffer)) != 0){
System.out.println("写入的字节数 = " + outlength);
}
}catch (Exception e){
e.printStackTrace();
}finally {
channel.close();
}
}
}
2.5 强制刷新到磁盘
在将缓冲区写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据保存到硬盘,完成最终的数据保存。在将缓冲区数据写入通道时,要保证数据能写入硬盘,可以在写入后调用FileChannel类的force()方法。channel.force(true)
2.6 使用FileChannle 完成文件发复制实战
需求:使用FileChannel复制文件,把原文件中的内容复制到目标文件中。
public class FileNIOCopyDemo {
//原文件路径
public static final String srcPath = "";
//目标文件路径
public static final String destPath = "";
public static void main(String[] args) {
//演示复制资源文件
nioCopyFile(srcPath,destPath);
}
public static void nioCopyFile(String srcPath, String destPath){
File src_file = new File(srcPath);
File dest_file = new File(destPath);
try {
//如果目标文件不存在,则新建
if(!dest_file.exists()){
dest_file.createNewFile();
}
long start_time = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
//输入通道
FileChannel inChannel = null;
//输出通道
FileChannel outChannel = null;
try {
fis = new FileInputStream(src_file);
fos = new FileOutputStream(dest_file);
inChannel = fis.getChannel();
outChannel = fos.getChannel();
int length = -1;
//新建buf,处于写模式
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从输入通道读取buffer
while((length = inChannel.read(buffer)) != -1){
//buffer第一次切换成功,从写模式变成读模式
buffer.flip();
int outlength = 0;
//把buffer写入输出的通道
while ((outlength = outChannel.write(buffer)) != 0){
System.out.println("写入的字节数:" + outlength);
}
//buffer第二次模式切花,清除buffer,变成写模式
buffer.flip();
}
//强制刷新到磁盘
outChannel.force(true);
}finally {
outChannel.close();
fos.close();
inChannel.close();
fis.close();
}
long end_time = System.currentTimeMillis();
System.out.println("复制花费的时间是:" + (end_time - start_time) + "毫秒!");
}catch (Exception e){
}
}
}
3 SocketChannel
3.1 概述
在NIO中,涉及网路连接的通道有两个:一个是SocketChannel,负责连接的数据传输;另一个是ServerSocketChannel,负责连接的监听。其中,NIO中的SocketChannel传输通道与OIO中的Socket类对应,NIO中的ServerSocketChannel监听通道对应于OIO中的ServerSocket类。
ServerSocketChannel仅应用于服务端,而 SocketChannel 同时处于服务端和客户端。所以,对于一个链接,两端都有一个负责传输的SocketChannel。
无论是ServerSocketChannel 还是 SocketChannel,都支持阻塞和非阻塞两种模式。如何进行模式的设置呢?调用configureBlocking()方法,具体如下:
(1)socketChannel.configureBlocking(false):设置为非阻塞模式。
(2)socketChannel.configureBlocking(true):设置为阻塞模式。
在阻塞模式下,SocketChannel的连接、读写操作都是同步阻塞式的,在效率上与Java OIO面向流的阻塞式读写操作相同。因此,在这里不介绍阻塞模式下通道的具体操作。在非阻塞模式下,通道的操作是异步、高效的,这也是相对于传统的OIO的优势所在。
3.2 获取SocketChannel传输通道
【重要通知:本章小节里的代码只是方法演示,不具备运行功能。在介绍完SocketChannel常用的方法后,会举例并成功运行】
在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道,然后将socket设置为非阻塞模式,最后通过connet()实例方法对服务器的IP和端口发起连接。在非阻塞情况下,与服务器的连接可能还没有建立,socketChannel.connect()方法就反悔了,因此需要不断地自旋,检查当前是否连接到主机。
客户端:ClientTest.java
public class ClientTest {
public static void main(String[] args) {
//获取一个套接字传输通道
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
while(! socketChannel.finishConnect()){
System.out.println("连接成功");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
在连接建立的事件到来时,服务端的ServerSocketChannel能成功地查询出这个新连接事件,并且通过调用服务端ServerSocketChannel监听套接字的accept()方法来获取新连接的套接字通道。
服务端:ServerTest.java
public class ServerTest {
public static void main(String[] args) throws IOException {
//新连接事件到来,首先通过事件获取服务器监听通道
//key是具体的什么事件,是否为新连接事件-后面会说在,这里理解为一个事件对象
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//获取新连接的套接字通道
SocketChannel socketChannel = server.accept();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
}
}
【说明】NIO套接字通道主要用于非阻塞的传输场景。
3.3 读取SocketChannel传输通道
当SocketChannel传输通道可读时,可以从SocketChannel读取数据,具体方法与前面的文件通道读取方法时相同的。调用read()方法,将数据读入缓冲区ByteBuffer。
ByteBuffer buffer = ByteBuffer.allocate(1024);
int byteRead = socketChannel.read(buffer);
在读取时,因为是异步的,所以我们必须检查read()的返回值,以便判断当前是否读取了数据。read()方法的返回值是读取的字节数,如果是-1,那么表示读取到对方的输出结束标志,即对方已经输出结束,准备关键连接。实际上,通过read()方法读数据本身是很简单的,比较困难的是在非阻塞模式下如何知道通道何时是可读的。这需要用到NIO的新组件——Selector通道选择器,稍后会介绍它。
3.4 写入SocketChannel传输通道
和前面把数据写入FileChannel一样,大部分应用场景都会调用通道的write(ByteBuffer buf)方法。
//写入缓冲区前要求ByteBuffer是读模式
buffer.flip();
socketChannel.write(buffer);
3.5 关闭SocketChannel传输通道
在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次shutdownOutput()终止输出方法,向对方发送一个输出的结束标志(-1),然后调用socketChannel.close()方法,关闭套接字连接。
SocketChannel socketChannel = SocketChannel.open();
socketChannel.shutdownOutput();
socketChannel.close();
3.6 使用 SocketChannel 发送文件 实战
需求:使用FileChannel读取本地文件内容,然后在客户端使用SocketChannel 把文件信息和文件内容发送到服务器。
public class NioSendClient {
private Charset charset = Charset.forName("UTF-8");
public static final String SMALL_PATH = "/Users/jay/Documents/files/src_nio.txt";
public static final String BIG_PATH = "/Users/jay/Documents/files/孔乙己.docx";
//向服务器传输文件
public void sendFile() throws Exception {
File file = new File(BIG_PATH);
FileChannel fileChannel = new FileInputStream(file).getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.socket().connect(new InetSocketAddress("127.0.0.1",18899));
socketChannel.configureBlocking(false);
System.out.println("成功连接服务端");
while(!socketChannel.finishConnect()){
int count = 0;
//不断自旋、等待,或者做一些其他的是
System.out.println(count ++);
}
//发送文件的名称
ByteBuffer fileNameByteBuffer = charset.encode(file.getName());
ByteBuffer buffer = ByteBuffer.allocate(1024);
//发送的文件长度
int fileNameLen = fileNameByteBuffer.remaining();
buffer.clear();
buffer.putInt(fileNameLen);
//切换到读模式
buffer.flip();
socketChannel.write(buffer);
System.out.println("Client 文件名称长度发送完成:"+fileNameByteBuffer);
//发送文件
socketChannel.write(fileNameByteBuffer);
System.out.println("Client 文件名称名称发送完成:" + file.getName());
//清空
buffer.clear();
buffer.putInt((int) file.length());
//切换到读模式
buffer.flip();
//写入文件长度
socketChannel.write(buffer);
System.out.println("Client 文件长度发送完成:"+ file.length());
//发送文件内容
System.out.println("开始传输文件");
int length = 0;
long offset = 0L;
buffer.clear();
while((length = fileChannel.read(buffer)) > 0){
buffer.flip();
socketChannel.write(buffer);
offset += length;
System.out.println("| " + (100 * offset / file.length()) + "% |");
buffer.clear();
}
//等待一分钟关闭连接
Thread.sleep(60000);
if (length == -1){
fileChannel.close();
socketChannel.shutdownOutput();
socketChannel.close();
}
System.out.println("文件传输成功");
}
}