java nio

14. Buffers

传统应用程序设计上采用同步I/O,这些应用具有如下特征:

  • 文件可能会很大,有可能吧整个文件读入内存。
  • 应用程序在同一时间只读取或写入几个文件或网络连接,理想情况下只使用一个流一次。
  • 应用是有序的,在完成读文件或写文件之前,不能够做其他的事情。

只要这些特性保持不变,基于流的输入/输出就相当快并且运行得相当高效。然而,如果违反了这些先决条件,标准I/O模型开始暴露一些缺陷。例如:web服务器通常需要为成千上百的连接服务。科学、工程、多媒体应用程序经常需要处理大小为千兆字节的数据集。

JDK1.4介绍了一种新的I/O模型,它专为此类应用设计,除此,还减少了传统应用程序的I/O数。新I/O的库在java.nio和其子包内。新I/O不是为了替代传统的、基于流的I/O。甚至,一些新I/O是以流式I/O为基础的。然而,新的i/o模型对于某些类型的i/o绑定应用程序来说更加高效。

传统的I/O模型基于流,新I/O模型基于缓冲区和通道。缓冲区就像是一个数组(在一些实现里,实际上它就是一个数组),它装载了要读要写的数据。然而,与传统的输入输出流不一样,同样的缓存区既能用于读又能用于写。数据从输入通道填如缓冲区,从输出通道流出。缓冲区是供通道交换数据的中立场所,而不是通道的一部分。此外,由于缓冲区是通过方法访问的对象,他们有可能不是数组。他们可以直接在在内存或磁盘上实现,以实现极快地、随机性访问。nio用于合适的应用程序上,带来的性能提升是显著的。

不同的缓冲区有不同的元素类型,就像数组一样。例如,字节数组、整型数组、浮点型数组、字符数组。但不包含字符串数组和对象数组,如果你发现需要,你可以自己实现。以下是这些不同类型的数组共有的操作:

  • Alloact the buffer 分配缓冲区
  • put values in the buffer 向缓冲区存入数据
  • get values from buffer 从缓冲区读出数据
  • 翻转缓冲区
  • 清空缓冲区 将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回0
  • 缓冲区倒带 不影响上界属性。它只是将位置值设回0。
  • 标记缓冲区
  • 重置缓冲区
  • 分割缓冲区
  • 融合缓冲区
  • 复制缓冲区

14.1 利用缓冲区拷贝文件

显而易见,这个程序通过传统的流式I/O也能实现,但是用newio模型编写程序也能运行成功。NIO并没有是以前不可能的事变成可能。然而,如果文件很大而且本地操作系统足够复杂,NIO版本的文件拷贝可能比传统版本更快。

典型的程序大致如下:

import java.io.*;
import java.nio.*;
public class NIOCopier {
 public static void main(String[] args) throws IOException {
 FileInputStream inFile = new FileInputStream(args[0]);
 FileOutputStream outFile = new FileOutputStream(args[1]);
 // copy files here...
 inFile.close( );
 outFile.close( );
 }
}

然而,我要做的不是仅仅从输入流读取并写入到输出流,而是做一些不同的事情。首先,我使用FilelnputStream和FileOutputStream中的getChannel()方法打开两个文件的通道:

FileChannel inChannel = inFile.getChannel( );
FileChannel outChannel = outFile.getChannel( );

接下来,我使用静态工厂方法bytebufferer . allocation():创建一个1兆字节的缓冲区。

要读取数据,需要将缓冲区传递给输入通道的read()方法,就像将字节数组传递给输入流raed()方法一样:

inChannel.read(buffer);

read()方法返回它所读取的字节数。与输入流一样,它不能保证read()方法完全填充缓冲区。它可能读取更少的字节或根本没有字节。当输入数据耗尽时,read()方法返回-1。因此,你通常会像这样做:

long bytesRead = inChannel.read(buffer);
if (bytesRead == -1) break;

现在输出通道需要将缓冲区中的数据写入副本中。不过,在此之前,必须翻转缓冲区。

buffer.flip( );

翻转缓冲区将其从输入转换为输出。

要写入数据,您需要将缓冲区传递给输出通道的write()方法:

outChannel.write(buffer);

但是,这与输出流的write(byte)方法不同,输出流的writer方法保证将数组中的每个字节写入目标。或者抛出一个IOException异常。输出通道的write()方法更像read()方法。它将写入一些字节,但可能不是全部,甚至可能没有。它返回写入的字节数。你可以重复循环直到所有字节被写入,像这样:

long bytesWritten = 0;
while (bytesWritten < bytesRead){
 bytesWritten += outChannel.write(buffer);
}

然而,有一个更简单的方法。缓冲区对象本身知道是否写入了所有数据。hasRemaining()方法能够检查:

while (buffer.hasRemaining( )) outChannel.write(buffer);

这段代码最多能读和写1兆字节。要复制更大的文件,我们必须将所有这些包装成一个循环:

while (true) {
 ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
 int bytesRead = inChannel.read(buffer);
 if (bytesRead == -1) break;
 buffer.flip( );
 while (buffer.hasRemaining( )) outChannel.write(buffer);
}

为每次读取分配一个新的缓冲区既浪费又低效;我们应该重用相同的缓冲区。在我们这样做之前,我们必须通过调用它的clear()方法:

ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
while (true) {
 int bytesRead = inChannel.read(buffer);
 if (bytesRead == -1) break;
 buffer.flip( );
 while (buffer.hasRemaining( )) outChannel.write(buffer);
 buffer.clear( );
}

最后,应该关闭输入和输出通道,以释放通道对象可能持有的任何本机资源:

inChannel.close( );
outChannel.close( );

