java nio 基础(一)
来源:互联网 发布:网络金融最初发展阶段 编辑:程序博客网 时间:2024/06/08 01:50
如今JVM在执行效率方面有了很大的进步,I/O操作逐渐成为限制java效率的主要因素。其实现代操作系统在传送数据方面也有着较高的效率,这通常是在DMA的协助下完成的。但是JVM的I/O类往往一次只处理少量数据。结果,操作系统送来整缓冲区的数据,java.io 的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io 类则喜欢一铲子一铲子地加工数据。有了 NIO,就可以轻松地把一卡车数据备份到您能直接使用的地方(ByteBuffer 对象)。这并不是说使用传统的 I/O 模型无法移动大量数据——当然可以(现在依然可以)。具体地说,RandomAccessFile 类在这方面的效率就不低,只要坚持使用基于数组的 read( )和 write( )方法。这些方法与底层操作系统调用相当接近,尽管必须保留至少一份缓冲区拷贝。
当进程请求 I/O 操作的时候,它执行一个系统调用(有时称为陷阱)将控制权移交给内核。当内核找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。
把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问用户空间 。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作(这也暗示了其中的时间代价)。
为了在内核空间的文件系统页与用户空间的内存区之间移动数据,一次以上的拷贝操作几乎总是免不了的。这是因为,在文件系统页与用户缓冲区之间往往没有
一一对应关系。内存映射 I/O解决了这一个问题,它完全摒弃缓冲区拷贝。内存映射 I/O 使用文件系统建立从用户空间直接到可用文件系统页的虚拟内存映射。
这样做有几个好处:
1)用户进程把文件数据当作内存,所以无需发布 read( )或 write( )系统调用。
2)当用户进程碰触到映射内存空间,页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。
3)操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。
4)数据总是按页对齐的,无需执行缓冲区拷贝。
5)大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。
在处理大量数据时,尤其要记得这一点。如果数据缓冲区是按页对齐的,且大小是内建页大小的倍数,那么,对大多数操作系统而言,其处理效率会大幅提升(如上面蓝色部分介绍的,省去了数据的处理工作,可以不需要缓冲区拷贝)。
Channel的ByteBuffer也是经过了内核空间的缓冲区的拷贝的,二者的实现是类似的(我目前是这么理解的)。采用BufferedInputStream和Channel两种方式读取500M的文件(Channel与BufferedInputStream都使用1M的缓冲区),前者用了10s,后者用了8s(具体原因细节还不太清楚)。而内存映射文件是真正没有经过内核缓冲区拷贝的。但这不表明内存映射文件的效率就一定比前二者高。复制一个文件发现使用Channel到Channel方式的拷贝大大高于内存映射文件的方式。使用BufferedInputStream和Channel的方式复制一个文件效率在合理选择缓冲区大小的情况下也可以高于内存映射文件(考虑缺页中断的开销与内核缓冲区到用户缓冲区拷贝开销的一个平衡点),具体原因从上面的介绍中可以找到。
上面的是自己在读书的过程中遇到的一些知识点和一些思考,下面介绍java NIO的基础知识。
java NIO 的核心内容:缓冲区、通道和选择器。
一、缓冲区
Buffer的基本用法
使用Buffer读写数据一般遵循以下四个步骤:
- 写入数据到Buffer
- 调用flip()方法
- 从Buffer中读取数据
- 调用clear()方法或者compact()方法
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer的capacity,position和limit
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
为了理解Buffer的工作原理,需要熟悉它的三个属性:
- capacity
- position
- limit
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
Buffer的类型
Java NIO 有以下Buffer类型
ByteBuffer MappedByteBufferCharBuffer DoubleBufferFloatBuffer IntBufferLongBuffer ShortBuffer
这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。
Buffer的分配
要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有一个allocate方法。下面是一个分配48字节capacity的ByteBuffer的例子。
ByteBuffer buff = ByteBuffer.allocate(48);
提供您自己的数组用做缓冲区的备份存储器,请调用wrap()函数
char [] myArray = new char [100];
分配一个1024字节的直接缓冲区
向Buffer中写数据
写数据到Buffer有两种方式:
- 从Channel写到Buffer。
- 通过Buffer的put()方法写到Buffer里。
从Channel写到Buffer的例子
int bytesRead = inChannel.read(buf); //read into buffer.
通过put方法写Buffer的例子:
buf.put(127);
put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。 更多Buffer实现的细节参考JavaDoc。
flip()方法
flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。
rewind()方法
Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
从Buffer中读取数据
从Buffer中读取数据有两种方式:
- 从Buffer读取数据到Channel。
- 使用get()方法从Buffer中读取数据。
从Buffer读取数据到Channel的例子:
使用get()方法从Buffer中读取数据的例子
get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。
clear()与compact()方法
一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。
如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。
compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。这一缓冲区工具在复制数据时要比您使用get()和put()函数高效得多。所以当您需要时,请使用compact()。
mark()与reset()方法
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:
equals()与compareTo()方法
可以使用equals()和compareTo()方法两个Buffer。
equals()
当满足下列条件时,表示两个Buffer相等:
- 有相同的类型(byte、char、int等)。
- Buffer中剩余的byte、char等的个数相等。
- Buffer中所有剩余的byte、char等都相同。
如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素(position 到limit之间的)。
compareTo()方法
compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
- 第一个不相等的元素小于另一个Buffer中对应的元素 。
- 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
批量移动
缓冲区的涉及目的就是为了能够高效传输数据。一次移动一个数据元素并不高效。buffer API提供了向缓冲区内外批量移动数据元素的函数。
ByteBuffer
get(byte[] dst)
ByteBuffer
get(byte[] dst, int offset, int length)
ByteBuffer
put(byte[] src)
ByteBuffer
put(byte[] src, int offset, int length)
这些批量移动的合成效果与循环的方式是相同的,但是这些方法可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。如果您所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不变,同时抛出BufferUnderflowException异常。要将一个缓冲区释放到一个大数组中,要这样做:
char [] bigArray = new char [1000];
int length = buffer.remaining( ); // Get count of chars remaining in the buffer
chars buffer.get (bigArrray, 0, length); // Buffer is known to contain < 1,000
processData (bigArray, length);// Do something useful with the data
Put()的批量版本工作方式相似,但以相反的方向移动数据,从数组移动到缓冲区。如果缓冲区有足够的空间接受数组中的数据(buffer.remaining()>myArray.length),数据将会被复制到从当前位置开始的缓冲区,并且缓冲区位置会被提前所增加数据元素的数量。如果缓冲区中没有足够的空间,那么不会有数据被传递,同时抛出一个BufferOverflowException异常。
复制缓冲区
当一个管理其他缓冲器所包含的数据元素的缓冲器被创建时,这个缓冲器被称为视图缓冲器。复制一个缓冲区会创建一个新的Buffer对象,但并不复制数据。原始缓冲区和副本都会操作同样的数据元素,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。
CharBuffer buffer = CharBuffer.allocate (8);
buffer.position (3).limit (6).mark( ).position (5);
CharBuffer dupeBuffer = buffer.duplicate( );
buffer.clear( );
还可以使用asReadOnlyBuffer()函数来生成一个只读的缓冲区视图。这与duplicate()相同,除了这个新的缓冲区不允许使用put(),并且其isReadOnly()函数将会返回true。
如果一个只读的缓冲区与一个可写的缓冲区共享数据,或者有包装好的备份数组,那么对这个可写的缓冲区或直接对这个数组的改变将反映在所有关联的缓冲区上,包括只读缓冲区。
分割缓冲区与复制相似,但slice()创建一个从原始缓冲区的当前位置开始的新缓冲区,并且其容量是原始缓冲区的剩余元素数量(limit-position)。这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。
视图缓冲区
视图缓冲区通过已存在的缓冲区对象实例的工厂方法来创建。这种视图对象维护它自己的属性,容量,位置,上界和标记,但是和原来的缓冲区共享数据元素。但是ByteBuffer类允许创建视图来将byte型缓冲区字节数据映射为其它的原始数据类型。例如,asLongBuffer()函数创建一个将八个字节型数据当成一个long型数据来存取的视图缓冲区(这里还有个字节序的问题)。下面的代码创建了一个ByteBuffer缓冲区的CharBuffer视图:ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer( );
数据元素视图
ByteBuffer类提供了一个不太重要的机制来以多字节数据类型的形式存取byte数据组。ByteBuffer类为每一种原始数据类型提供了存取的和转化的方法。
根据这个缓冲区的当前的有效的字节顺序,这些字节数据会被排列或打乱成需要的原始数据类型。比如说,如果getInt()函数被调用,从当前的位置开始的四个字节会被包装成一个int类型的变量然后作为函数的返回值返回。
二、通道Channel
这些是Java NIO中最重要的通道的实现(有一个FileChannel类和三个socket通道类):
- FileChannel//从文件中读写数据
- DatagramChannel// 能通过UDP读写网络中的数据
- SocketChannel//能通过TCP读写网络中的数据ServerSocketChannel//可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个
- SocketChannel//可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
1.FileChannel
FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。
打开FileChannel
在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");FileChannel inChannel = aFile.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。如:
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);}
注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。
关闭FileChannel
用完FileChannel后必须将其关闭。如:
channel.close();
FileChannel的position方法
同底层的文件描述符一样,每个FileChannel都有一个叫“file position”的概念。这个position值决定文件中哪一处的数据接下来将被读或者写。有两种形式的position( )方法。第一种,不带参数的,返回当前文件的position值。返回值是一个长整型(long),表示文件中的当前字节位置。第二种形式的position( )方法带一个long(长整型)参数并将通道的position设置为指定值。如果position设置到超出文件尾,这样做会把position设置为指定值而不改变文件大小。假如在将position设置为超出当前文件大小时实现了一个read( )方法,那么会返回一个文件尾(end-of-file)条件;倘若此时实现的是一个write( )方法则会引起文件增长以容纳写入的字节,具体行为类似于实现一个绝对write( )并可能导致出现一个文件空洞(file hole)。
FileChannel的size方法
FileChannel实例的size()方法将返回该实例所关联文件的大小。如:
long fileSize = channel.size();
FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如:
channel.truncate(1024);
这个例子截取文件的前1024个字节。
当需要减少一个文件的size时,truncate( )方法会砍掉您所指定的新size值之外的所有数据。如果当前size大于新size,超出新size的所有字节都会被悄悄地丢弃。如果提供的新size值大于或等于当前的文件size值,该文件不会被修改。这两种情况下,truncate( )都会产生副作用:文件的position会被设置为所提供的新size值。
FileChannel的force方法
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。对于关键操作如事务(transaction)处理来说,这一点是非常重要的,可以保证数据完整性和可靠的恢复。
force()方法有一个boolean类型的参数,指明是否同时将文件元数据(文件所有者、访问权限、最后一次修改时间等)写到磁盘上。大多数情形下,该信息对数据恢复而言是不重要的。一些大数量事务处理程序可能通过在每次调用force( )方法时不要求元数据更新来获取较高的性能提升,同时也不会牺牲数据完整性。
下面的例子同时将文件数据和元数据强制写到磁盘上:
channel.force(true);
文件锁定
FileLock
lock()
abstract FileLock
lock(long position, long size, boolean shared)
FileLock
tryLock()
abstract FileLock
tryLock(long position, long size, boolean shared)
内存映射文件
新的FileChannel类提供了一个名为map( )的方法,该方法可以在一个打开的文件和一个特殊类型的ByteBuffer之间建立一个虚拟内存映射。在FileChannel上调用map( )方法会创建一个由磁盘文件支持的虚拟内存映射,并在那块虚拟内存空间外部封装一个MappedByteBuffer对象。
调用get( )方法会从磁盘文件中获取数据,此数据反映该文件的当前内容,即使在映射建立之后文件已经被一个外部进程做了修改。相似地,对映射的缓冲区实现一个put( )会更新磁盘上的那个文件(假设对该文件您有写的权限),并且您做的修改对于该文件的其他阅读者也是可见的。
通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高(这个要看具体情况)。因为不需要做明确的系统调用,那会很消耗时间(其实缺页中断也会导致现场保存,并等待下一次调度选中它。内存映射节省了一次内存之间的拷贝)。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗Java虚拟机内存堆(memory heap)。
那些包含索引以及其他需频繁引用或更新的内容的巨大而结构化文件能因内存映射机制受益非常多(内存映射文件对于顺序读取全部文件的情况优势并不是很明显,但是这里所说的情况下,的确会有很大优势)。如果同时结合文件锁定来保护关键区域和控制事务原子性,那您将能了解到内存映射缓冲区如何可以被很好地利用。
public abstract MappedByteBuffer map (MapMode mode, long position,long size)
MapMode 是访问模式,包括READ_ONLY、READ_WRITE,这里只介绍第三种模式MapMode.PRIVATE。它表示您想要一个写时拷贝(copy-on-write)的映射(写时拷贝这一技术经常被操作系统使用,以在一个进程生成另一个进程时管理虚拟地址空间)。这意味着您通过put( )方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。尽管写时拷贝的映射可以防止底层文件被修改,您也必须以read/write权限来打开文件以建立MapMode.PRIVATE映射。只有这样,返回的MappedByteBuffer对象才能允许使用put( )方法。选择使用MapMode.PRIVATE模式并不会导致您的缓冲区看不到通过其他方式对文件所做的修改(注意写时拷贝是按页拷贝的,没有拷贝的页如果被其他操作修改,也是可以看到的)。
没有unmap( )方法。也就是说,一个映射一旦建立之后将保持有效,直到MappedByteBuffer对象被施以垃圾收集动作为止。同锁不一样的是,映射缓冲区没有绑定到创建它们的通道上。关闭相关联的FileChannel不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。
所有的MappedByteBuffer对象都是直接的,这意味着它们占用的内存空间位于Java虚拟机内存堆之外(并且可能不会算作Java虚拟机的内存占用,不过这取决于操作系统的虚拟内存模型)。
load( )方法会加载整个文件以使它常驻内存。然而,load( )方法返回并不能保证文件就会完全常驻内存,这是由于请求页面调入(demand paging)是动态的。我们可以通过调用isLoaded( )方法来判断一个被映射的文件是否完全常驻内存了。如果该方法返回true值,那么很大概率是映射缓冲区的访问延迟很少或者根本没有延迟。不过,这也是不能保证的。方法force( )同FileChannel类中的同名方法相似,该方法会强制将映射缓冲区上的更改应用到永久磁盘存储器上。当用MappedByteBuffer对象来更新一个文件,您应该总是使用MappedByteBuffer.force( )而非FileChannel.force( ),因为通道对象可能不清楚通过映射缓冲区做出的文件的全部更改。MappedByteBuffer没有不更新文件元数据的选项——元数据总是会同时被更新的。
Channel-to-Channel传输
Socket Channel
新的socket通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。再也没有为每个socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。
全部socket通道类(SocketChannel、DatagramChannel和ServerSocketChannel)都是由位于java.nio.channels.spi包中的AbstractSelectableChannel引申而来。这意味着我们可以用一个Selector对象来执行socket通道的有条件的选择(readiness selection)。
1.SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:
- 打开一个SocketChannel并连接到互联网上的某台服务器。
- 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
打开 SocketChannel
下面是SocketChannel的打开方式:
SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
关闭 SocketChannel
当用完SocketChannel之后调用SocketChannel.close()关闭SocketChannel:
socketChannel.close();
从 SocketChannel 读取数据
要从SocketChannel中读取数据,调用一个read()的方法之一。以下是例子:
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = socketChannel.read(buf);
首先,分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。
然后,调用SocketChannel.read()。该方法将数据从SocketChannel 读到Buffer中。read()方法返回的int值表示读了多少字节进Buffer里。如果返回的是-1,表示已经读到了流的末尾(连接关闭了)。
写入 SocketChannel
写数据到SocketChannel用的是SocketChannel.write()方法,该方法以一个Buffer作为参数。示例如下:
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);}
注意SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。
非阻塞模式
可以设置 SocketChannel 为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(), read() 和write()了。
connect()
如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。像这样:
socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));while(! socketChannel.finishConnect() ){ //wait, or do something else...}
write()
非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。。
read()
非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。
2、ServerSocketChannel
打开 ServerSocketChannel
通过调用 ServerSocketChannel.open() 方法来打开ServerSocketChannel.如:
关闭 ServerSocketChannel
通过调用ServerSocketChannel.close() 方法来关闭ServerSocketChannel. 如:
监听新进来的连接
通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。
通常不会仅仅只监听一个连接,在while循环中调用 accept()方法. 如下面的例子:
当然,也可以在while循环中使用除了true以外的其它退出准则。
非阻塞模式
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null.如:
3.DatagramChannel
创建DatagramChannel的模式和创建其他socket通道是一样的:调用静态的open( )方法来创建一个新实例。新DatagramChannel会有一个可以通过调用socket( )方法获取的对等DatagramSocket对象。DatagramChannel对象既可以充当服务器(监听者)也可以充当客户端(发送者)。如果您希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。
- 您的程序可以承受数据丢失或无序的数据。
- 您希望“发射后不管”(fire and forget)而不需要知道您发送的包是否已接收。
- 数据吞吐量比可靠性更重要。
- 您需要同时发送数据给多个接受者(多播或者广播)。
- 包隐喻比流隐喻更适合手边的任务。
Pipe
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。Pipe的source通道和sink通道提供类似java.io.PipedInputStream和java.io.PipedOutputStream所提供的功能,不过它们可以执行全部的通道语义。请注意,SinkChannel和SourceChannel都由AbstractSelectableChannel引申而来(所以也是从SelectableChannel引申而来),这意味着pipe通道可以同选择器一起使用。
管道可以被用来仅在同一个Java虚拟机内部传输数据。虽然有更加有效率的方式来在线程之间传输数据,但是使用管道的好处在于封装性。根据给定的通道类型,相同的代码可以被用来写数据到一个文件、socket或管道。选择器可以被用来检查管道上的数据可用性,如同在socket通道上使用那样地简单。这样就可以允许单个用户线程使用一个Selector来从多个通道有效地收集数据,并可任意结合网络连接或本地工作线程使用。因此,这些对于可伸缩性、冗余度以及可复用性来说无疑都是意义重大的。
Pipes的另一个有用之处是可以用来辅助测试。一个单元测试框架可以将某个待测试的类连接到管道的“写”端并检查管道的“读”端出来的数据。它也可以将被测试的类置于通道的“读”端并将受控的测试数据写进其中。两种场景对于回归测试都是很有帮助的。
这里是Pipe原理的图示:
创建管道
通过Pipe.open()方法打开管道。例如:
Pipe pipe = Pipe.open();
向管道写数据
要向管道写数据,需要访问sink通道。像这样:
Pipe.SinkChannel sinkChannel = pipe.sink();
通过调用SinkChannel的write()方法,将数据写入SinkChannel,像这样:
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()) { sinkChannel.write(buf);}
从管道读取数据
从读取管道的数据,需要访问source通道,像这样:
Pipe.SourceChannel sourceChannel = pipe.source();
调用source通道的read()方法来读取数据,像这样:
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = sourceChannel.read(buf);
read()方法返回的int值会告诉我们多少字节被读进了缓冲区。
- 《java nio》
- http://ifeve.com/file-channel/
- http://ifeve.com/socket-channel/
- http://ifeve.com/datagram-channel/
- http://ifeve.com/pipe/
- java nio 基础(一)
- JAVA NIO 基础(一)
- Java NIO:一、NIO基础
- java nio基础一
- JAVA中NIO基础(一)
- java nio(一)--Buffer基础
- 不惑JAVA之JAVA基础 - NIO (一)
- Java基础——Java NIO详解(一)
- Java NIO(一)
- java NIO(一)
- Java NIO(一)
- Java NIO (一)
- JAVA-NIO(一)
- NIO(一)基础理解
- Java NIO(一) 初步理解NIO
- java基础(7)NIO
- Java NIO学习(一)
- (一)Java NIO概述
- JAVAEE与Android心得分享
- Android中定位经纬度问题
- Source Insight添加文件时提示No Files Found
- win7安装Android环境
- 磊科Q3刷236W 免交换机双拨
- java nio 基础(一)
- Merge Two Sorted Lists
- 网站中防盗链技术
- androidswitcher的使用
- Cocos2d-x VLC Player
- 关于vc6.0下配置GDI+的问题
- 机器学习基石——第3-4讲.Types of Learning
- 大数据,怎么搞?
- C++ 服务端开发之Socket 入门