Channel
在我的理解里,通道是对I/O服务进一步的包装。Channel提供与 I/O 服务的直接连接,并且通过通道,多路复用和非阻塞I/O得以实现。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
Channel类提供维持平台独立性所需的抽象过程,不过仍然会模拟现代操作系统本身的I/O性能。通道是一种途径,借助该途径,可以用最小的总开销来访问操作系统本身的I/O服务。缓冲区则是通道内部用来发送和接收数据的端点。
一、通道基
Channel类的继承关系比较复杂,并且部分 channel 类依赖于在 spi子包中定义的类。Channel的API主要由接口指定。不同的操作系统上通道实(Channel Implementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
InterruptibleChannel是一个标记接口,当被通道使用时可以标示该通道是可以中断的;WritableByteChannel和ReadableByteChannel表示通道只能在字节缓冲区上操作;GatheringByteChannel和ScatteringByteChannel提供矢量I/O服务;AsynchronousChannel是1.7新增的,支持异步I/O的信道。
1.1 打开通道
通道是访问I/O服务的导管,I/O 可以分为广义的两大类别: File I/O和Stream I/O,相应地存在两种类型的通道:文件(file)通道和套接字(socket)通道。
Socket通道有可以直接创建新socket通道的工厂方法。但FileChannel对象只能通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream 对象上调用getChannel( )方法来获取,不能直接创建一个 FileChannel 对象。
1 DatagramChannel dc = DatagramChannel.open( ); 2 RandomAccessFile raf = new RandomAccessFile ("somefile", "r"); 3 FileChannel fc = raf.getChannel( );
net包下的socket类也有getChannel( )方法,但这些方法永远不会创建新通道,不是新通道的来源。只有在已经有通道存在的时候,它们才返回与一个socket关联的通道。而RandomAccessFile.getChannel( )方法才是新通道的来源。
1 DatagramSocket datagramSocket = new DatagramSocket(); 2 DatagramChannel channel = datagramSocket.getChannel(); 3 System.out.println(channel); 4 //result:null
1.2 使用通道
图1.1 ByteChannel继承关系
以ByteChannel为例,通道可以是单向(unidirectional)或者双向的(bidirectional)。ReadableByteChannel接口定义了read( )方法,WritableByteChannel 接口定义了write( )方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据(实现ByteChannel就能双向传输,这是很好的类设计技巧)。
这里需要注意的是,File和Socket都是双向传输的,但对File来说,一个文件可以在不同的时候以不同的权限打开。从FileInputStream对象的getChannel( )方法获取的FileChannel对象是只读的。虽然FileChannel是双向的,但在这样一个通道上调用write( )方法将抛出未经检查的NonWritableChannelException异常,因为 FileInputStream对象总是read-only的权限打开文件。
1 FileInputStream input = new FileInputStream (fileName); 2 FileChannel channel = input.getChannel( ); 3 channel.write (buffer); 4 //result: This will will throw an IOException
以上几个类的read( )和write( )方法使用ByteBuffer对象作为参数。两种方法均返回已传输的字节数,可能比缓冲区的字节数少甚至可能为零。缓冲区的位置也会发生与已传输字节相同数量的前移。如果只进行了部分传输,缓冲区可以被重新提交给通道并从上次中断的地方继续传输。
1 public class ChannelTest { 2 public static void main(String[] args) throws IOException, InterruptedException { 3 BufferedInputStream stream = new BufferedInputStream( 4 new FileInputStream( 5 new File(".\\Test.txt"))); 6 ReadableByteChannel src = Channels.newChannel(stream); 7 WritableByteChannel dest = Channels.newChannel(System.out); 8 transfer1(src,dest ); 9 src.close(); 10 dest.close(); 11 } 12 13 private static void transfer1(ReadableByteChannel src, WritableByteChannel dest) throws IOException, InterruptedException { 14 ByteBuffer buffer = ByteBuffer.allocate(4); 15 16 while (src.read(buffer) != -1){ 17 buffer.flip(); 18 while (buffer.hasRemaining()) 19 dest.write(buffer); 20 buffer.clear(); 21 } 22 } 23 }
1.3 关闭通道
通道不能被重复使用,一个打开的通道即代表与一个特定I/O服务的特定连接并封装该连接的状态。当通道关闭时,连接会丢失,通道将不再连接任何东西。
调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞,即使该通道处于非阻塞模式。通道关闭时的阻塞行为是高度取决于操作系统或者文件系统的,在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。
另外,通道引入了一些与关闭和中断有关的新行为。如果一个通道实现InterruptibleChannel接口,它的行为以下述语义为准:如果一个线程在一个通道上被阻塞并且同时被中断(由调用该被阻塞线程的interrupt( )方法的另一个线程中断),那么该通道将被关闭,该被阻塞线程也会产生一个ClosedByInterruptException异常。例如,假如一个线程的interrupt status被设置并且该线程试图访问一个通道,那么这个通道将立即被关闭,同时将抛出相同的ClosedByInterruptException异常。
可中断的通道也是可以异步关闭的。实现InterruptibleChannel接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个I/O操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个AsynchronousCloseException异常,接着通道就被关闭并将不再可用。
二、Scatter/Gather
Scatter/Gather可以在多个缓冲区上实现一个简单的 I/O 操作。对于write操作,数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。对于read操作,从通道读取的数据会按顺序被散布(称为 scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。Scatter/Gather操作减少或避免了缓冲区拷贝和系统用。
1 ByteBuffer header = ByteBuffer.allocateDirect (10); 2 ByteBuffer body = ByteBuffer.allocateDirect (80); 3 ByteBuffer [] buffers = { header, body }; 4 int bytesRead = channel.read (buffers);
以该例,假定channel连接到一个有48字节数据等待读取的socket上,read( )方法返回后,bytesRead就等于48,header缓冲区将包含前10个从通道读取的字节而body缓冲区则包含接下来的38个字节。通道会自动地将数据scatter到这两个缓冲区中。
如图2.1,从通道传输来的数据被scatter到所列缓冲区,依次填充每个缓冲区(从缓冲区的position处开始到 limit处结束)。这里显示的position和limit值是读操作开始之前的。
另外带有offset和length参数的方法,offset是指从那个缓冲区开始,length指使用的缓冲区的数量。Scatter/Gather很大程度上减少了工作量,这一点在内存映射文件的例子中会有很好的展示。
图2.1 一个使用四个缓冲区的scatter操作
三、文件通道
1 public abstract class FileChannel 2 extends AbstractInterruptibleChannel 3 implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
文件通道总是阻塞式的,不能被置于非阻塞模式(该模式对文件I/O意义不大),但在1.7加入异步I/O,允许一个进程可以从操作系统请求一个或多个I/O操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的I/O操作已完成的通知。
FileChannel对象是线程安全(thread-safe)的。多个进程可以在同一个实例上并发调用方法而不会引起任何问题,不过并非所有的操作都是多线程的(multithreaded)。影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。
FileChannel是一个反映Java虚拟机外部一个具体对象的抽象。 FileChannel类保证同一个Java虚拟机上的所有实例看到的某个文件的视图均是一致的,但是Java虚拟机却不能对超出它控制范围的因素提供担保。通过一个FileChannel实例看到的某个文件的视图同通过一个外部的非Java进程看到的该文件的视图可能一致,也可能不一致。
3.1 文件访问
每个FileChannel都有一个“file position”的概念,这个position值决定文件中哪一处的数据接下来将被读或者写,它是从底层的文件描述符获得的。
API中有两个position( )方法,不带参数的,返回一个长整型(long),表示文件中的当前字节位置;带有参数的,参数是将通道的position设置为指定值。如果将通道position设置为一个负值会导致IllegalArgumentException异常,但能把position设置到超出文件尾,这样做会把position设置为指定值而不改变文件大小。
假如在将position设置为超出当前文件大小,执行一个read( )方法时,会返回一个文件尾(end-of-file)条件;若此执行一个write( )方法则会引起文件增长以容纳写入的字节,这样会导致一个“文件空洞”。
3.1.1 文件空洞
1 public class FileHole2 { 2 public static void main(String[] argv) throws IOException { 3 4 File temp = new File(".\\filehole.txt"); 5 RandomAccessFile file = new RandomAccessFile(temp, "rw"); 6 FileChannel channel = file.getChannel(); 7 8 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100); 9 putData(0, byteBuffer, channel); 10 putData(5000000, byteBuffer, channel); 11 putData(50000, byteBuffer, channel); 12 13 System.out.println("Wrote temp file '" + temp.getPath() 14 + "', size=" + channel.size()); 15 channel.close(); 16 file.close(); 17 } 18 19 private static void putData(int position, ByteBuffer buffer, FileChannel channel) throws IOException { 20 String string = "*<-- location " + position; 21 buffer.clear( ); 22 buffer.put (string.getBytes ("US-ASCII")); 23 buffer.flip( ); 24 channel.position (position); 25 channel.write (buffer); 26 } 27 }
当磁盘上一个文件的分配空间小于它的文件大小时会出现“文件空洞”。上例,我们在position==0位置写入一些字符,此时如果关闭通道,则最终该文件的大小将是:字符大小+文件描述信息。但我们再将position定位在5,000,000,再写入一些字符,这时文件的大小变成了5,000,021字节,但实际上我们远没有写入这么多信息。从0到5,,000,000没有被写入字节的区域都被“0”填充,这些就是“文件空洞”。第三次定位在50,000时,最终写入位置在5,000,000之前,没有在该值基础上增加50,000。文件空洞在内存映射时也会产生一些问题。
图3.1 文件空洞
3.1.2 read和write
position是从底层的文件描述符获得的,同时被作为通道引用获取来源的文件对象共享。这也就意味着一个对象对该position的更新可以被另一个对象看到的。
当字节被read( )或write( )方法传输时,文件position会自动更新。如果position值达到了文件大小的值(文件大小的值可以通过size( )方法返回),read( )方法会返回一个文件尾条件值-1。不同于缓冲区的是,如果实现write( )方法时position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节。
position也有带参数的绝对形式的read( )和write( )方法。这种绝对形式的方法在返回值时不会改变当前的文件position。由于通道的状态无需更新,因此绝对的读和写能会更加有效率,操作请求可以直接传到本地代码。尝试在文件末尾之外的position进行一个绝对读操作,size( )方法会返回一个end-of-file。在超出文件大小的position上做一个绝对write( )会导致文件增加以容纳正在被写入的新字节(”文件空洞“)。
3.1.3 truncate
当需要减少一个文件的size时,truncate( )方法会砍掉您所指定的新size值之外的所有数据。 如果当前size大于新size,超出新size的所有字节都会被悄悄地丢弃。如果提供的新size值大于或等于当前的文件size值,该文件不会被修改。这两种情况下,truncate( )都会产生副作用:文件的position会被设置为所提供的新size值。
3.1.4 force
该方法告诉通道强制将全部待定的修改都应用到磁盘的文件上。所有的现代文件系统都会缓存数据和延迟磁盘文件更新以提高性能,而方法要求文件的所有待定修改立即同步到磁盘。
如果文件位于一个本地文件系统,那么一旦force( )方法返回,即可保证从通道被创建时起的对文件所做的全部修改已经被写入到磁盘。但如果文件位于一个远程的文件系统,如NFS上,那么不能保证待定修改一定能同步到永久存储器上,因Java虚拟机不能做操作系统或文件系统不能实现的承诺。
force( )方法的布尔型参数表示在方法返回值前文件的元数据(metadata)是否也要被同步更新到磁盘。
四、内存映射文件
FileChanne的map( )方法可以在一个打开的文件和一个特殊类型的ByteBuffer之间建立一个虚拟内存映射(virtual memory mapping)。由map( )方法返回的MappedByteBuffer对象的行为在多数方面类似一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘上的一个文件中。
调用get( )方法会从磁盘文件中获取数据,该数据同您用常规方法读取文件看到的内容是完全一样的;调用put( )会更新磁盘上的文件,并且这个修改对于该文件的其它访问者也是可见的。因为不需要做明确的系统调用,通过内存映射机制来访问一个文件会比使用常规方法读写高效得多。并且它不会消耗 Java 虚拟机内存堆。
1 public abstract class FileChannel 2 extends AbstractChannel 3 implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { 4 public abstract MappedByteBuffer map (MapMode mode, long position,long size) 5 public static class MapMode{ 6 public static final MapMode READ_ONLY; 7 public static final MapMode READ_WRITE; 8 public static final MapMode PRIVATE; 9 } 10 }
map( )方法有mode,position和size三个参数。position和size表示可以创建一个MappedByteBuffer来代表一个文件中字节的某个子范围,position为起始位置,size为映射文件大小。映射文件的范围不应超过文件的实际大小,如果请求一个超出文件大小的映射,文件会被增大以匹配映射的大小(产生“文件空洞”)。
内存映射有三种模式,MapMode.READ_ONLY、MapMode.READ_WRITE和MapMode.PRIVATE。请求的映射模式将受被调用map( )方法的FileChannel对象的访问权限所限制。如果通道是以只读的权限打开的,去请求MapMode.READ_WRITE模式,map( )方法会抛出NonWritableChannelException异常;如果在没有读权限的通道上请求MapMode.READ_ONLY映射模式,将产生NonReadableChannelException异常。但在read/write权限打开的通道上请求MapMode.READ_ONLY映射是允许的。
这里需要注意的是MapMode.PRIVATE模式,它表示一个写时拷贝(copy-on-write)映射。我们通过put( )方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到。该过程不会对底层文件做任何修改(修改发生在拷贝文件中),而且一旦缓冲区被施以垃圾收集动作,修改将会丢失。虽然写时拷贝的映射可以防止底层文件被修改,但也必须以read/write权限来打开文件以建立MapMode.PRIVATE映射,只有这样MappedByteBuffer对象才能允许使用put( )方法。
MapMode.PRIVATE模式下也可以看到通过其他方式对文件所做的修改,但若该缓冲区对文件做了修改就不可见了。因为在一个写时拷贝的缓冲区上调用put( )方法时(此时才真正进行了拷贝,产生拷贝页),受影响的页会被拷贝,然后更改就会应用到该拷贝中。
MappedByteBuffer对象都是直接的,它们占用的内存空间位于Java虚拟机内存堆之外,一个映射建立之后将保持有效,直到MappedByteBuffer对象被施以垃圾收集动作为止。
这里需要注意,如果映射有效时文件大小变化了,那么缓冲区的部分或全部内容都可能无法访问,并将返回未定义的数据或者抛出未检查的异常。
另外,如果映射是以MapMode.READ_ONLY或MAP_MODE.PRIVATE模式建立的,那么调用force( )方法将不起任何作用,因为永远不会有更改需要应用到磁盘上,但也无害。
1 public class MapFile { 2 public static void main (String [] argv) throws Exception { 3 File tempFile = new File(".\\file\\mapfile.txt"); 4 RandomAccessFile file = new RandomAccessFile (tempFile, "rw"); 5 FileChannel channel = file.getChannel( ); 6 ByteBuffer temp = ByteBuffer.allocate (100); 7 8 temp.put ("This is the file content".getBytes( )); 9 temp.flip( ); 10 channel.write (temp, 0); 11 12 temp.clear( ); 13 temp.put ("This is more file content".getBytes( )); 14 temp.flip( ); 15 channel.write (temp, 8192); 16 // Create three types of mappings to the same file 17 MappedByteBuffer ro = channel.map ( 18 FileChannel.MapMode.READ_ONLY, 0, channel.size( )); 19 MappedByteBuffer rw = channel.map ( 20 FileChannel.MapMode.READ_WRITE, 0, channel.size( )); 21 MappedByteBuffer cow = channel.map ( 22 FileChannel.MapMode.PRIVATE, 0, channel.size( )); 23 // the buffer states before any modifications 24 System.out.println ("Begin"); 25 showBuffers (ro, rw, cow); 26 // Modify the copy-on-write buffer 27 cow.position (8); 28 cow.put ("COW".getBytes( )); 29 System.out.println ("Change to COW buffer"); 30 showBuffers (ro, rw, cow); 31 // Modify the read/write buffer 32 33 rw.position (9); 34 rw.put (" R/W ".getBytes( )); 35 rw.position (8194); 36 rw.put (" R/W ".getBytes( )); 37 rw.force( ); 38 System.out.println ("Change to R/W buffer"); 39 showBuffers (ro, rw, cow); 40 // Write to the file through the channel; hit both pages 41 temp.clear( ); 42 temp.put ("Channel write ".getBytes( )); 43 temp.flip( ); 44 channel.write (temp, 0); 45 temp.rewind( ); 46 channel.write (temp, 8202); 47 System.out.println ("Write on channel"); 48 showBuffers (ro, rw, cow); 49 // Modify the copy-on-write buffer again 50 cow.position (8207); 51 cow.put (" COW2 ".getBytes( )); 52 System.out.println ("Second change to COW buffer"); 53 showBuffers (ro, rw, cow); 54 // Modify the read/write buffer 55 rw.position (0); 56 rw.put (" R/W2 ".getBytes( )); 57 rw.position (8210); 58 rw.put (" R/W2 ".getBytes( )); 59 rw.force( ); 60 System.out.println ("Second change to R/W buffer"); 61 showBuffers (ro, rw, cow); 62 // cleanup 63 channel.close( ); 64 file.close( ); 65 tempFile.delete( ); 66 } 67 // Show the current content of the three buffers 68 public static void showBuffers (ByteBuffer ro, ByteBuffer rw, ByteBuffer cow) throws Exception { 69 dumpBuffer ("R/O", ro); 70 dumpBuffer ("R/W", rw); 71 dumpBuffer ("COW", cow); 72 System.out.println (""); 73 } 74 // Dump buffer content, counting and skipping nulls 75 public static void dumpBuffer (String prefix, ByteBuffer buffer) throws Exception { 76 System.out.print (prefix + ": '"); 77 int nulls = 0; 78 int limit = buffer.limit( ); 79 for (int i = 0; i < limit; i++) { 80 char c = (char) buffer.get (i); 81 if (c == '\u0000') { 82 nulls++; 83 continue; 84 } 85 if (nulls != 0) { 86 System.out.print ("|[" + nulls 87 + " nulls]|"); 88 nulls = 0; 89 } 90 System.out.print (c); 91 } 92 System.out.println ("'"); 93 } 94 }
该例展示了三种模式的特点,特别是PRIVATE模式下,当写入的数据位置超过映射size时,再次读取时PRIVATE模式也能看到超出位置的被修改的部分,但size范围内的修改不可见(不同操作系统可能结果不同)。
下例使用映射文件和gathering写操作来编写HTTP回复,大大简化了操作。
1 public class MappedHttp { 2 private static final String OUTPUT_FILE = "MappedHttp.out"; 3 private static final String LINE_SEP = "\r\n"; 4 private static final String SERVER_ID = "Server: Ronsoft Dummy Server"; 5 private static final String HTTP_HDR = 6 "HTTP/1.0 200 OK" + LINE_SEP + SERVER_ID + LINE_SEP; 7 private static final String HTTP_404_HDR = 8 "HTTP/1.0 404 Not Found" + LINE_SEP + SERVER_ID + LINE_SEP; 9 private static final String MSG_404 = "Could not open file: "; 10 11 public static void main(String[] args) throws IOException { 12 //用gather封装正常返回时的报文头,同时保留实体部分 13 ByteBuffer headerOK = ByteBuffer.wrap(HTTP_HDR.getBytes()); 14 ByteBuffer entityInf = ByteBuffer.allocate(128); 15 ByteBuffer[] gather = new ByteBuffer[]{headerOK,entityInf,null}; 16 17 //用通道读取文件,直接使用内存映射,填充实体部分 18 FileInputStream input = new FileInputStream(new File("C:\\Users\\zxc38\\Downloads\\NIO\\Test.txt")); 19 FileChannel channel = input.getChannel(); 20 MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); 21 gather[2] = map; 22 23 //发送报文,产生异常则填充异常报文 24 try { 25 channel.read(gather); 26 } catch (IOException e) { 27 // 文件异常,则发送404报文 28 gather[0] = ByteBuffer.wrap(HTTP_404_HDR.getBytes()); 29 gather[2] = ByteBuffer.wrap(MSG_404.getBytes()); 30 } 31 32 //发送报文(这里以FileOut进行模拟 33 FileOutputStream outputStream = new FileOutputStream(OUTPUT_FILE); 34 FileChannel outChannel = outputStream.getChannel(); 35 while (outChannel.write(gather)>0){} 36 37 channel.close(); 38 outChannel.close(); 39 } 40 } 41
4.1 Channel-to-Channel
FileChannel提供了transferTo( )和 transferFrom( )方法,该方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。虽然该方法不能直接在socket通道之间直接传输数据,但文件的内容可以用transferTo( )方法传输给 一个 socket 通道,也可以用transferFrom( )方法将数据从一个socket通道直接读取到一个文件中。
position参数表示开始位置,传输的字节数不超过count参数的值,实际传输的字节数会由方法返回,可能少于您请求的字节数。对于transferTo( )方法,如果position + count的值大于文件的size值,传输会在文件尾的位置终止;对于 transferFrom( )方法,如果来源src是另外一个FileChannel并且已经到达文件尾,那么传输将提早终止。
1 public class ChannelTransfer { 2 public static void main(String[] args) throws IOException { 3 FileInputStream input = new FileInputStream(new File(".\\file\\mapfile.txt")); 4 FileChannel inputChannel = input.getChannel(); 5 6 long l = inputChannel.transferTo(inputChannel.position(), inputChannel.size(), Channels.newChannel(System.out)); 7 System.out.println(l); 8 inputChannel.close(); 9 } 10 } 11
五、Socket通道
全部socket通道类都是继承AbstractSelectableChannel,可以运行非阻塞模式并且是可选择的,这是NIO的最大亮点。socket通道类在被实例化时都会创建一个对等socket对象,通过调用socket( )方法可以从通道上获取对等的socket对象,该对象已经被更新以识别通道。
虽然每个socket通道都有一个关联的socket对象,但不是所有的socket都有一个关联的通道。如果直接通过实例化创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel( )方法将总是返回null。 socket通道委派协议操作给对等socket对象。一个socket通道有一个关联的socket对象,无论调用多少次socket( )返回的是同一个socket对象。
5.1 非阻塞模式
非阻塞模式的设置只用调用SelectableChannel.configureBlocking( )即可,这里需要注意的是,blockingLock( )方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的,只有拥有此对象的锁的线程才能更改通道的阻塞模式,它对于确保在执行代码的关键部分时,socket通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说是有利的。
1 Socket socket = null; 2 Object lockObj = serverChannel.blockingLock( ); 3 4 // have a handle to the lock object, but haven't locked it yet 5 // may block here until lock is acquired 6 synchronize (lockObj) { 7 // This thread now owns the lock; mode can't be changed 8 boolean prevState = serverChannel.isBlocking( ); 9 serverChannel.configureBlocking (false); 10 socket = serverChannel.accept( ); 11 serverChannel.configureBlocking (prevState); 12 } 13 14 // lock is now released, mode is allowed to change 15 if (socket != null) { 16 doSomethingWithTheSocket (socket); 17 }
5.2 ServerSocketChannel
用静态的open( )工厂方法创建一个新的ServerSocketChannel对象,将会返回同一个未绑定的ServerSocket关联的通道。该对等ServerSocket可以通过在返回的ServerSocketChannel上调用socket( )方法来获取。
需要注意的是,ServerSocketChannel和其对等体ServerSocket都有accept( )方法, 如果在ServerSocket上调用accept( )方法,那么它和实例化的ServerSocket一样,总是阻塞并返回一个Socket对象;如果在ServerSocketChannel上调用accept( )方法,则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。两种形式的方法调用都执行相同的安全检查。
1 public class ChannelAccept { 2 public static final String GREETING = "Hello I must be going.\r\n"; 3 4 public static void main (String [] argv) throws Exception { 5 int port = 1234; // default 6 if (argv.length > 0) { 7 port = Integer.parseInt (argv [0]); 8 } 9 ByteBuffer buffer = ByteBuffer.wrap (GREETING.getBytes( )); 10 ServerSocketChannel ssc = ServerSocketChannel.open( ); 11 ssc.socket( ).bind (new InetSocketAddress(port)); 12 ssc.configureBlocking (false); 13 14 while (true) { 15 System.out.println ("Waiting for connections"); 16 SocketChannel sc = ssc.accept( ); 17 if (sc == null) { 18 // no connections, snooze a while 19 Thread.sleep (2000); 20 } else { 21 System.out.println ("Incoming connection from: " 22 + sc.socket().getRemoteSocketAddress( )); 23 buffer.rewind( ); 24 sc.write (buffer); 25 sc.close( ); 26 } 27 } 28 } 29 }
5.3 SocketChannel
同ServerSocketChannel一样,SocketChannel也通过工厂方法创建,并通过调用socket( )方法能返回对等的Socket对象,在该Socket上调用getChannel( )方法则能返回SocketChannel。注意,每个SocketChannel对象都会创建一个对等的Socket对象,但反过来却不成立。实例化创建的Socket对象不会关联SocketChannel对象,其getChannel( )方法只返回 null。
新创建的SocketChannel是已打开但却未连接的,SocketChannel和Socket都有connect( )方法,在SocketChannel调用,默认是阻塞的,但可以通过configureBlocking( )设置为非阻塞模式;在Socket上调用,则无法设置非阻塞模式。非阻塞模式下,connect( )方法立即返回,true表示连接成功,false连接失败。连接需要一个过程,isConnectPending( )返回是否处于连接等待中。
1 SocketChannel socketChannel = 2 SocketChannel.open (new InetSocketAddress ("somehost", somePort)); 3 //两段代码等价 4 SocketChannel socketChannel = SocketChannel.open( ); 5 socketChannel.connect (new InetSocketAddress ("somehost", somePort));
finishConnect( )方法用于完成套接字通道的连接过程,该方法任何时候都可以安全地进行调用。在非阻塞模式下调用finishConnect( )方法,将可能出现:
- connect( )方法尚未被调用,则将产生NoConnectionPendingException异常。
- 连接建立过程正在进行,尚未完成,则什么都不会发生,finishConnect( )方法会立即返回false值。
- 在非阻塞模式下调用connect( )方法后,SocketChannel又被切换回了阻塞模式。如果有必要,调用该方法的线程会阻塞直到连接建立完成,finishConnect( )方法接着就会返回true值。
- 在初次调用connect( )或最后一次调用finishConnect( )之后,连接建立过程已经完成。则SocketChannel 对象的内部状态将被更新到已连接状态,finishConnect( )方法会返回true值。
- 连接已经建立。则什么都不会发生,finishConnect( )方法会返回 true 值。
Socket通道是线程安全的。并发访问时无需特别措施来保护发起访问的多个线程,不过任何时候都只有一个读操作和一个写操作在进行中。connect( )和finishConnect( )方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。
5.4 DatagramChannel
DatagramChannel 是无连接的,它可以接收来自任意地址的数据包。但和DatagramSocket一样,它也提供connect( )方法,将数据报对话限制为两方,这样DatagramChannel将被置于已连接的状态,使得除了它所“连接”到的地址之外的任何其他源地址的数据报被忽略。其优点是可以将不想要的包在网络层就丢弃,避免了使用代码接收、检查然后丢弃包的麻烦。但当DatagramChannel已连接时,不能发送包到除了指定给connect( )方法的目的地址以外的任何其他地址否则会抛出SecurityException异常。
当DatagramChannel处于已连接状态时,发送数据将不用提供目的地址而且接收时的源地址也是已知的。也就意味着DatagramChannel已连接时可以使用常规的read( )和write( )方法。
DatagramChannel可以任意次数地进行连接或断开连接,每次连接都可以到一个不同的远程地址。调用disconnect( )方法可以配置通道,以便它能再次接收来自安全管理器所允许的任意远程地址的数据或发送数据到这些地址上。
1 public class TimeServer implements Serializable { 2 private static final int DEFAULT_TIME_PORT = 37; 3 private static final long DIFF_1900 = 2208988800L; 4 protected DatagramChannel channel; 5 6 public TimeServer (int port) throws Exception { 7 this.channel = DatagramChannel.open( ); 8 this.channel.socket( ).bind (new InetSocketAddress(port)); 9 System.out.println ("Listening on port " + port 10 + " for time requests"); 11 } 12 13 public void listen( ) throws Exception { 14 ByteBuffer longBuffer = ByteBuffer.allocate (8); 15 longBuffer.order (ByteOrder.BIG_ENDIAN); 16 longBuffer.putLong (0, 0); 17 longBuffer.position (4); 18 ByteBuffer buffer = longBuffer.slice( ); 19 while (true) { 20 buffer.clear( ); 21 SocketAddress sa = this.channel.receive (buffer); 22 if (sa == null) { 23 continue; 24 } 25 System.out.println ("Time request from " + sa); 26 buffer.clear( ); 27 longBuffer.putLong (0, 28 (System.currentTimeMillis( ) / 1000) + DIFF_1900); 29 this.channel.send (buffer, sa); 30 } 31 } 32 33 public static void main (String [] argv) throws Exception { 34 int port = DEFAULT_TIME_PORT; 35 if (argv.length > 0) { 36 port = Integer.parseInt (argv [0]); 37 } 38 try { 39 TimeServer server = new TimeServer (port); 40 server.listen( ); 41 } catch (SocketException e) { 42 System.out.println ("Can't bind to port " + port 43 + ", try a different one"); 44 } 45 } 46 }
六、管道
管道就是一个用来在两个实体之间单向传输数据的导管。Unix 系统中,管道被用来连接一个进程的输出和另一个进程的输入。而Pipe类是实现一个管道范例,它所创建的管道是进程内(在Java虚拟机进程内部)而非进程间使用的。
Pipe类创建一对提供环回机制的Channel对象,这两个通道的远端是连接起来的,以便任何写在SinkChannel对象上的数据都能出现在SourceChannel对象上。Pipe实例通过Pipe.open( )工厂方法来创建,Pipe类定义了两个嵌套的通道类:Pipe.SourceChannel(管道负责读的一端)和Pipe.SinkChannel(管道负责写的一端),来实现管路。这两个通道实例是在Pipe对象创建的同时被创建的,通过调用source( )和sink( )方法来取回。
1 public abstract class Pipe{ 2 public static Pipe open( ) throws IOException 3 public abstract SourceChannel source( ); 4 public abstract SinkChannel sink( ); 5 6 public static abstract class SourceChannel 7 extends AbstractSelectableChannel 8 implements ReadableByteChannel, ScatteringByteChannel 9 10 public static abstract class SinkChannel 11 extends AbstractSelectableChannel 12 implements WritableByteChannel, GatheringByteChannel 13 }
管路所能承载的数据量是依赖实现的(implementation-dependent),唯一可保证的是写到SinkChannel中的字节都能按照同样的顺序在SourceChannel上重现。Pipe有很好的封装性,大大简化了工作,并且它还能和选择器一起使用。
1 public class PipeTest { 2 public static void main (String [] argv) throws Exception { 3 WritableByteChannel out = Channels.newChannel (System.out); 4 ReadableByteChannel workerChannel = startWorker (10); 5 ByteBuffer buffer = ByteBuffer.allocate (100); 6 while (workerChannel.read (buffer) >= 0) { 7 buffer.flip( ); 8 out.write (buffer); 9 buffer.clear( ); 10 } 11 } 12 13 private static ReadableByteChannel startWorker (int reps) throws Exception { 14 Pipe pipe = Pipe.open( ); 15 Worker worker = new Worker (pipe.sink( ), reps); 16 worker.start( ); 17 return (pipe.source( )); 18 } 19 20 private static class Worker extends Thread { 21 WritableByteChannel channel; 22 private int reps; 23 Worker (WritableByteChannel channel, int reps) { 24 this.channel = channel; 25 this.reps = reps; 26 } 27 // Thread execution begins here 28 public void run( ) { 29 ByteBuffer buffer = ByteBuffer.allocate (100); 30 try { 31 for (int i = 0; i < this.reps; i++) { 32 doSomeWork (buffer); 33 34 while (channel.write (buffer) > 0) { 35 // empty 36 } 37 } 38 this.channel.close( ); 39 } catch (Exception e) { 40 // easy way out; this is demo code 41 e.printStackTrace( ); 42 } 43 } 44 45 private String [] products = { 46 "No good deed goes unpunished", 47 "To be, or what?", 48 "No matter where you go, there you are", 49 "Just say \"Yo\"", 50 "My karma ran over my dogma" 51 }; 52 53 private Random rand = new Random(); 54 55 private void doSomeWork (ByteBuffer buffer) { 56 int product = rand.nextInt (products.length); 57 buffer.clear( ); 58 buffer.put (products [product].getBytes( )); 59 buffer.put ("\r\n".getBytes( )); 60 buffer.flip( ); 61 } 62 } 63 } 64