完整的程序如下:

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class NIOCopier {
 public static void main(String[] args) throws IOException {
 FileInputStream inFile = new FileInputStream(args[0]);
 FileOutputStream outFile = new FileOutputStream(args[1]);
 FileChannel inChannel = inFile.getChannel( );
 FileChannel outChannel = outFile.getChannel( );
 for (ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
 inChannel.read(buffer) != -1;
 buffer.clear( )) {
 buffer.flip( );
 while (buffer.hasRemaining( )) outChannel.write(buffer);
 }
 inChannel.close( );
 outChannel.close( );
 }

在一个非常不科学的测试中,使用带有缓冲流的传统I/O和8192字节的缓冲区,在一个平台(运行Mac OS X 10.4.1的双2.5 ghz PowerMac G5运行)上复制一个大的(4.3 gb)文件需要305秒。扩展和减少缓冲区大小不超过总体数字的5%,都会增加复制的时间,那么扩展和减小缓冲区大小并不能减少它们。(使用像示例14-1那样的1兆字节的缓冲区,实际上增加了超过23分钟的时间。)使用新的I/O作为实现的irExample 14-1大约快了16%,在255秒。一个直接Finder(抱歉这里实在不知道怎么翻译…)复制需要197秒。使用Unix cp命令实际上花费了312。

这表明,新的I/O对文件的从头到尾的移动操作没有太大帮助。新的/O API显然不是解决所有/O性能问题的灵丹妙药。你可以期待在其他两个方面看到最大改进:

  • 与许多客户端同时通信的网络服务器
  • 对大文件部分的重复随机访问

14.2 创建buffer

java nio 有七种buffer对应着其中基本类型

  • ByteBuffer
  • ShortBuffer
  • IntBuffer
  • CharBuffer
  • FloatBuffer
  • DoubleBuffer
  • LongBuffer

这七个缓冲区类都有非常相似的api,它们主要在get方法的返回类型和参数类型上有所不同。在这七个类中,ByteBuffer是最重要的。例如,其中的read()和write()方法FileChannel只接受ByteBuffer作为参数。但是,有一些方法可以将ByteBuffer的视图创建为其他类型之一。因此,你仍然可以在只使用字节的通道上写入int或chars或double。这些模式和DataOutputStream非常相似,DataOutputStream允许你向期望接收字节的流写入int或char或double。

创建buffer的两种主要方式

  • by allocation 分配创建一个新的由内存支持的缓冲区,

  • by wrapping. 包装使用缓冲区作为现有数组的接口。

ByteBuffer bBuffer = ByteBuffer.allocate(1024);//配置很简单。只需将缓冲区的所需容量传递给您想要实例化的类中的静态分配()方法。例如,该语句创建一个新的ByteBuffer,大小为1024字节:

IntBuffer iBuffer = IntBuffer.allocate(500);//This statement creates a new IntBuffer with room for 500 ints:

//这两个缓冲区的分配都将由一个数组来支持。这是。bBuffer包含一个lenath为1024的字节数组,iBuffer包含一个长度为500的int数组。您可以使用array()方法取到对这些数组的引用:
//int[] iData = iBuffer.array( );
//byte[] bData = bBuffer.array( );


//If you already have data in an array, you can build a buffer around it using the wrap( ) methods. For example:
int[] data = {1, 3, 2, 4, 23, 53, -9, 67};
IntBuffer buffer = IntBuffer.wrap(data);

/**
*在这两种情况下,数组都不是副本。这些是缓冲区保存其数据的实际内部数组。改变这些数组中的数据也会改变缓冲区的内容,反之亦然。
*
*
*
/

//然而,还有另一种选择,这就是事情变得非常有趣的地方。并不是所有的缓冲区都是由Java数组支持的。你可以直接使用allocDirect()方法来分配一个数组,而不是allocate()方法。
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

//对于直接缓冲区的APl与间接缓冲区完全相同,除了创建它的工厂方法之外。然而,在内部,计算机可能使用不同的优化技术。例如,它可以将缓冲区直接映射到主存,
//而不是通过中间的Java阵列对象。此外,它还将额外努力将数据存储在一个连续的内存块中。


这样的技巧可以极大地提高大型缓冲区的性能。然而,分配一个直接缓冲区要比分配一个间接缓冲区花费的时间长。因此,直接缓冲区可能在清除时最优,而对于较小的缓冲区来说,最坏的情况要比间接的,基于数组的缓冲区要慢得多。此外,直接缓冲区的确切实现细节是高度依赖于平台的。在一些平台上。直接缓冲区提供了巨大的性能提升。因此,性能大体和流的相同,也可能会更糟糕。如果性能是您主要关注的问题,请确保在使用直接缓冲区之前和之后都要仔细度量。

如果你以后需要知道某个特定的缓冲区是否已经被直接或间接地分配了,isDirect( )方法会告诉你:
public abstract boolean isDirect( )

14.3 Buffer的布局

缓冲区的概念模型是一个固定大小的数组。例如,假设我们分配一个容量为8的IntBuffer:
IntBuffer buffer = buffer.allocate(8);

image

缓冲区的容量在第一次创建时是固定的。缓冲区不会扩展或收缩以适应它们所放置的数据量。试图将更多的数据放入缓冲区中,会抛出BufferOverflowException。这是一个运行时异常,因为溢出的缓冲区通常表示程序错误。

除了一个索引值(index)列表之外,缓冲区还包含一个指向该位置的列表的指针(position)。这个位置是将要被读取或写入的缓冲区的下一个槽的索引。它的值介于0和1之间,小于缓冲区的容量。它最初被设置为0,并且随着数据从缓冲区中读取或读取而增加。您可以使用position()方法获得缓冲区的当前位置:

public final int position( )

你也可以通过将新位置传递给这个位置()方法来改变缓冲区的位置:

public final Buffer position(int new Position)

然而。大多数情况下,你不需要明确地这样做。当数据被放入缓冲区或从缓冲区中检索时,位置会自动更新。例如,假设我们将值7放入缓冲区中,如图14-1:

buffer.put(7);

当整型被放置在位置的缓冲区中,位置自动增加到1,如图14-2所示。

image

image

当向buffer中get数据时,position的值也会指向下一个slot所在的位置。因此这就是为什么要在往buffer中写完文件后 要调用flip()方法。flip()就是将limit置为当前position所在的位置,然后再把position置为0。

14.4 大块的存取 (Bulk put and get)

你已经看到了相关的put和get方法,它们在当前位置插入或取回数据。get\put方法 还能以缓冲区为操作对象进行块级的处理。例如。ByteBuffer有这两种块级的操作的方法:

public final ByteBuffer put(byte[] data)

public ByteBuffer put(byte[] data, int offset, int length)

第一次放入数据时,会在缓冲区的当前position开始放入。position的值根据数组的lenath增加。第二次存入数据,从偏移量开始。这些方法复制数组。在调用put()之后更改数据数组不会改变缓冲区中的数据。

public ByteBuffer get(byte[] data)
public ByteBuffer get(byte[] data, int offset, int length)
//Both methods update the position by the number of bytes returned

对于put和get来说,数组必须符合可用空间。如果您试图将一个较大的数组(或子数组)放入缓冲区中,而不是留出空间,那么put)抛出BufferOverflowException异常。如果您试图获得比缓冲区有数据还存在的更大的阵列(或子阵列))HRows一个BufferUnderflowException。在这两种情况下,缓冲区都处于它的原始状态,并且没有传输数据。

其他类型的缓冲区 有着类似的操作方法 put/get方法,只是返回类型和参数不一样

public final IntBuffer put(int[] data)
public IntBuffer put(int[] data, int offset, int length)
public IntBuffer get(int[] data)
public IntBuffer get(int[] data, int offset, int length)

public final DoubleBuffer put(double[] data)
public DoubleBuffer put(double[] data, int offset, int length)
public DoubleBuffer get(double[] data)
public DoubleBuffer get(double[] data, int offset, int length)

14.5 缓冲区mark and Reset操作

跟输入流一样,缓冲区能够标记和重置。

public final Buffer mark( )
public final Buffer reset( ) throws InvalidMarkException

mark的初始值是未设定的,调用缓冲区的mark()方法会将mark置为当前位置的position值,reset()会返回先前的position。不像输入流,缓冲区没有markSupported()方法。所有的Buffer都支持mark()和reset(),因为他们都继承自Buffer类。

mark的值总是小于等于当前position值、和是limit值。如果limit或position值被设置为小于当前标记的值,则标记将被清除。在没有标记的情况下进行复位会抛出一个InvalidMarkException。

14.6. Absolute Put and Get

暂不译

14.7 收缩 Compaction

缓冲区通常用于连续的阅读和写作。也就是说,首先从文件、网络连接或其他来源读取数据,并存储在缓冲区中。下一个。数据从缓冲区中抽干,并写入另一个文件、网络连接或其他目的地。然而。从缓冲区mav中吸取数据的输出不像填充缓冲区的输入那样移动。例如。如果数据是从一个文件中读取并写入到网络连接上,那么输入很可能会大大超过输出。

为了处理这种场景,许多缓冲区可以通过调用它们的紧凑的方法来压缩:
public abstract ByteBuffer compact( )

Compacting在当前位置之前从缓冲区中删除了所有数据,然后将剩余的数据在缓冲区中向后移动到开始。最后,限制被设置为容量,并且位置被设置为第一个空位置的index。举个例子,假设我们放入5个ints intor。是这样的:

太简单了 不想翻译了…

Compacting removes all the data from the buffer before the current position, then shifts the remaining data backwards in the buffer to the
beginning. Finally, the limit is set to the capacity, and the position is set to the first empty space. For example, suppose we put five ints into
a buffer, like this:
IntBuffer buffer = IntBuffer.allocate(8);
buffer.put(10).put(20).put(30).put(40).put(50);

We now flip the buffer to prepare it for draining and read three ints from it using a bulk get:
buffer.flip( );
int[] data = new int[3]
buffer.get(data);

image

Now the buffer is in the state shown in Figure 14-16.

image

如果我们仅仅只是想从当前位置继续抽取数据,当然没问题。但是,如果我们现在想要用更多的数据填充缓冲区,我们就有几个问题。首先,这个位置被设置为3。不是5。如果我们现在开始。我们会覆盖未处理过的数据。我们可以把这个位置移动到5,也可以把这个限制移到8,但是我们仍然只有三个空槽,我们可能有更多的数据。我们可以清除缓冲区。但是我们会丢失未读的数据。任何对位置和限制的操纵都不能解决问题。因而 ,我们调用compact():

这会将缓冲区置于如图14-17所示的状态。正如您所看到的,剩下的两个int仍然可用,并且这个位置已经被更新,以允许尽可能多的数据放在缓冲区中,而不会丢失任何未处理的元素。

image

代码示例 : 用nio拷贝文件。

	public static void main(String[] args) throws IOException {
		File readFrom = new File("readFrom.txt");
		File writeTo = new File("writeTo.txt");
		if (!(readFrom.exists() && writeTo.exists())) {
			readFrom.createNewFile();
			writeTo.createNewFile();
		}
		FileInputStream in = new FileInputStream(readFrom);
		FileOutputStream out = new FileOutputStream(writeTo);

		FileChannel inChannel = in.getChannel();
		FileChannel outChannel = out.getChannel();

		ByteBuffer buffer = ByteBuffer.allocate(128);

		int readByte = 0;

		// 我写的
		while ((readByte = inChannel.read(buffer)) > 0) {
			buffer.flip();
			while (buffer.hasRemaining()) {
				System.out.println("开始写文件...");
				outChannel.write(buffer); //流式write()方法 保证每个字节都能写出去,而nio write()不能保证,可能全部写出去,也肯能一个没写。
			}
			buffer.compact(); // 为什么要用compact? 因为读文件的速度是快于写文件的,这样子的程序能更快点,不会因为缓冲区的数据没流干,而影响数据读入缓冲区、
		}

		// 书上代码 java io  好像有问题 一直不能退出循环。
		
		// hasRemaining()方法能够检查:缓冲区对象是否写入了所有数据 position < limit
//		  while (readByte >= 0 || buffer.hasRemaining()) { 
//			  if (readByte != -1) { 
//              readByte = inChannel.read(buffer);
//			  }
//            buffer.flip();
//			  outChannel.write(buffer); buffer.compact(); // comapct 
//		  }
		inChannel.close();
		outChannel.close();
	}

如果outout倾向于阻塞,而输入没有,那么这个程序可能比示例14-1快一些,但话说回来,它可能没有。与任何详细的性能分析一样,实际结果因系统和平台的不同而不同。一个更好,更可靠的解决方案是使用非阻塞I/O,我将在第16章中讨论,

14.8 副本

有时候,制造缓冲区的副本会很有用。复制不是拷贝或者克隆。它是一个拥有和原来缓冲区相同内部数据的新的缓冲区对象,但是具有独立的mark、limit、position值。将数据放入缓冲区的原始缓冲中的元素的更改会影响副本,反之亦然。然而,从一个缓冲区获取数据,或对其进行fipping操作、reset重置操作、rewinding恢复或clear清除操作,对另一个缓冲区没有任何影响。当你希望将相同的固定内容传递给同时或独立运行的多个不同操作时,副本通常很有用。

public abstract ByteBuffer duplicate( )

创建副本时,它的位置和标记设置为0,其限制设置为容量,而不考虑原始缓冲区中的位置、标记和限制。

14.9 切片

一个切片类似于一个复制品。但是,它只包含子序列,而不包含原始数据的完整副本。这个子序列从生成切片时的原始位置开始,一直持续到原始缓冲区的limit。因为数据是共享的。对原始缓冲区中的元素的更改也会更改切片。反之亦然。然而。切片中的position、limit、mark都与原始的位置、极限和标记无关。切片的容量将小于或等于原始容量。此外,他们index不同。初始位置5可能是切片中的位置0。在这种情况下,初始位置6是切片中的位置1,位置7是位置2。等等。

例如 ,假如我们在IntBuffer缓冲区中放入8个10的倍数

IntBuffer original = IntBuffer.allocate(8);
original.put(10).put(20).put(30).put(40).put(50).put(60).put(70).put(80);

把poisition等于4的位置进行切片

original.position(4);
IntBuffer slice = original.slice( );

现在我们有两个缓冲区,如图14-19所示。我们可以在不改变另一个的情况下从其中一个得到另一个。但是,放入切片或从位置4开始放入原始数据将影响其他缓冲区。

切片通常用于截断数据标题。为例。一个PNG图像包含一个初始的8字节签名(十六进制),0x89 0x50 0x4E 0x47 OxOD OxOA Ox1A OxOA, 后面跟着三个或更多的数据块。每个块由四个部分组成:

  1. 一个4字节的大端整数,给出数据块中的数据长度。无符号,值在0和2的31次方之间。
  2. 4字节的ASCIl签名,如IHDR或时间标识块的类型。
  3. 字段1给出长度的块数据。这可以是空的。(也就是说,它的长度可以是0)
  4. 块的4字节CRC校验和。校验和是通过field2和field3计算的:即签名和数据,而不是长度。

pngBuffer.position(8);
pngBuffer = buffer.slice( );

14.10 Typed Data

I/O实际上是关于字节而不是int,不是text,不是doublebytes。读取和写入的字节可以用各种方式解释,但就文件系统、网络套接字或其他几乎所有已知的东西而言,它们只是字节。详细的解释由程序负责读取和写入这些字节。因此,在下一章中,当您发现不同种类的channelsTCP通道、UDP通道、文件通道以及like通道几乎都只处理字节缓冲区,而几乎从不处理int缓冲区、char缓冲区或其他任何缓冲区时,应该不会感到意外。

然而。有时候,假装I/O与其他事情有关是很方便的。如果程序在处理ints。能够读取和写入int(而不是字节)会很好。在传统的I/O中,DatalnputStream和DataOutputStream填补了这个空白。在新I / O。视图缓冲区满足这一需求。

14.10.1 视图缓冲区

ByteBuffer类,并且只有ByteBuffer类,可以将自己的视图表示为另一种类型的缓冲区:

IntBuffer,CharBuffer,ShortBuffer,LongBuffer,FloatBuffer,DoubleBuffer.

视图缓冲区是以ByteBuffer为基础的。 当你将int(例如1,789,554)写入视图缓冲区时,缓冲区将与此int相对应的四个字节写入底层缓冲区。所使用的编码与DataOutputStream所使用的编码相同,除了可能的字节顺序调整。

**视图缓冲区具有根据其类型定义的位置、标记、限制和容量。**基础ByteBuffer具有以字节为单位定义的位置、标记、限制和容量。

如果视图缓冲区是IntBuffer,那么底层ByteBuffer的位置、标记、限制和容量将是视图缓冲区的位置、标记、限制和容量的4倍,因为int中有4个字节。

如果视图缓冲区是一个双缓冲区,那么下面的bytebuffer的位置、标记、限制和容量将是视图缓冲区的位置、标记、限制和容量的8倍,因为双字节中有8个字节。(如果缓冲区的大小不是视图类型大小的精确倍数,则会忽略末尾的多余字节。

ByteBuffer中创建视图缓冲区的六个方法:

public abstract ShortBuffer asShortBuffer( )

public abstract IntBuffer asIntBuffer( )

public abstract LongBuffer asLongBuffer( )

public abstract FloatBuffer asFloatBuffer( )

public abstract DoubleBuffer asDoubleBuffer( )

public abstract CharBuffer asCharBuffer( )

在示例8-3中,您看到了DataOutputStream如何将文件中的平方根写成双精度形式。示例14-3使用新的I/O API而不是流。

首先,分配一个足以容纳1001个双人间的bytebufferer。接下来,创建一个双缓冲的ByteBuffer视图。平方根放在这个视图缓冲区中。最后,将底层ByteBufferis写入文件。

/**
 * @Example 14-3
 *下午7:45:25 2018年8月25日
 * java io
 * 用视图缓冲区写入doubles数据
 */
public class RootsChannel {
	final static int SIZE_OF_DOUBLE = 8;
	final static int LENGTH = 1001;
	public static void main(String[] args) throws IOException {
	// Put 1001 roots into a ByteBuffer via a double view buffer
	ByteBuffer data = ByteBuffer.allocate(SIZE_OF_DOUBLE * LENGTH);
	DoubleBuffer roots = data.asDoubleBuffer( );
	while (roots.hasRemaining( )) {
	double sqrt = Math.sqrt(roots.position());
	System.out.println(sqrt);
	roots.put(sqrt);
	}
	// Open a channel to the file where we'll store the data
	FileOutputStream fout = new FileOutputStream("roots.dat");
	FileChannel outChannel = fout.getChannel( );
	outChannel.write(data);
	outChannel.close( );
	}
}

    有趣的是,本例中的ByteBuffer不需要翻转。因为原始缓冲区和视图缓冲区有各自的位置和限制,所以将数据写入视图缓冲区不会改变原始的位置;
    它只改变它的数据。当我们准备将来自oriainal缓冲区的数据写入通道时,原始缓冲区的位置和限制仍然分别具有默认值0和容量。

14.10.2 Put Type Method

只要您希望只编写一种数据类型(例如,所有数据都是双精度的,因为文件通常需要包含多种类型的数据:双精度、int、chars等。例如,PNG文件包含无符号整数、ASCII字符串和原始字节。为此,ByteBuffer有一系列的put方法,这些方法采用其他基本类型:

  • public abstract ByteBuffer putChar(char c)
  • public abstract ByteBuffer putShort(short s)
  • public abstract ByteBuffer putInt(int i)
  • public abstract ByteBuffer putLong(long l)
  • public abstract ByteBuffer putFloat(float f)
  • public abstract ByteBuffer putDouble(double d)

每一个都以对应类型的大小推进位置。例如,putChar和putShort将位置增加2,putlnt和putFloat将位置增加4,putlong和putDouble将位置增加8。

当然,有相应的get方法:

  • public abstract char getChar( )public abstract short getShort( )
  • public abstract int getInt( )
  • public abstract long getLong( )
  • public abstract float getFloat( )
  • public abstract double getDouble()

这些都是从当前位置得到的。每个方法都有一个绝对的变体,允许您指定放置或获取值的位置:

  • public abstract ByteBuffer putChar(int index , char c)
  • public abstract ByteBuffer putShort(int index , short s)
  • public abstract ByteBuffer putInt(int index , int i)
  • public abstract ByteBuffer putLong(int index , long l)
  • public abstract ByteBuffer putFloat(int index , float f)
  • public abstract ByteBuffer putDouble(int index , double d)
  • public abstract char getChar(int index)
  • public abstract short getShort(int index)
  • public abstract int getInt(int index)
  • public abstract long getLong(int index)
  • public abstract float getFloat(int index)
  • public abstract double getDouble(int index)

14.10.3. Byte Order

暂时不译

14.11只读缓冲区

缓冲区可以只读。例如,由CD-ROM上的文件支持的缓冲区是只读的。缓冲区也可能是只读的,如果它们是映射到没有写权限的文件的内存,或者映射到网卡上的输入缓冲区。包装CharSequence的CharBuffer是只读的。如果您不确定是否可以写入缓冲区,那么可以在尝试这样做之前调用invokesReadOnly():

public abstract boolean isReadOnly( )

你可以创建自己的只读缓冲区:

public abstract ByteBuffer asReadOnlyBuffer( )
//以这种方式创建的缓冲区实际上是原始缓冲区的一个视图缓冲区。对于以这种方式创建的缓冲区,“只读”有点用词不当。虽然不能放入这样的缓冲区,但仍然可以将值放入原始的底层缓冲区,以这种方式进行的任何更改都将立即反映在覆盖的只读缓冲区中。

没有只写缓冲区,所有的缓冲区都能够被读。

14.13 内存映射

内存映射I/O不是所有问题的解决方案。只有当数据集等于或超过可用内存时才适用。

14.13.1 创建映射缓冲区

MappedByteBuffer类将文件直接映射到ByteBuffer中。你可以使用put() get() putint() getint()等其他操作字节缓冲区的方法。然而,put和get可以直接访问文件,而无需将大量数据复制到RAM中

映射字节缓冲区是使用FileChannel类中的map()方法创建的:
public abstract MappedByteBuffer map(FileChannel.MapMode mode,long position, long size) throws IOException

内存映射的三种操作模式:

  1. FileChannel.MapMode.READ_ONLY 只能读取缓冲区中的数据 不能修改
  2. FileChannel.MapMode.READ_WRITE 既能读取也能写入数据
  3. FileChannel.MapMode.PRIVATE 可以从缓冲去读取数据,也能存入数据,但是存入的数据只能通过这个缓冲区可见,文件本省没有修改。

示例代码:

andomAccessFile file = new RandomAccessFile("test.png", "rw");
FileChannel channel = file.getChannel( );
ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length( ))

缓冲区的position初始为0,limit初始为缓冲区的容量。容量是第三个参数传递的值。它们不一定与文件本身的零位置和长度相同。如果你不需要全部的东西,那么你可以并且经常只做一个大文件的一部分内存映射。例如,这个代码片段映射了在初始8字节签名之后的PNG文件:

ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 8 , file.length( )-8);

文件的初始position不能为0.但是,如果文件打开以便写入,则该容量可能超过文件的长度。如果是,文件将被扩展到请求的长度。

可用的模式取决于创建FileChannel的基础组件

  • 随机访问文件可以以只读模式或读/写模式映射。
  • 文件输入流可以以只读模式映射。文件输出流根本无法映射。
  • 通常,RandomAccessFile是源文件。

Java没有指定在映射文件时,如果另一个进程甚至是同一程序的另一部分更改文件,会发生什么。缓冲区对象ByteBuffer可能显示,也可能不显示出这种变化。如果反射确实反映了这些变化,那么反射可能是即时的,也可能不是即时的。这将随平台的不同而不同。

14.13.12MappedByteBuffer的方法

除了ByteBuffer共有的方法,MappedByteBuffer还有自己独有的方法。

1、load()

尝试将整个缓冲区加载到主内存中.这可能使访问缓冲区的速度更快,但也可能不是这样。如果数据大于Java的堆大小,这可能会导致一些页面错误和磁盘抖动。

2、isLoaded()

是否加载了缓冲区

3、force()

向MappedByteBuffer写入了数据后,需要冲洗它(flush),就像输出流一样,需要调用flush()方法,不过在这里,你调用的是force()方法。

例14-4将整个文件映射到内存中。然后将0写入文件,然后是1,然后是a产生的随机数据
java.util。SecureRandom对象。每次运行之后,缓冲区都必须确保数据确实写入磁盘。否则,只
最后一个通行证可能被提交。如果不强制执行数据,可能会留下对手可以分析的磁性模式,即使实际文件内容相同。

// 使用MappedByteBuffer擦除文件
public class SecureDelete {
public static void main(String[] args) throws IOException {
File file = new File(args[0]);
if (file.exists( )) {
SecureRandom random = new SecureRandom( );
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel channel = raf.getChannel( );
MappedByteBuffer buffer
= channel.map(FileChannel.MapMode.READ_WRITE, 0, raf.length( ));
// overwrite with zeros
while (buffer.hasRemaining( )) {
buffer.put((byte) 0);
}buffer.force( );
buffer.rewind( );
// overwrite with ones
while (buffer.hasRemaining( )) {
buffer.put((byte) 0xFF);
}
buffer.force( );
buffer.rewind( );
// overwrite with random data; one byte at a time
byte[] data = new byte[1];
while (buffer.hasRemaining( )) {
random.nextBytes(data);
buffer.put(data[0]);
}
buffer.force( );
file.delete( );
}
}

这个程序不是特别快。在相当令人印象深刻的硬件上,它可以擦除略多于100K每秒。可以通过一次重写多于一个字节来进行一些改进,但是如果你这样做,那么要小心,最终的写操作不会写得太多,并且会导致BufferOverflowException异常。

15. 通道

15.1 通道接口

许多通道功能被抽象为一系列不同的接口。使用接口而不是抽象类,因为经常需要混合和匹配不同的组件。有些通道可以读取,有些可以写入,有些可以读取和写入。有些通道是可中断的,有些则不是。有些渠道分散,有些渠道聚集。实际上,这些功能几乎出现在任何组合中。

15.1.1 通道

java.nio.channels.Channel(所有通道的父接口)。这个接口定义了所有通道实现的唯一两个方法,isOpen()和close():

15.1.1 可读字节通道和可写字节通道

次重要的接口是ReadableByteChannel、WritableByteChannel,它们用于读写字节。一些通道既能读又能写,但是大多数通道只能做其中一样。理论上,通道可以作用于ints、doubles、strings、等等。但实际上,通常是字节。

ReadableByteChannel、WritableByteChannel这两个接口板都只声明了一个方法。

ReadableByteChannel: public int read(ByteBuffer target) throws IOException

WritableByteChannel : public int write(ByteBuffer source) throws IOException

15.1.2.1 字节通道

实现了ByteChannel接口是既可以读又可以写的通道。这只是一个实现了ReadableByteChannel和WritableByteChannel的简化接口。它不声明任何其他方法。

在核心库中,其实现类有SocketChannel、DatagramChannel、FileChannel。

15.1.3 聚集通道、分散通道

大多数实现了ReadableByteChannel的通道也实现了它的子接口ScatteringByteChannel 。 这个接口添加了两个可以使用多个缓冲区的读方法。

public long read(ByteBuffer[] dsts) throws IOException

public long read(ByteBuffer[] dsts, int offset, int length) throws IOException

在第一个缓冲区被填满后,从通道读取的数据被放在dsts的第二个缓冲区中。在第二个缓冲区填满之后,数据将被放在第三个缓冲区中,以此类推。第二个方法是相同的,只是它从offset的缓冲区开始,然后通过长度缓冲区继续。也就是说,偏移量和长度定义了要从dsts数组中使用的缓冲区的子集,而不是每个单独缓冲区中的偏移量和长度。

类似地,大多数实现writablebytechannelel的类也实现了它的子接口,即集合GatheringByteChannel 这个接口又添加了两个write()方法,从缓冲区数组中抽取数据:

public long write(ByteBuffer[] srcs) throws IOException

public long write(ByteBuffer[] srcs, int offset, int length) throws IOException

在第一个缓冲区清空之后,通道开始抽取第二个缓冲区。在第二个缓冲区为空之后,从第三个缓冲区中提取数据,依此类推。同样,第二个方法,只是它开始在偏移量处抽取缓冲区,并通过长度缓冲区继续。

当写入到通道的数据由几个不同的片段组成时,这是非常有用的。例如,HTTP服务器可能将HTTP头存储在一个缓冲区中,而将HTTP头存储在另一个缓冲区中,然后使用集合写来同时写。如果你正在编写包含单个记录的文件,则可以将每个记录存储在单独的缓冲区中。

实例代码:

	public static void main(String[] args) throws IOException {
	/*	loop :for (int i =0 ; i<10; i++) {
			int rand = (int) ((Math.random()*10)+1);
			System.out.println(rand);
			if (rand < 6) {
				continue loop;
			} else  break;
		}*/
		
		if (args.length < 2) {
			System.err.println("Usage: java NIOCat inFile1 inFile2... outFile");
			return;
			}
			ByteBuffer[] buffers = new ByteBuffer[args.length-1];
			for (int i = 0; i < args.length-1; i++) {
			RandomAccessFile raf = new RandomAccessFile(args[i], "r");
			FileChannel channel = raf.getChannel( );
			buffers[i] = channel.map(FileChannel.MapMode.READ_ONLY, 0, raf.length( ));
			}
			FileOutputStream outFile = new FileOutputStream(args[args.length-1]);
			FileChannel out = outFile.getChannel( );
			out.write(buffers);
			out.close( );
			}
	}

示例15-1做了一个危险的假设:只有当write()方法从每个缓冲区写入每个字节时,它才会工作。对于阻塞模式下的文件通道,这种情况可能会出现,并且大多数情况下都是这样。一个集合写尝试写所有可能的字节,而且通常它会这样做。然而,一些渠道可能是有限的。例如,在非阻塞模式下运行的套接字通道不能写入比本地TCP缓冲区容纳更多的字节。一个更健壮的解决方案将在循环中连续写,直到所有缓冲区都没有剩余的数据

the buffers had any remaining data:
outer: while (true) {
out.write(buffers);
for (int i = 0; i < buffers.length; i++) {
if (buffers[i].hasRemaining( )) continue outer;
}
break;
}
Honestly, this is ugly, and on a nonblocking channel I'd be inclined to just write the buffers individually instead, like so:
for (int i = 0; i < buffers.length; i++) {
while (buffers[i].hasRemaining( )) out.write(buffers[i]);
}

15.2 文件通道

FileChannel实现了GatheringByteChannel和ScatteringByteChannel接口。大多数情况下,文件通道不是可读就是可写的,而不是两者兼具。
创建文件通道的三种方式:

  • 调用FileInputStream对象的getChannnel()方法。 //可读通道
  • 调用FileOutputStream对象的getChannel()方法。 //可写通道
  • 调用RandomAccessFile对象的getChannel()方法。 //可读可写通道

文件通道可以被锁定,其它进程将无法访问他们


文件通道可以被冲洗。

15.2.1 通道间传输数据

前面的章节,利用FileChannel和ByteBuffer演示了如何拷贝文件,其实还有一种简化方法,主要是利用了FileChannel的两个transfer方法。


public abstract long transferFrom(ReadableByteChannel src, long position, long count)

public abstract long transferTo( long position, long count, WritableByteChannel target)


第一个方法从源通道复制到文件的起始位置。第二个方法从文件复制计数字节到目标通道开始的位置。

示例代码:

	public static void main(String[] args) throws IOException {
		FileInputStream inputStream = new FileInputStream("readFrom.txt");
		FileOutputStream outputStream = new FileOutputStream("writeTo.txt");
		FileChannel inChannel = inputStream.getChannel();
		FileChannel outChannel = outputStream.getChannel();
		inChannel.transferTo(0, inChannel.size(), outChannel);
//		还可以这样写 :是不是很神奇!!!
//		outChannel.transferFrom(inChannel, 0, inChannel.size()) ;
		inChannel.close();
		outChannel.close();
	}

源通道和目标通道都不是文件。这些方法能够将数据从文件通道传送到网络通道中或者是网络通道到文件中

transferTo()和transferFrom()都不能保证传输所需的字节。但是,它们比InputStream的多字节read()方法更可靠。只有在输入通道没有那么多字节或者输出通道是非阻塞的情况下,这些方法才能传递计数字节。这两种情况在这里都不是。

这可能只是通过缓冲区将数据从一个通道移动到另一个通道的捷径,如示例14-1所示。然而,
在使用此方法时,有些平台可以更直接、更快速地传输字节。可能不使用中间缓冲。
因此,当在文件之间移动数据时,应该尽可能使用transferTo()或transferfrom()。他们不应该
与手动管理缓冲区相比,它的速度明显要慢得多,有时甚至可能快得多。

15.2.2 随机存取

虽然文件通常由流读取,但文件不是流。与网络套接字)不同,文件不必按顺序读取。的
磁盘控制器可以很容易地将自己重新定位为在文件中的任何给定位置进行读写,而文件通道可以利用这一点能力。

每个文件通道都知道它在文件中的当前位置。这是以字节为单位度量的,并由theposition()函数返回

public abstract long position( ) throws ClosedChannelException, IOException

当数据从文件中读取或写入文件时,位置将自动更新。然而,你也可以改变位置
手动使用这个重载位置()方法:

public abstract FileChannel position(long newPosition)
throws IllegalArgumentException , ClosedChannelException, IOException

您可以将文件指针定位到超过文件末尾的位置。试图从这个位置读取数据返回-1,表示文件结束。当然
与InputStream不同的是,您可以重新定位文件指针并在文件的前面重新读取,尽管已经完成了它的末尾。
试图写入超过文件结束自动展开文件。你唯一不能做的事情就是在开始之前读或写
该文件。将位置设置为负数会引发anIllegalArgumentException。

如果您将文件指针设置在文件的末尾之后,然后开始写入,则在旧端和新位置之间的文件内容为
系统的依赖。在一些系统上,这个区域可能被磁盘上的随机数据填充。在很多情况下这
可能是一个安全漏洞,因为它可能会暴露要删除的数据。因此,你真的不应该这样做,除非你知道
稍后您将返回并在关闭文件之前填写这些字节。

public abstract long size( ) throws IOException //返回文件的大小。

public abstract FileChannel truncate(long size) throws IOException //缩短(截断)文件。

但是,被截断的字节仍然存在于磁盘上,直到某个进程重写它们为止。具有安全意识的应用程序可能希望在截断数据之前覆盖它们。

15.2.3 线程和锁

与流不同,从多个并发线程访问文件通道是安全的。多个线程可以同时从同一文件通道读写。读取使用文件中的绝对位置可能是真正同步的。Writes and relative
reads queue up behind each other(怎么翻译???)。操作块,以保持文件处于良好定义的状态。写入不允许重叠。

尽管如此,不同线程的写入顺序仍然不可预测。对于某些用例,例如日志文件,这可能无关紧要.真正最重要的是每次写都是原子的,所有的写之间的相对顺序不重要。然而,很多时候,更多控制是必要的。

应用程序有时需要对特定文件的独占访问。例如,如果两个不同的字处理程序试图同时编辑同一文档,冲突几乎是不可避免的。文件通道允许Java应用程序尝试锁定给定的文件或文件的一部分以进行独占访问。当然,这种尝试可能并不总是成功。另一个进程可能已经锁定了相同的文件。

public final FileLock lock( ) throws IOException

这个方法会阻塞,直到它能够获得文件上的锁为止。如果它不能获得文件上的锁,它会抛出各种ioexception中的一个,这取决于它为什么不能获得锁。除了通常的原因(IOException,ClosedChannelException,等等)之外,如果另一个线程在等待锁时中断了这个线程,它还可能抛出一个filelockinterruptionexception

在Windows上,锁是强制性的,由操作系统强制执行。一旦锁定了一个文件,没有其他程序可以访问它(尽管相同VM中的另一个线程可以)。但是,在Unix上,大多数时候,锁只是建议性的。也就是说,另一个进程应该检查文件是否被锁定,如果是,则等待。但是,操作系统不强制执行。

对于某些情况,不需要锁定整个文件。相反,您可以只在指定的位置请求一个位置和连续的特定字节数:

public abstract FileLock lock(long position, long size, boolean shared) throws IOException

位置和大小限制的范围不需要包含在文件中。您可以锁定超出文件末尾或完全超出文件之外的区域。如果是,那么如果文件增长,锁将被保留。如果文件扩展到您锁定的范围之外,超过锁定边界的新内容不会被锁定。

一些操作系统,包括Windows和Unix,支持共享锁。共享锁允许多个应用程序(所有应用程序都具有共享锁)访问文件。但是,没有一个应用程序可以独自占有锁。如果第三个参数为真,lock()将尝试获得共享锁。然而,如果操作系统不支持这个功能,它只会等待一个独占锁。共享锁只允许在可读的文件(例如在只写通道上不能有共享锁)。非共享锁只允许在只能写的文件中(例如,在只读通道上不能有非共享锁)。

//这两个方法都可以等待不确定时间,如果你想获取锁,又不想阻塞整个线程。
public final FileLock tryLock( ) throws IOException<br/>
public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException<br/>

//这些方法的作用与lock()相同,只是它们立即返回。如果文件已经被锁定,这两个方法将返回
null。如果由于其他原因无法锁定文件,这些方法将抛出IOException。

15.2.4 文件锁

所有四个lock()和trylock()方法都返回一个表示文件上的锁的filelock对象。这个对象的主要目的是释放锁当你完成文件:

public abstract void release( ) throws IOException //释放锁

public final boolean overlaps(long position, long size) // 在给定的范围内是否存在锁(被锁定的部分可能是给定范围的子集)

public final FileChannel channel( )//返回锁定文件的通道

public final long position( )//返回锁定范围开始的字节索引

public final long size( )//返回锁定范围的长度

public final boolean isShared( )//如果是共享锁,返回true

public abstract boolean isValid( )//锁被释放或者通道被关闭时 返回false

示例代码:

public class LockingCopier {
public static void main(String[] args) throws IOException {
FileInputStream inFile = new FileInputStream(args[0]);
FileOutputStream outFile = new FileOutputStream(args[1]);
FileChannel inChannel = inFile.getChannel( );
FileChannel outChannel = outFile.getChannel( );
FileLock outLock = outChannel.lock( ); //该输出通道是仅可写,因此需要非共享锁,所以使用无参的lock()方法。
FileLock inLock = inChannel.lock(0, inChannel.size( ), true);//该输入通道仅可读的,因此我们需要设置lock()方法第三个参数为true,获取共享锁。
inChannel.transferTo(0, inChannel.size( ), outChannel);
outLock.release( );//从技术上说release()方法不需要,因为关闭通道就会释放锁。
inLock.release( );
inChannel.close( );
outChannel.close( );

}
}

15.2.5 Flushing 冲洗

跟文件流一样,文件通道也能被缓冲。两者的实现方式是不一样的。,一个文件通道的缓冲很可能反映本机文件系统的缓存,而outputstream的缓冲区通常反映操作在虚拟机里面。
文件通道中的冲洗方法是force(),而不是flush().
public abstract void force(boolean metaData) throws IOException //参数指定除了提交文件的内容,是否还应该更改任何文件元数据(例如,最后一次修改时间、名称更改等等)
提交。

  • force()只能保证在本地文件上工作。挂载在网络上的磁盘(比如NFS文件系统)上的文件可能是也可能不是是被迫的。
  • 只有使用FileChannel方法编写的数据才会被强制执行。使用内存映射编写的数据可以或不能写出来。对于内存映射文件,使用MappedByteBuffer类的force()方法。

流与通道间的转换

通道很炫酷,但流不会消失。在许多情况下,特别是对于少量的数据,流会更快。其次,他们更方便。有时它们是遗留API的一部分。例如,我见过很多Java的XML库。我甚至写了一两个,但我还没有遇到一个使用缓冲区和通道。它们都对流有很深的依赖性。因此,即使在使用通道时,也会发现需要与基于流的I/O交互的情况。

java.nio.Channel提供了八个静态方法将通道转换为流,反之也提供了流到通道的方法。

public static InputStream newInputStream(ReadableByteChannel ch) //将可读通道(包括文件通道)转换为InputStream 。

public static OutputStream newOutputStream(WritableByteChannel ch) //将可写通道转换为Outputstream


小策略 :加快文件系统的访问

请看下面的代码片段:

XMLReader parser = XMLReaderFactory.createXMLReader( );
FileInputStream in = new FileInputStream("document.xml");
FileChannel channel = in.getChannel( );
in = Channels.newInputStream(channel);
parser.parse(in);

你可能对这段代码的地三四行很迷惑,本身就有了inputstream,为什么要得到通道,然后又转成inputstream呢?。到底得到了什么?

这样做不同之处在于原始I/O现在是用通道而不是流来完成的。原始的FilelnputStream仅用于创建通道。它的read()方法从未被调用。实际的磁盘读取是由本地文件通道代码完成的,它应该非常快。当然,这都是假设。这种策略是否能真正提高性能,必须在计划运行代码的特定系统上进行仔细的评估。

使用通道装换而来的流而不是原始的流的优势:不像大多数流,它们是线程安全的,这些流能够在多线程间共享,java能够确保读写是原子性的而不会相互干扰,仅这一点就足以让我们使用这些方法,而不是直接创建流。

15.3.2 将流转换为通道

public static ReadableByteChannel newChannel(InputStream in)

public static WritableByteChannel newChannel(OutputStream out)

流转换为通道的代码实例:

public class NIOUnzipper {
public static void main(String[] args) throws IOException {
FileInputStream fin = new FileInputStream(args[0]);
GZIPInputStream gzin = new GZIPInputStream(fin); //解压文件流
ReadableByteChannel in = Channels.newChannel(gzin);//将流转换为可读字节通道
WritableByteChannel out = Channels.newChannel(System.out);//将标准输出流转换为可写字节通道
//通过中间缓冲区,将解压数据从一个通道拷贝到另一个。
ByteBuffer buffer = ByteBuffer.allocate(65536);
while (in.read(buffer) != -1) {
buffer.flip( );
out.write(buffer);
buffer.clear( );
}
}

15.3.3 通道转换为字符流 字节流

通道也有四种方法在字节通道和字符流间进行转换

1、public static Reader newReader(ReadableByteChannel channeString characterSetName)

2、public static Writer newWriter(WritableByteChannel channel,String characterSetName)


参数二指定编码解码对象,因newReader/newWriter是缓冲的,参数三可以指定缓冲区大小。


3、public static Reader newReader(ReadableByteChannel channel,CharsetDecoder decoder, int minimumBufferCapacity)

4、public static Writer newWriter(WritableByteChannel channel,CharsetEncoder encoder, int minimumBufferCapacity)

15.4 套接字通道

网络套接字通道一般都是可读可写的,但是读写的操作方法和文件通道类似,但是你可能需要用不同的缓冲区来区分读和写。

Sockets:

  • 套接字必须显式连接。
  • 可以断开连接的套接字。
  • 套接字可以选择。
  • 套接字非阻塞I / O的支持。

创建ServerSocket的方法:


public static SocketChannel open( ) throws IOException //创建一个未连接任何东西的SocketChannel通道

public static SocketChannel open(SocketAddress remote) throws IOException//连接过的

检查SocketChannel是否连接:


public abstract boolean isConnected( )

假如你要写一个程序,它能从远程下载数据然后保存在文件中。思路如下:

  1. 从命令行读取URL和文件名。
  2. 打开文件的FileChannel。
  3. 打开一个SocketChannel到远程服务器。
  4. 连接通道。
  5. 在套接字通道上写入HTTP请求标头。
  6. 将响应从服务器传输到文件。
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class HTTPGrab {
public static void main(String[] args) throws IOException {
if (args.length != 2) {
System.err.println("Usage: java HTTPGrab url filename");
return;
}
URL u = new URL(args[0]);
if (!u.getProtocol( ).equalsIgnoreCase("http")) {
System.err.println("Sorry, " + u.getProtocol( )
+ " is not supported");
return;
}
String host = u.getHost( );
int port = u.getPort( );
String file = u.getFile( );
if (file == null) file = "/";
if (port <= 0) port = 80;
SocketAddress remote = new InetSocketAddress(host, port);SocketChannel channel = SocketChannel.open(remote);
FileOutputStream out = new FileOutputStream(args[1]);
FileChannel localFile = out.getChannel( );
String request = "GET " + file + " HTTP/1.1\r\n"
+ "User-Agent: HTTPGrab\r\n"
+ "Accept: text/*\r\n"
+ "Connection: close\r\n"
+ "Host: " + host + "\r\n"
+ "\r\n";
ByteBuffer header = ByteBuffer.wrap(request.getBytes("US-ASCII"));
channel.write(header);
ByteBuffer buffer = ByteBuffer.allocate(8192);
while (channel.read(buffer) != -1) {
buffer.flip( );
localFile.write(buffer);
buffer.clear( );
}
localFile.close( );
channel.close( );
}
}

15.5 ServerSocket通道

ServerSocketChannel类是NIO真正开始发光的地方。使用ServerSocketChannel的一个服务器线程可以管理许多不同的客户机。这里的核心是非阻塞I/O。

使用新的IO API编写服务器的基本策略是:

  1. 使用Open()方法打开ServerSocketChannel
  2. 使用socket()方法检索通道的ServerSocket
  3. 将ServerSocket绑定到端口。
  4. 接受传入连接以获得套接字通道。
  5. 通过SocketChannel进行交流。
  6. 关闭SocketChannel。
  7. 所示。转到步骤4。

这跟使用传统IO编写服务器很相似,只是使用缓冲区和通道而不是流来进行通信。你可以把步骤五 和步骤六放入单独的线程以同时处理多个连接。

Api介绍:

1.创建ServerSocket对象

//ServerSocketChannel没有构造器,ServerSocketChannel 实例对象通过open()方法返回
public static SocketChannel open( ) throws IOException 。

2.为了监听即将到来的连接必须绑定端口,但是这并不是由ServerSocketChannel对象自己完成,而是由另外的对象完成,该对象通过ServerSocketChannel的socket()方法返回。

SocketAddress port = new InetSocketAddress(8000);
channel.socket( ).bind(port);

3.接受连接。该方法返回用于和远程客户端通信的SocketChannel的对象,ServerSocketChannel本身没有任何read()和writer()方法。当然 ServerSocketChannel继承了其它通道的通用方法,例如open()和close()方法。

public abstract SocketChannel accept( ) throws IOException

简单的服务器 示例代码:

public class NIOHelloServer {
	public static final int port=2345;
	public static void main(String[] args) throws IOException {
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		SocketAddress inetSocketAddress = new InetSocketAddress(port);
		serverSocketChannel.socket().bind(inetSocketAddress);
		while (true) {
			SocketChannel socketChannel = serverSocketChannel.accept();
			String response = "Hello"
					+socketChannel.socket().getInetAddress()
					+" on port :"
					+socketChannel.socket().getPort();
			response+="\r\n主机地址:"+serverSocketChannel.socket().getInetAddress() + "端口:"+serverSocketChannel.socket().getLocalPort();
			ByteBuffer buffer = ByteBuffer.wrap(response.getBytes("GBK"));
			while (buffer.hasRemaining()) {
				socketChannel.write(buffer);
			}
			socketChannel.close();
		}
		
	}
}

运行结果:

image

这个示例只是为了演示相关的要点。说实话。对于这样一个简单的服务器来说,这是完全多余的。如果原始的基于流的示例和这个示例有任何性能差异。原始的可能还更好。在设置缓冲区和通道时,存在着的固定开销,只有在使用非阻塞I/O时,才会明显增加速度。

16. 非阻塞IO

16.1

传统I/O加线程实现服务端的例子,这里不过多赘述了,代码如下:

import java.net.*;
import java.io.*;
public class DataStuffer {
private static byte[] data = new byte[256];
public static void main(String[] args) throws IOException {
int port = 9000;
for (int i = 0; i < data.length; i++) data[i] = (byte) i;
ServerSocket server = new ServerSocket(port);
while (true) {
Socket socket = server.accept( );
Thread stuffer = new StuffThread(socket);
stuffer.start( );
}
}
private static class StuffThread extends Thread {
private Socket socket;
public StuffThread(Socket socket) {
this.socket = socket;
}
public void run( ) {
try {
OutputStream out = new BufferedOutputStream(socket.getOutputStream( ));
while (!socket.isClosed( )) {
out.write(data);
}
}
catch (IOException ex) {
if (!socket.isClosed( )) {
try {
socket.close( );
}
catch (IOException e) {
// Oh well. We tried.
   }
    }
   }
  }
 }
}

使用NIO来实现的步骤:


1、Open aServerSocketChannel.

2、Put the channel in nonblocking mode.

3、Open aSelector.

4、Register theServerSocketChannel with theSelectorfor accept operations

ServerSocketChannel server = ServerSocketChannel.open( );
server.configureBlocking(false);
Selector selector = Selector.open( );
server.register(selector, SelectionKey.OP_ACCEPT);

以下常量代表你注册的是什么操作:

SelectionKey.ACCEPT

                                Accept a connection from a client.

SelectionKey.CONNECT

                                pen(连接) a connection to a server…

SelectionKey.READ

                                Read data from a channel…

SelectionKey.WRITE

                                Write data to a channel.

接下来你会进入一个无限循环,选择 准备就绪了的通道:

while (true) {
selector.select( );
Set readyKeys = selector.selectedKeys( );
// process each ready key...
}

最初,选择器只注册了一个键,因此只能选择一个键。但是,当连接被接受时,我们将在循环中使用更多键来注册选举人。键本身在一个有限的循环中处理,如下所示:

Iterator iterator = readyKeys.iterator( );
while (iterator.hasNext( )) {
SelectionKey key = (SelectionKey) iterator.next( );
iterator.remove( );
// work with the key...
}

在处理之前,有必要从一组就绪密钥中删除每个密钥。如果将来密钥再次准备就绪,那么readyKeys()返回的下一个集合将包含它。

不同的键做不同的,处理之前弄清key是做什么的:

if (key.isAcceptable( )) {
// accept the connection and register the Selector
// with the key for this connection...
}
else if (key.isWritable( )) {
// write to the connection...
}
SocketChannel client = server.accept( );
client.configureBlocking(false);
SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);

第一种可能是selector找到了一个准备接受传入连接的通道。在本例中,我们告诉服务器
通道接受连接。然后返回一个非阻塞模式配置并注册到
相同的选择器。但是,它注册为对写操作感兴趣:

键还需要知道向通道写入了哪些数据,以及已经写入了多少数据。这需要一些
某种对象,该对象包含对实际数据的引用和该数据的索引。对于某些服务器,数据是文件或流
一些。在本例中,它是一个常量字节数组。事实上,相同的字节数组被写入到所有不同的通道。然而,不同的通道在数组中处于不同的位置,在不同的时间,因此我们将ByteBuffer封装在数组周围,以用于此目的.只要每个连接都将其缓冲区视为只读的,就不会有任何冲突。然后这个缓冲区被附加到key上:

ByteBuffer source = ByteBuffer.wrap(data);
key2.attach(source);
另一种可能是,key没有准备好接受数据,但是写入数据已经准备就绪。在这种情况下,key指向先前打开的SecketChannel通道,我们可以向通道中写入一些数据。
SocketChannel client = (SocketChannel) key.channel( );
ByteBuffer output = (ByteBuffer) key.attachment( );
if (!output.hasRemaining()) output.rewind( );
client.write(output);
注意,当通道被接受时,先前附加到键上的ByteBuffer现在被检索到。

nio服务器 代码示例:

import java.net.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
public class NewDataStuffer {
private static byte[] data = new byte[255];
public static void main(String[] args) throws IOException {
for (int i = 0; i < data.length; i++) data[i] = (byte) i;
ServerSocketChannel server = ServerSocketChannel.open( );
server.configureBlocking(false);
server.socket( ).bind(new InetSocketAddress(9000));
Selector selector = Selector.open( );
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select( );
Set readyKeys = selector.selectedKeys( );
Iterator iterator = readyKeys.iterator( );
while (iterator.hasNext( )) {
SelectionKey key = (SelectionKey) iterator.next( );
iterator.remove( );
try {
if (key.isAcceptable( )) {
SocketChannel client = server.accept( );
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
ByteBuffer source = ByteBuffer.wrap(data);
SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
key2.attach(source);
}
else if (key.isWritable( )) {
SocketChannel client = (SocketChannel) key.channel( );
ByteBuffer output = (ByteBuffer) key.attachment();
if (!output.hasRemaining( )) {
output.rewind( );
}
client.write(output);
}
}
catch (IOException ex) {
key.cancel( );
try {
key.channel().close( );
}
catch (IOException cex) {}
}
}
}
}
}

16.2 Selectable Channels

默认情况下,通道和流是阻塞的。也就是说,当你从通道读取数据或者是写入数据时,当前线程将停止,知道写入或者读取完成(连接和接受连接也是一样的的)。相比之下,在非阻塞模式下,读或写的发生速度与硬件允许的一样快。它不需要绝对零时间,但它尽可能快地运行。如果线程必须等待更多从网络到达的数据,由于以太网卡缓冲区必须由其他进程释放,或其他一些相对持久的操作,它仅返回读或写的部分字节甚至没有一个字节。你的程序需要追踪究竟读入了多少字节或者写入了多少字节,然后继续没有写入或者读入的字节。如果程序没有其他的事情要做,它也可以在阻塞模式下运行。然而,如果程序确实有其他的东西同时需要做,例如,一个网络服务器可能处理一个不同的连接,那么这是值得的。

并不是所有的通道支持非阻塞IO。
网络通道支持非阻塞模式,但是文件通道不支持非阻塞。
支持非阻塞IO的通道都是SelectableChannel的子类。

所有的通道以阻塞模式创建,为了切换通道为非阻塞模式,你需要调用这个方法:传入false参数


public abstract SelectableChannel configureBlocking(boolean block)
throws IOException

一旦将通道设置为非阻塞模式,就不会立即读写通道。相反,您可以向通道注册一个选择器。然后询问选择器通道是否准备好进行读写。如果Selector说:“是的,通道准备好了”。你就可以继续进行读或写工作。否则,你会做别的事情。(更准确地说,你问选择器它的哪个通道已经准备好进行一些操作,了你在选择器说已经准备好了的通道上操作。你不会去问单独的通道。)

register( )方法向Selector注册通道:


public abstract SelectionKey register(Selector selector, int operations)
throws ClosedChannelException

对于任何给定的选择器对象,每个通道只能注册一次。但是,在选择器和通道之间存在多对多关系。每个选择器通常监视许多不同的通道。每个频道都可以注册。几种不同的选择器。然而,最常见的使用模式是,每个通道只注册一个单选用户。

第二个参数指定选择器应该选择的操作。有四种:reading、writing、accepting和connecting。服务器套接字通道通常只注册接受。套接字通道注册的任何或所有其他三个。这些操作在SelectionKey类中表示为指定的常量。这些常量遵循两种模式的幂,因此您可以使用按位或运算符注册多个操作。例如,这个语句注册了一个读写通道:

channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

另一个重载的register()方法为附件添加了第三个参数:


public abstract SelectionKey register(
Selector selector, int operations, Object attachment)
throws ClosedChannelException

通道和选择器都不使用附件对象,它可以为空。那是你自己用的。大多数程序使用它来存储ByteBuffer或其他对象,这些对象跟踪从通道写入或读取的数据。

这两个register()方法都返回一个SelectionKey对象,该对象表示此通道与此selector之间的唯一连接。然而,返回值常常被忽略。当你需要的时候,选择器会把key还给你。

如果您需要更改感兴趣的操作(例如,将read改为write,或将只读改为read/write),您可以再次调用register()方法。这也改变了附件(attachment),但它没有改变key。对于特定的选择器/SelectableChannel对,总是返回相同的键。如果键已经被取消,那么重新注册抛出ancelledKeyException。

register()和configureblocking()方法是线程安全的,但是当并发使用时,它们可能会阻塞一小段时间。
您还可以在同一个锁对象上同步您自己的代码,由blockingLock()方法返回:

public abstract Object blockingLock( )

如果通道处于阻塞模式,isBlocking()方法返回true, 否则false

public abstract boolean isBlocking( )

isRegistered()方法如果向此通道向一个或多个通道注册,则返回true,否则返回false。

public abstract boolean isRegistered( )

keyFor()方法返回与特定选择器对应的SelectionKey:

public abstract SelectionKey keyFor(Selector sel)

validOps()方法返回一组位标志,指定哪些操作对该通道可用,哪些操作不可用:

public abstract int validOps( )

16.3 Selectors

java.nio.channels.Selector类是非阻塞I/O的关键组件。它是使程序能够确定哪些通道那些通道已经准备好访问了。唯一的构造函数是受保护的,并且自己子类化这个类是不寻常的。
相反,您可以通过使用静态的Selector.open()方法创建Selector。


public static Selector open( ) throws IOException

每个选择器可以注册多个通道,注册是线程安全的,你可以在任何时刻向选择器注册各种各样的通道。

当你的程序有空闲去做一些工作的时候,你向Selector询问哪些通道准备好了,哪些通道能够不会阻塞,当读取或写入的时候。


public abstract int selectNow( ) throws IOException

public abstract int select( ) throws IOException

public abstract int select(long timeout) throws IOException


这三个方法返回更改了就绪状态的keys的总数。重要提示:这并不一定是可以操作的键的数量!额外的键可能在选择之前就已经准备好了,也可能仍然准备好了。您很少需要知道选择更改了多少键的就绪状态,因此这些方法的返回值是通常被忽视。

selectNow()//非阻塞的,如果没有选中任何key,立即返回。
select()//阻塞的,选中之后才返回。
select(long timeout)//阻塞的,选中之后返回或时间到期了。

在非阻塞I/O中,如果除了I/O应用程序没有别的事情需要处理,你倾向与使用select()方法。如果当没有通道准备就绪时,你的应用程序需要做些更有用的事情,你可能会使用selectNow()替代select()方法。

当你调用了这三个方法中的任何一个之后,你就可以调用
selectedKeys()返回一个装着就绪通道的Set集合。java5使用泛型改造了这个方法的签名,使其类型安全:

public abstract Set selectedKeys( )
通常 你会像这样使用迭代器迭代Set集合:

while (true) {
selector.select( );
Set readyKeys = selector.selectedKeys( );
Iterator iterator = readyKeys.iterator( );
if (iterator.hasNext( )) {
SelectionKey
key = (SelectionKey) iterator.next( );
iterator.remove( );
// process key...
}
}

Selector会消耗系统的本地资源,所以,用完之后你需要使用close()方法关闭它。否则你的程序肯能会导致内存泄漏和其它的资源,尽管细节会因平台有所不同。关闭选择器将撤销其关联的所有key并且唤醒在select()等待的所有线程。以后,任何企图使用关闭了的选择器或者是它的key都将会抛出异常。如果你不知道,选择器是否已经被关闭了,你可以使用isOpen()方法进行判断。

在大容量系统中,即使是阻塞的select()方法也可能很快返回。然而,在压力较小的环境中,select()可以阻塞一个线程一段时间。另一个线程可以通过调用Selelector的wakeup()方法唤醒被select()方法阻塞的线程。

唤醒()方法:
public abstract Selector wakeup( )
事实上,你可以在select()方法之前唤醒Selector,如果你那样做了,下一次调用select()方法时,将会立即返回。

16.4 Selection Keys

java.nio.channels.SelectionKey类封装了有关用Selector注册的通道的信息。每个SelectionKeyl对象包含以下信息:

  • 通道
  • 选择器
  • 任意附件对象,通常用于指向通道中正在读的或者写的数据
  • 通道感兴趣的操作
  • 在不阻塞的情况下,当前通道可以执行的操作(更准确的说,调用select()后 准备好的操作)

SelectionKey类的大多数方法是get和set方法,当然也包含了取消这个key的方法。

SelectionKey的唯一一个构造器是受保护的,但你几乎不用自己去扩展这个类。SelectionKey对象往往是。由选择器的selectedKeys()方法和SelectableChannel的register()方法返回。 通常,你最想做的第一件事是弄清哪个通道已经准备好了读|写|连接|accept。readyOps()方法返回int内的一组位标志,指示在此键的通道上可能进行哪些操作:

返回值的低阶4位是1或0,这取决于键的通道是否准备好进行读取、写入、连接或接受。用于标志的特定掩码存储在命名常量中:


public static final int OP_READ

public static final int OP_WRITE

public static final int OP_CONNECT

public static final int OP_ACCEPT

例如下面的代码片段检查是否做好了读的准备

if (key.readyOps( ) & SelectionKey.READ != 0) {
// read from the key's channel...
}

然而,用这四种方法分别询问这四种操作中的每一种通常更方便:

public final boolean isReadable( )
public final boolean isWritable( )
public final boolean isConnectable( )
public final boolean isAcceptable( )

如果channel已经准备好了,那就去read吧;如果通道已经准备好write了,那就去write吧,等等。当然,您可以简单地忽略您不感兴趣的操作。示例16-2没有检查通道是否准备好连接或读取,因为它永远不会执行这两种操作。但是,在大多数的情况下,一个选择器被多个通道注册,其中一些通道用于读取,一些用于写入,还有一些用于两者。选择器只告诉您准备好了哪些通道。它不读取、写入、接受或连接自身。

interestOps( )方法设置并获取键的选择器感兴趣的操作

public abstract int interestOps( )
public abstract SelectionKey interestOps(int ops)

这两个方法使用了readyOps()方法相同的位常量,无参的interestOps( )告诉你Selector感兴趣的操作有哪些,有参数的版本能够让你改变这些操作,因此你可以改变Selector的感兴趣事件,例如,从读事件改变为写事件,反之亦然。

16.4.1 Getters

选择器返回关于SectionKey的set集合,暗示那些通道已经准备好了。它并不返回通道本身,为了读或者写,你必须通过这个key拿到通道,因而 你可以使用channel()方法

public abstract SelectableChannel channel( )

通常你需要把该结果转换为SelectableChannel通道的子类。例如:你在Selector上给socket channel注册了读事件。

SocketChannel client = (SocketChannel) key.channel( );

没有相应的setter方法。您不能更改与键关联的通道。一个通道可能有多个键,但每个键只有一个通道。

16.4.2 Attachments

因为这是非阻塞I/O,所以在每次调用时,可能无法写入或读取所需的数据。例如,当
从网络读取数据时,在以太网卡的缓冲区中可能会有1000字节等待您,您可以立即读取这些字节。然而,可能还会有兆字节的数据。你需要某种数据结构来存储你读到的数据,在其中您可以存储您所读取的数据,以跟踪您在流中的位置。这个数据结构到底是什么取决于你想做什么。例如,它可能是字节数组、文件或字符串。通常,这种数据结构表示为某种java.nio. buffer object。这就是设计缓冲器的目的。

无论这个数据结构是什么,它通常使用attach()方法(或SelectableChannel中的register()方法)附加到密钥上,并使用attachment()方法从密钥中得到。


public final Object attach(Object ob)

public final Object attachment( )

16.4.3 Canceling

您可以通过cancel()方法来注销该通道:

public abstract void cancel( )

这并不像关闭流那样是必须的。然而,如果你已经完成了一个通道,让选择器不再监视它这可能是一个好主意。

public abstract boolean isValid( )//key 是否还有意义,当key关联的通道和选择器没有关闭并且key没有被取消返回true。

16.5 管道通道

管道通道在两个线程间移动数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值