java nio的学习-7

5 篇文章 0 订阅
5 篇文章 0 订阅

continue----------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


Socket 通道:Socket 通道有与文件通道不同的特征。

新的socket 通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失

java.nio.channels
Class SocketChannel

java.lang.Object
  extended by java.nio.channels.spi.AbstractInterruptibleChannel
      extended by java.nio.channels.SelectableChannel
          extended by java.nio.channels.spi.AbstractSelectableChannel
              extended by java.nio.channels.SocketChannel
All Implemented Interfaces:
Closeable, ByteChannel, Channel, GatheringByteChannel, InterruptibleChannel, ReadableByteChannel, ScatteringByteChannel, WritableByteChannel

public abstract class SocketChannel
   
   
    
    extends 
    
    AbstractSelectableChannel
   
   
   
   
    
    implements 
    
    ByteChannel, 
    
    ScatteringByteChannel, 
    
    GatheringByteChannel
   
   
全部 socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel )都是由位于 java.nio.channels.spi 包中的 AbstractSelectableChannel 引申而来。这意味着我们 可以用一个 Selector 对象来执行 socket 通道的有条件的选择(readiness selection )




请注意 DatagramChannel 和SocketChannel 实现定义读和写功能的接口而 ServerSocketChannel不实现。ServerSocketChannel 负责监听传入的连接和创建新的SocketChannel 对象,它本身从不传输数据

public abstract Socket socket()
Retrieves a socket associated with this channel.

The returned object will not declare any public methods that are not declared in theSocket class.

Returns:
A socket associated with this channel
全部socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel )在被实例化时都会创建一个对等 socket 对象。这些是我们所熟悉的来自 java.net 的类(Socket 、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等 socket 可以通过调用 socket( ) 方法从一个通道上获取。此外,这三个 java.net 类现在都有 getChannel(  )方法。

虽然每个 socket 通道(在 java.nio.channels包中)都有一个关联的 java.net  socket 对象,却并非所有的 socket 都有一个关联的通道。如果您用传统方式(直接实例化)创建了一个Socket 对象,它就不会有关联的 SocketChannel 并且它的 getChannel( )方法将总是返回 null


非阻塞模式 :Socket 通道可以在非阻塞模式下运行。这个陈述虽然简单却有着深远的含义。传统 Java socket的阻塞性质曾经是 Java 程序可伸缩性的最重要制约之一。非阻塞 I/O 是许多复杂的、高性能的程序构建的基础。

要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。下面的方法就是关于通道的阻塞模式的: 

public abstract class SelectableChannel 
extends AbstractChannel  
implements Channel 
{ 
// This is a partial API listing 
public abstract void configureBlocking (boolean block) 
throws IOException; 
public abstract boolean isBlocking();  
 
public abstract Object blockingLock(); 
}


blockingLock()
          Retrieves the object upon which the configureBlocking andregister methods synchronize.


configureBlocking(boolean block)
          Adjusts this channel's blocking mode.

isBlocking()
          Tells whether or not every I/O operation on this channel will block until it completes.

有条件的选择(readiness selection )是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞 I/O 和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在 SelectableChannel 超级类中定义的原因

设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。真的,就这么简单!您可以通过调用 isBlocking( )方法来判断某个 socket 通道当前处于哪种模式:

SocketChannel sc = SocketChannel.open( ); 
sc.configureBlocking (false); // nonblocking 
...  
if ( ! sc.isBlocking( )) { 
doSomething (cs); 
}

服务器端的使用经常会考虑到非阻塞 socket 通道,因为它们使同时管理很多 socket 通道变得更容易。但是,在 客户端使用一个或几个非阻塞模式的 socket 通道也是有益处的,例如,借助非阻塞socket 通道,GUI 程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。 在很多程序上,非阻塞模式都是有用的

偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个 blockingLock( )方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式(对象的锁是用同步的 Java 密码获取的,它不同于我们在 3.3节中介绍的 lock( ) 方法)。对于确保在执行代码的关键部分时 socket 通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说,这个方法都是非常方便的

Socket socket = null; 
Object lockObj = serverChannel.blockingLock( ); 
// have a handle to the lock object, but haven't locked it yet 
// may block here until lock is acquired 
synchronize (lockObj) 
{ 
// This thread now owns the lock; mode can't be changed 
boolean prevState = serverChannel.isBlocking( );  
serverChannel.configureBlocking (false); 
socket = serverChannel.accept( );  
serverChannel.configureBlocking (prevState); 
} 
// lock is now released, mode is allowed to change 
if (socket != null) { 
doSomethingWithTheSocket (socket); 
}


ServerSocketChannel :

public abstract class ServerSocketChannel 
extends AbstractSelectableChannel  
{ 
public static ServerSocketChannel open() throws IOException 
public abstract ServerSocket socket(); 
pub lic abstract ServerSocket accept() throws IOException; 
public final int validOps()  
}
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket执行相同的基本任务,不过它增加了 通道 语义,因此 能够在非阻塞模式下运行

用静态的 open( ) 工厂方法创建一个新的 ServerSocketChannel 对象,将会返回同一个未绑定的java.net.Ser verSocket 关联的通道。该对等 ServerSocket可以通过在返回的 ServerSocketChannel 上调用socket( ) 方法来获取。作为 ServerSocketChannel 的对等体被创建的 ServerSocket对象依赖通道实现。这些 socket 关联的 SocketImpl 能识别通道。通道不能被封装在随意的 socket 对象外面。 由于ServerSocketChannel 没有bind( ) 方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等 Serv erSocket 的API 来根据需要设置其他的 socket 选项。

ServerSocketChannel ssc = ServerSocketChannel.open(); 
ServerSocket serverSocket = ssc.socket(); 
// Listen on port 1234  
 
serverSocket.bind (new InetSocketAddress (1234)); 

同它的对等体 java.net.ServerSocket 一样,ServerSocketChannel 也有accept( )方法。一旦您创建了一个 ServerSocketChannel 并用对等 socket 绑定了它,然后您就可以在其中一个上调用 accept( ) 。如果您选择在 ServerSocket上调用accept( ) 方法,那么它会同任何其他的 ServerSocket表现一样的行为:总是阻塞并返回一个 java.net.Socket对象。如果您选择在 ServerSocketChannel 上调用 accept( )方法则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。假设系统已经有一个安全管理器(security manager ),两种形式的方法调用都执行相同的安全检查。

public abstract SocketChannel accept()
                              throws IOException
Accepts a connection made to this channel's socket.

If this channel is in non-blocking mode then this method will immediately returnnull if there are no pending connections. Otherwise it will block indefinitely until a new connection is available or an I/O error occurs.  

如果以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept( )会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。

可选择性也因此得到实现。我们可以使用一个选择器实例来注册一个 ServerSocketChannel 对象以实现新连接到达时自动通知的功能。下例演示了如何使用一个非阻塞的 accept( )方法:

package com.ronsoft.books.nio.channels;

import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;

/**
 * Test nonblocking accept() using ServerSocketChannel. Start this program,
 * then " telnet localhost 1234" to connect to it.
 * 
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class ChannelAccept {
	public static final String GREETING = "Hello I must be going.\r\n";

	public static void main(String[] argv) throws Exception {
		int port = 1234; // default
		if (argv.length > 0) {
			port = Integer.parseInt(argv[0]);
		}
		ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
		ServerSocketChannel ssc = ServerSocketChannel.open();

		ssc.socket().bind(new InetSocketAddress(port));
		ssc.configureBlocking(false);
		while (true) {
			System.out.println("Waiting for connections");
			SocketChannel sc = ssc.accept();
			if (sc == null) {
				// no connections, snooze a while
				Thread.sleep(2000);
			} else {
				System.out.println("Incoming connection from: "
						+ sc.socket().getRemoteSocketAddress());
				buffer.rewind();
				sc.write(buffer);
				sc.close();
			}
		}
	}
}

结果:
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Incoming connection from: /127.0.0.1:2893
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
Waiting for connections
结果截图:


SocketChannel:它是使用最多的 socket通道类。

public abstract class SocketChannel 
extends AbstractSelectableChannel  
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel 
{ 
// This is a partial API listing 
public static SocketChannel open() throws IOException 
public static Sock etChannel open (InetSocketAddress remote) 
throws IOException  
public abstract Socket socket();  
public abstract boolean connect (SocketAddress remote) 
throws IOException; 
 
public abstract boolean isConnectionPending(); 
public abstract boolean finishConnect() throws IOException; 
public abstract boolean isConnected(); 
public final int validOps()  
}

Socket 和SocketChannel 类封装点对点、有序的网络连接,类似于我们所熟知并喜爱的 TCP/IP网络连接。 SocketChannel 扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收

每个SocketChannel 对象创建时都是同一个对等的 java.net.Socket对象串联的。静态的 open( ) 方法可以创建一个新的 SocketChannel 对象,而在新创建的 SocketChannel 上调用 socket( ) 方法能返回它对等的 Socket 对象;在该 Socket 上调用 getChannel( )方法则能返回最初的那个 SocketChannel 。 
 
虽然每个 SocketChannel 对象都会创建一个对等的 Socket 对象,反过来却不成立。直接创建的 Socket 对象不会关联 SocketChannel 对象,它们的getChannel( )方法只返回 null。

新创建的 SocketChannel 虽已打开却是未连接的。在一个未连接的 SocketChannel 对象上尝试一个I/O 操作会导致 NotYetConnectedException 异常。我们可以通过在通道上直接调用 connect( )方法或在通道关联的 Socket 对象上调用 connect( )来将该 socket 通道连接。一旦一个 socket 通道被连接,它将保持连接状态直到被关闭。您可以通过调用布尔型的 isConnected( ) 方法来测试某个SocketChannel 当前是否已连接。 
第二种带 InetSocketAddress 参数形式的open( ) 是在返回之前进行连接的便捷方法。这段代码: 

SocketChannel socketChannel = 
SocketChannel.open (new InetSocketAddress ("somehost", somePort)); 
 

等价于下面这段代码: 
SocketChannel socketChannel = SocketChannel.open(); 
socketChannel.connect (new InetSocketAddress ("somehost", somePort)); 
如果您选择使用传统方式进行连接——通过在对等 Socket 对象上调用 connect( )方法,那么传统的连接语义将适用于此。线程在连接建立好或超时过期之前都将保持阻塞。如果您选择通过在通道上直接调用 connect( )方法来建立连接并且通道处于阻塞模式(默认模式),那么连接过程实际上是一样的。


在SocketChannel 上并没有一种 connect( )方法可以让您指定超时(timeout)值,当 connect( )方法在非阻塞模式下被调用时 SocketChannel 提供并发连接:它发起对请求地址的连接并且立即返回值。如果返回值是 true,说明连接立即建立了(这可能是本地环回连接);如果连接不能立即建立,connect( )方法会返回 false 且并发地继续连接建立过程

面向流的的socket 建立连接状态需要一定的时间,因为两个待连接系统之间必须进行包对话以建立维护流 socket 所需的状态信息。跨越开放互联网连接到远程系统会特别耗时。假如某个SocketChannel 上当前正由一个并发连接,isConnectPending( )方法就会返回 true 值。


调用finishConnect( ) 方法来完成连接过程,该方法任何时候都可以安全地进行调用。假如在一个非阻塞模式的 SocketChannel 对象上调用 finishConnect( ) 方法,将可能出现下列情形之一: 
  connect( )方法尚未被调用。那么将产生 NoConnectionPendingException异常。 
  连接建立过程正在进行,尚未完成。那么什么都不会发生,finishConnect( ) 方法会立即返回false 值。 
  在非阻塞模式下调用 connect( )方法之后,SocketChannel 又被切换回了阻塞模式。那么如果有必要的话,调用线程会阻塞直到连接建立完成,finishConnect( ) 方法接着就会返回 true值。 
  在初次调用 connect( )或最后一次调用 finishConnect( ) 之后,连接建立过程已经完成。那么SocketChannel 对象的内部状态将被更新到已连接状态,finishConnect( ) 方法会返回true值,然后 SocketChannel 对象就可以被用来传输数据了。 
  连接已经建立。那么什么都不会发生,finishConnect( ) 方法会返回 true 值。 


当通道处于中间的连接等待(connection-pending)状态时,您只可以调用 finishConnect( ) 、isConnectPending( )或isConnected( ) 方法。一旦连接建立过程成功完成,isConnected( ) 将返回 true值。

InetSocketAddress addr = new InetSocketAddress (host, port); 
SocketChannel sc = SocketChannel.open(); 
sc.configureBlocking (false); 
sc.connect (addr); 
while (! sc.finishConnect()) { 
doSomethingElse(); 
} 
doSomethingWithChannel (sc);  
sc.close();


下例 是一段用来管理异步连接的可用代码。 

package com.ronsoft.books.nio.channels;

import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;

/**
 * Demonstrate asynchronous connection of a SocketChannel.
 * 
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class ConnectAsync {
	public static void main(String[] argv) throws Exception {
		String host = "localhost";
		int port = 80;
		if (argv.length == 2) {
			host = argv[0];
			port = Integer.parseInt(argv[1]);
		}
		InetSocketAddress addr = new InetSocketAddress(host, port);
		SocketChannel sc = SocketChannel.open();
		sc.configureBlocking(false);
		System.out.println("initiating connection");
		sc.connect(addr);
		while (!sc.finishConnect()) {
			doSomethingUseful();
		}
		System.out.println("connection established");
		// Do something with the connected socket
		// The SocketChannel is still nonblocking
		sc.close();
	}

	private static void doSomethingUseful() {
		System.out.println("doing something useless");
	}
}

如果尝试异步连接失败,那么下次调用 finishConnect( ) 方法会产生一个适当的经检查的异常以指出问题的性质。通道然后就会被关闭并将不能被连接或再次使用。 与连接相关的方法使得我们可以对一个通道进行轮询并在连接进行过程中判断通道所处的状态。


Socket通道是线程安全的。并发访问时无需特别措施来保护发起访问的多个线程,不过任何时候都只有一个读操作和一个写操作在进行中。请记住,sockets 是面向流的而非包导向的。它们可以保证发送的字节会按照顺序到达但无法承诺维持字节分组。某个发送器可能给一个 socket 写入了20个字节而接收器调用 read( ) 方法时却只收到了其中的 3 个字节。剩下的 17 个字节还是传输中。由于这个原因,让多个不配合的线程共享某个流 socket 的同一侧绝非一个好的设计选择。 

connect( )和finishConnect( ) 方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。如果此情形下您有疑问或不能承受一个读或写操作在某个通道上阻塞,请用isConnected( ) 方法测试一下连接状态。


DatagramChannel  :最后一个 socket 通道是 DatagramChannel。正如 SocketChannel 对应 Socket ,ServerSocketChannel 对应 ServerSocket,每一个DatagramChannel对象也有一个关联的DatagramSocket 对象。不过原命名模式在此并未适用:“DatagramSocketChannel ”显得有点笨拙,
因此采用了简洁的“DatagramChannel”名称
。 
正如SocketChannel 模拟连接导向的流协议(如 TCP/IP ),DatagramChannel则模拟包导向的无连接协议(如 UDP/IP): 

public abstract class DatagramChannel 
extends AbstractSelectableChannel  
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel 
{ 
// This is a partial API listing 
public static DatagramChannel open() throws IOException 
public abstract DatagramSocket socket(); 
public abstract DatagramChannel connect (SocketAddress remote) 
throws IOException; 
public abstract boolean isConnected(); 
public abstract DatagramChannel disconnect() throws IOException; 
public abstract SocketAddress receive (ByteBuffer dst) 
throws IOException; 
 
public abstract int send (ByteBuffer src, SocketAddress target)  
public abstract int read (ByteBuffer dst) throws IOException; 
public abstract long read (ByteBuffer [] dsts) throws IOException; 
public abstract long read (ByteBuffer [] dsts, int offset,  
int length) 
throws IOException; 
public abstract int write (ByteBuffer src) throws IOException; 
public abstract long write(ByteBuffer[] srcs) throws IOException;  
pub lic abstract long write(ByteBuffer[] srcs, int offset, 
int length) 
throws IOException; 
}
创建DatagramChannel的模式和创建其他 socket 通道是一样的:调用静态的 open( ) 方法来创建一个新实例。新 DatagramChannel会有一个可以通过调用 socket( )方法获取的对等 DatagramSocket对象。 DatagramChannel对象既可以充当服务器(监听者)也可以充当客户端(发送者)。如果您希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。绑定DatagramChannel同绑定一个常规的 DatagramSocket 没什么区别,都是委托对等socket 对象上的API 实现的:

DatagramChannel channel = DatagramChannel.open();
DatagramSocket socket = channel.socket(); 
socket.bind (new InetSocketAddress (portNumber)); 

DatagramChannel是无连接的。每个数据报(datagram )都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据净荷。与面向流的的socket 不同,DatagramChannel 可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)

数据的实际发送或接收是通过 send( )和receive( ) 方法来实现的:

public abstract class DatagramChannel 
extends AbstractSelectableChannel   
 
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel 
{ 
// This is a partial API listing 
public abstract SocketAddress receive (ByteBuffer dst) 
throws IOException; 
public abstract int send (ByteBuffer src, SocketAddress target)  
}

public abstract SocketAddress receive(ByteBuffer dst)
                               throws IOException
Receives a datagram via this channel.

If a datagram is immediately available, or if this channel is in blocking mode and one eventually becomes available, then the datagram is copied into the given byte buffer and its source address is returned. If this channel is in non-blocking mode and a datagram is not immediately available then this method immediately returns null.

receive( ) 方法将下次将传入的数据报的数据净荷复制到预备好的 ByteBuffer 中并返回一个SocketAddress 对象以指出数据来源。如果通道处于阻塞模式,receive( ) 可能无限期地休眠直到有包到达。如果是非阻塞模式,当没有可接收的包时则会返回 null。如果包内的数据超出缓冲区能承受的范围,多出的数据都会被悄悄地丢弃。 
 
public abstract int send(ByteBuffer src,
                         SocketAddress target)
                  throws IOException
Sends a datagram via this channel.

If this channel is in non-blocking mode and there is sufficient room in the underlying output buffer, or if this channel is in blocking mode and sufficient room becomes available, then the remaining bytes in the given buffer are transmitted as a single datagram to the given target address.

调用send( ) 会发送给定 ByteBuffer对象的内容到给定 SocketAddress 对象所描述的目的地址和端口,内容范围为从当前 position开始到末尾处结束。如果 DatagramChannel对象处于阻塞模式,调用线程可能会休眠直到数据报被加入传输队列。如果通道是非阻塞的,返回值要么是字节缓冲区的字节数,要么是“0”。 发送数据报是一个全有或全无(all -or -nothing)的行为。如果传输队列没有足够空间来承载整个数据报,那么什么内容都不会被发送。

如果安装了安全管理器,那么每次调用 send( ) 或receive( ) 时安全管理器的 checkConnect( ) 方法都会被调用以验证目的地址,除非通道处于已连接的状态。


请注意,数据报协议的不可靠性是固有的,它们不对数据传输做保证。send( ) 方法返回的非零值并不表示数据报到达了目的地,仅代表数据报被成功加到本地网络层的传输队列。此外,传输过程中的协议可能将数据报分解成碎片。例如,以太网不能传输超过 1,500个字节左右的包。如果您的数据报比较大,那么就会存在被分解成碎片的风险,成倍地增加了传输过程中包丢失的几率。被分解的数据报在目的地会被重新组合起来,接收者将看不到碎片。但是,如果有一个碎片不能按时到达,那么整个数据报将被丢弃。

public abstract class DatagramChannel 
extends AbstractSelectableChannel  
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel 
 
{ 
// This is a partial API listing 
public abstract DatagramChannel connect (SocketAddress remote) 
throws IOException; 
public abstract boolean isConnected(); 
public abstract DatagramChannel disconnect() throws IOException;  
}

DatagramChannel 对数据报 socket 的连接语义不同于对流 socket 的连接语义。有时候,将数据报对话限制为两方是很可取的。将 DatagramChannel置于已连接的状态可以使除了它所“连接”到的地址之外的任何其他源地址的数据报被忽略。这是很有帮助的,因为不想要的包都已经被网络层丢弃了,从而避免了使用代码来接收、检查然后丢弃包的麻烦。


当DatagramChannel已连接时,使用同样的令牌,您不可以发送包到除了指定给 connect( )方法的目的地址以外的任何其他地址。试图一定要这样做的话会导致一个SecurityException 异常。

我们可以通过调用带 SocketAddress 对象的 connect( )方法来连接一个 DatagramChannel,该SocketAddress 对象描述了 DatagramChannel远程对等体的地址。如果已经安装了一个安全管理器,那么它会进行权限检查。之后,每次 send/receive 时就不会再有安全检查了,因为来自或去到任何其他地址的包都是不允许的。 

已连接通道会发挥作用的使用场景之一是一个客户端/服务器模式、使用 UDP 通讯协议的实时游戏。每个客户端都只和同一台服务器进行会话而希望忽视任何其他来源地数据包。将客户端的DatagramChannel实例置于已连接状态可以减少按包计算的总开销(因为不需要对每个包进行安全检查)和剔除来自欺骗玩家的假包。服务器可能也想要这样做,不过需要每个客户端都有一个DatagramChannel对象。 
不同于流 socket ,数据报 socket 的无状态性质不需要同远程系统进行对话来建立连接状态。没有实际的连接,只有用来指定允许的远程地址的本地状态信息。由于此原因,DatagramChannel上也就没有单独的 finishConnect( ) 方法。我们可以使用 isConnected( ) 方法来测试一个数据报通道的连接状态。 


不同于 SocketChannel (必须连接了才有用并且只能连接一次),DatagramChannel对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。调用 disconnect( )方法可以配置通道,以便它能再次接收来自安全管理器(如果已安装)所允许的任意远程地址的数据或发送数据到这些地址上。\


当一个 DatagramChannel处于已连接状态时,发送数据将不用提供目的地址而且接收时的源地址也是已知的。这意味着 DatagramChannel 已连接时可以使用常规的 read( ) 和write( ) 方法,包括scatter/gather 形式的读写来组合或分拆包的数据

public abstract class DatagramChannel 
extends AbstractSelectableChannel  
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel 
{ 
// This is a partial API listing 
public a bstract int read (ByteBuffer dst) throws IOException;  
public abstract long read (ByteBuffer [] dsts) throws IOException; 
public abstract long read (ByteBuffer [] dsts, int offset,  
int length) 
throws IOException; 
public abstract int write (ByteBuffer src) throws IOException;  
public abstract long write(ByteBuffer[] srcs) throws IOException;  
public abstract long write(ByteBuffer[] srcs, int offset, 
int length) 
throws IOException; 
} 
read( ) 方法返回读取字节的数量,如果通道处于非阻塞模式的话这个返回值可能是“0”。write( ) 方法的返回值同 send( ) 方法一致:要么返回您的缓冲区中的字节数量,要么返回“0 ”(如果由于通道处于非阻塞模式而导致数据报不能被发送)。当通道不是已连接状态时调用 read( ) 或write( ) 方法,都将产生 NotYetConnectedException 异常。 
数据报通道不同于流 socket 。由于它们的有序而可靠的数据传输特性,流 socket 非常得有用。大多数网络连接都是流 socket (TCP/IP 就是一个显著的例子)。但是,像TCP/IP 这样面向流的的协议为了在包导向的互联网基础设施上维护流语义必然会产生巨大的开销,并且流隐喻不能适用所有的情形。数据报的吞吐量要比流协议高很多,并且数据报可以做很多流无法完成的事情。 
下面列出了一些选择数据报 socket 而非流 socket 的理由: 
  您的程序可以承受数据丢失或无序的数据。 
  您希望“发射后不管”(fire and forget )而不需要知道您发送的包是否已接收。 
  数据吞吐量比可靠性更重要。 
  您需要同时发送数据给多个接受者(多播或者广播)。 
  包隐喻比流隐喻更适合手边的任务。 
 
如果以上特征中的一个或多个适用于您的程序,那么数据报设计对您来说就是合适的。

下例显示了如何使用 DatagramChannel发送请求到多个地址上的时间服务器。DatagramChannel接着会等待回复(reply)的到达。对于每个返回的回复,远程时间会同本地时间进行比较。由于数据报传输不保证一定成功,有些回复可能永远不会到达。大多数 Linux 和Unix系统都默认提供时间服务。互联网上也有一个公共时间服务器,如 time.nist.gov。防火墙或者您的ISP 可能会干扰数据报传输,这是因人而异的。

使用DatagramChannel  的时间服务客户端 如下:

package com.ronsoft.books.nio.channels;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.net.InetSocketAddress;
import java.util.Date;
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;

/**
 * Request time service, per RFC 868. RFC 868
 * (http://www.ietf.org/rfc/rfc0868.txt) is a very simple time protocol whereby
 * one system can request the current time from another system. Most Linux, BSD
 * and Solaris systems provide RFC 868 time service on port 37. This simple
 * program will inter-operate with those. The National Institute of Standards
 * and Technology (NIST) operates a public time server at time.nist.gov.
 * 
 * The RFC 868 protocol specifies a 32 bit unsigned value be sent, representing
 * the number of seconds since Jan 1, 1900. The Java epoch begins on Jan 1, 1970
 * (same as unix) so an adjustment is made by adding or subtracting
 * 2,208,988,800 as appropriate. To avoid shifting and masking, a four-byte
 * slice of an eight-byte buffer is used to send/recieve. But getLong( ) is done
 * on the full eight bytes to get a long value.
 * 
 * When run, this program will issue time requests to each hostname given on the
 * command line, then enter a loop to receive packets. Note that some requests
 * or replies may be lost, which means this code could block forever.
 * 
 * @author Ron Hitchens (ron@ronsoft.com) 112
 */
public class TimeClient {
	private static final int DEFAULT_TIME_PORT = 37;
	private static final long DIFF_1900 = 2208988800L;
	protected int port = DEFAULT_TIME_PORT;
	protected List<InetSocketAddress> remoteHosts;
	protected DatagramChannel channel;

	public TimeClient(String[] argv) throws Exception {
		if (argv.length == 0) {
			throw new Exception("Usage: [ -p port ] host ...");
		}
		parseArgs(argv);
		this.channel = DatagramChannel.open();
	}

	protected InetSocketAddress receivePacket(DatagramChannel channel,
			ByteBuffer buffer) throws Exception {
		buffer.clear();
		// Receive an unsigned 32-bit, big -endian value
		return ((InetSocketAddress) channel.receive(buffer));
	}

	// Send time requests to all the supplied hosts
	protected void sendRequests() throws Exception {
		ByteBuffer buffer = ByteBuffer.allocate(1);
		Iterator<InetSocketAddress> it = remoteHosts.iterator();
		while (it.hasNext()) {
			InetSocketAddress sa = (InetSocketAddress) it.next();
			System.out.println("Requesting time from " + sa.getHostName() + ":"
					+ sa.getPort());
			// Make it empty (see RFC868)
			buffer.clear().flip();
			// Fire and forget
			channel.send(buffer, sa);

		}
	}

	// Receive any replies that arrive
	public void getReplies() throws Exception {
		// Allocate a buffer to hold a long value
		ByteBuffer longBuffer = ByteBuffer.allocate(8);
		// Assure big -endian (network) byte order
		longBuffer.order(ByteOrder.BIG_ENDIAN);
		// Zero the whole buffer to be sure
		longBuffer.putLong(0, 0);
		// Position to first byte of the low-order 32 bits
		longBuffer.position(4);
		// Slice the buffer; gives view of the low-order 32 bits
		ByteBuffer buffer = longBuffer.slice();
		int expect = remoteHosts.size();
		int replies = 0;
		System.out.println("");
		System.out.println("Waiting for replies...");
		while (true) {
			InetSocketAddress sa;
			sa = receivePacket(channel, buffer);
			buffer.flip();
			replies++;
			printTime(longBuffer.getLong(0), sa);
			if (replies == expect) {
				System.out.println("All packets answered");
				break;
			}
			// Some replies haven't shown up yet
			System.out.println("Received " + replies + " of " + expect
					+ " replies");
		}
	}

	// Print info about a received time reply
	protected void printTime(long remote1900, InetSocketAddress sa) {
		// local time as seconds since Jan 1, 1970

		long local = System.currentTimeMillis() / 1000;
		// remote time as seconds since Jan 1, 1970
		long remote = remote1900 - DIFF_1900;
		Date remoteDate = new Date(remote * 1000);
		Date localDate = new Date(local * 1000);
		long skew = remote - local;
		System.out.println("Reply from " + sa.getHostName() + ":"
				+ sa.getPort());
		System.out.println(" there: " + remoteDate);
		System.out.println(" here: " + localDate);
		System.out.print(" skew: ");
		if (skew == 0) {
			System.out.println("none");
		} else if (skew > 0) {
			System.out.println(skew + " seconds ahead");
		} else {
			System.out.println((-skew) + " seconds behind");
		}
	}

	protected void parseArgs(String[] argv) {
		remoteHosts = new LinkedList<InetSocketAddress>();
		for (int i = 0; i < argv.length; i++) {
			String arg = argv[i];
			// Send client requests to the given port
			if (arg.equals("-p")) {
				i++;
				this.port = Integer.parseInt(argv[i]);
				continue;
			}
			// Create an address object for the hostname
			InetSocketAddress sa = new InetSocketAddress(arg, port);
			// Validate that it has an address
			if (sa.getAddress() == null) {
				System.out.println("Cannot resolve address: " + arg);
				continue;
			}

			remoteHosts.add(sa);
		}
	}

	// --------------------------------------------------------------
	public static void main(String[] argv) throws Exception {
		TimeClient client = new TimeClient(argv);
		client.sendRequests();
		client.getReplies();
	}
}

下例中的程序是一个 RFC 868 时间服务器。这段代码回答来自上例 中的客户端的请求并显示出 DatagramChannel是怎样绑定到一个熟知端口然后开始监听来自客户端的请求的。该时间服务器仅监听数据报(UDP )请求。大多数 Unix 和Linux 系统提供的 rdate命令使用 TCP 协议连接到一个 RFC 868 时间服务。

DatagramChannel时间服务器:

package com.ronsoft.books.nio.channels;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.net.SocketAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;

/**
 * Provide RFC 868 time service (http://www.ietf.org/rfc/rfc0868.txt). This code
 * implements an RFC 868 listener to provide time service. The defined port for
 * time service is 37. On most unix systems, root privilege is required to bind
 * to ports below 1024. You can either run this code as root or provide another
 * port number on the command line. Use " -p port#" with TimeClient if you
 * choose an alternate port.
 * 
 * Note: The familiar rdate command on unix will probably not work with this
 * server. Most versions of rdate use TCP rather than UDP to request the time.
 * 116
 * 
 * 
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class TimeServer {
	private static final int DEFAULT_TIME_PORT = 37;
	private static final long DIFF_1900 = 2208988800L;
	protected DatagramChannel channel;

	public TimeServer(int port) throws Exception {
		this.channel = DatagramChannel.open();
		this.channel.socket().bind(new InetSocketAddress(port));
		System.out.println("Listening on port " + port + " for time requests");
	}

	public void listen() throws Exception {
		// Allocate a buffer to hold a long value
		ByteBuffer longBuffer = ByteBuffer.allocate(8);
		// Assure big -endian (network) byte order
		longBuffer.order(ByteOrder.BIG_ENDIAN);
		// Zero the whole buffer to be sure
		longBuffer.putLong(0, 0);
		// Position to first byte of the low-order 32 bits
		longBuffer.position(4);
		// Slice the buffer; gives view of the low-order 32 bits
		ByteBuffer buffer = longBuffer.slice();
		while (true) {
			buffer.clear();
			SocketAddress sa = this.channel.receive(buffer);
			if (sa == null) {
				continue; // defensive programming
			}
			// Ignore content of received datagram per RFC 868
			System.out.println("Time request from " + sa);
			buffer.clear(); // sets pos/limit correctly
			// Set 64-bit value; slice buffer sees low 32 bits

			longBuffer.putLong(0, (System.currentTimeMillis() / 1000)
					+ DIFF_1900);
			this.channel.send(buffer, sa);
		}
	}

	// --------------------------------------------------------------
	public static void main(String[] argv) throws Exception {
		int port = DEFAULT_TIME_PORT;
		if (argv.length > 0) {
			port = Integer.parseInt(argv[0]);
		}
		try {
			TimeServer server = new TimeServer(port);
			server.listen();
		} catch (SocketException e) {
			System.out.println("Can't bind to port " + port
					+ ", try a different one");
		}
	}
}


管道:java.nio.channels包中含有一个名为 Pipe (管道)的类。广义上讲,管道就是一个用来在两个实体之间单向传输数据的导管。管道的概念对于 Unix(和类 Unix)操作系统的用户来说早就很熟悉了。Unix系统中,管道被用来连接一个进程的输出和另一个进程的输入。Pipe 类实现一个管道范例,不过它所创建的管道是进程内(在 Java 虚拟机进程内部)而非进程间使用的

Pipe 类创建一对提供环回机制的 Channel对象。这两个通道的远端是连接起来的,以便任何写在SinkChannel对象上的数据都能出现在 SourceChannel对象上。下图显示了 Pipe 的类层级。


package java.nio.channels;  
public abstract class Pipe 
{ 
public static Pipe open() throws IOException 
public abstract SourceChannel source(); 
public abstract SinkC hannel sink();  
 
public static abstract class SourceChannel  
extends AbstractSelectableChannel 
implements ReadableByteChannel, ScatteringByteChannel  
public static abstract class SinkChannel  
extends AbstractSelectableChannel 
implements WritableByteChannel, GatheringByteChannel 
}
public abstract class Pipe {

    /**
     * A channel representing the readable end of a {@link Pipe}.  </p>
     *
     * @since 1.4
     */
    public static abstract class SourceChannel
	extends AbstractSelectableChannel
	implements ReadableByteChannel, ScatteringByteChannel
    {
	/**
	 * Constructs a new instance of this class.
	 */
	protected SourceChannel(SelectorProvider provider) {
	    super(provider);
	}

	/**
	 * Returns an operation set identifying this channel's supported
	 * operations.
	 *
	 * <p> Pipe-source channels only support reading, so this method
	 * returns {@link SelectionKey#OP_READ}.  </p>
	 *
	 * @return  The valid-operation set
	 */
	public final int validOps() {
	    return SelectionKey.OP_READ;
	}

    }

    /**
     * A channel representing the writable end of a {@link Pipe}.  </p>
     *
     * @since 1.4
     */
    public static abstract class SinkChannel
	extends AbstractSelectableChannel
	implements WritableByteChannel, GatheringByteChannel
    {
	/**
	 * Initializes a new instance of this class.
	 */
	protected SinkChannel(SelectorProvider provider) {
	    super(provider);
	}

	/**
	 * Returns an operation set identifying this channel's supported
	 * operations.
	 *
	 * <p> Pipe-sink channels only support writing, so this method returns
	 * {@link SelectionKey#OP_WRITE}.  </p>
	 *
	 * @return  The valid-operation set
	 */
	public final int validOps() {
	    return SelectionKey.OP_WRITE;
	}

    }

    /**
     * Initializes a new instance of this class.
     */
    protected Pipe() { }

    /**
     * Returns this pipe's source channel.  </p>
     *
     * @return  This pipe's source channel
     */
    public abstract SourceChannel source();

    /**
     * Returns this pipe's sink channel.  </p>
     *
     * @return  This pipe's sink channel
     */
    public abstract SinkChannel sink();

    /**
     * Opens a pipe.
     *
     * <p> The new pipe is created by invoking the {@link
     * java.nio.channels.spi.SelectorProvider#openPipe openPipe} method of the
     * system-wide default {@link java.nio.channels.spi.SelectorProvider}
     * object.  </p>
     *
     * @return  A new pipe
     *
     * @throws  IOException
     *          If an I/O error occurs
     */
    public static Pipe open() throws IOException {
	return SelectorProvider.provider().openPipe();
    }

}
Pipe 实例是通过调用不带参数的 Pipe.open( ) 工厂方法来创建的。 Pipe 类定义了两个嵌套的通道类来实现管路。这两个类是 Pipe.SourceChannel(管道负责读的一端)和 Pipe.SinkChannel(管道负责写的一端)。这两个通道实例是在 Pipe 对象创建的同时被创建的,可以通过在 Pipe 对象上分别调用 source( ) 和sink( ) 方法来取回。

此时,您可能在想管道到底有什么作用。您不能使用 Pipe 在操作系统级的进程间建立一个类Unix管道(您可以使用 SocketChannel 来建立)。Pipe 的source 通道和 sink 通道提供类似java.io.PipedInputStream 和java.io.PipedOutputStream 所提供的功能,不过它们可以执行全部的通道语义。请注意,SinkChannel和SourceChannel都由AbstractSelectableChannel 引申而来(所以也是从SelectableChannel 引申而来),这意味着 pipe 通道可以同选择器一起使用。

管道可以被用来仅在同一个 Java 虚拟机内部传输数据。虽然有更加有效率的方式来在线程之间传输数据,但是使用管道的好处在于封装性。生产者线程和用户线程都能被写道通用的 Channel API 中。根据给定的通道类型,相同的代码可以被用来写数据到一个文件、socket 或管道。选择器可以被用来检查管道上的数据可用性,如同在 socket 通道上使用那样地简单。这样就可以允许单个用户线程使用一个 Selector来从多个通道有效地收集数据,并可任意结合网络连接或本地工作线程使用。因此,这些对于可伸缩性、冗余度以及可复用性来说无疑都是意义重大的。 
Pipes 的另一个有用之处是可以用来辅助测试。一个单元测试框架可以将某个待测试的类连接到管道的“写”端并检查管道的“读”端出来的数据。它也可以将被测试的类置于通道的“读”端并将受控的测试数据写进其中。两种场景对于回归测试都是很有帮助的。

唯一可保证的是写到SinkChannel中的字节都能按照同样的顺序在 SourceChannel上重现。下例诠释了如何使用管道。工作线程对一个管道进行写操作 。

package com.ronsoft.books.nio.channels;

import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.Pipe;
import java.nio.channels.Channels;
import java.util.Random;

/**
 * Test Pipe objects using a worker thread.
 * 
 * Created April, 2002
 * 
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class PipeTest {
	public static void main(String[] argv) throws Exception {
		// Wrap a channel around stdout
		WritableByteChannel out = Channels.newChannel(System.out);
		// Start worker and get read end of channel
		ReadableByteChannel workerChannel = startWorker(10);
		ByteBuffer buffer = ByteBuffer.allocate(100);
		while (workerChannel.read(buffer) >= 0) {
			buffer.flip();
			out.write(buffer);
			buffer.clear();
		}
	}

	// This method could return a SocketChannel or
	// FileChannel instance just as easily
	private static ReadableByteChannel startWorker(int reps) throws Exception {
		Pipe pipe = Pipe.open();
		Worker worker = new Worker(pipe.sink(), reps);
		worker.start();
		return (pipe.source());

	}

	// -----------------------------------------------------------------
	/**
	 * A worker thread object which writes data down a channel. Note: this
	 * object knows nothing about Pipe, uses only a generic WritableByteChannel.
	 */
	private static class Worker extends Thread {
		WritableByteChannel channel;
		private int reps;

		Worker(WritableByteChannel channel, int reps) {
			this.channel = channel;
			this.reps = reps;
		}

		// Thread execution begins here
		public void run() {
			ByteBuffer buffer = ByteBuffer.allocate(100);
			try {
				for (int i = 0; i < this.reps; i++) {
					doSomeWork(buffer);
					// channel may not take it all at once
					while (channel.write(buffer) > 0) {
						// empty
					}
				}
				this.channel.close();
			} catch (Exception e) {
				// easy way out; this is demo code
				e.printStackTrace();
			}
		}

		private String[] products = { "No good deed goes unpunished",
				"To be, or what?", "No matter where you go, there you are",
				"Just say \"Yo \"", "My karma ran over my dogma" };
		private Random rand = new Random();

		private void doSomeWork(ByteBuffer buffer) {
			int product = rand.nextInt(products.length);
			buffer.clear();
			buffer.put(products[product].getBytes());
			buffer.put(" \r\n".getBytes());
			buffer.flip();
		}
	}
}
结果(不固定):
To be, or what? 
My karma ran over my dogma 
To be, or what? 
My karma ran over my dogma 
No matter where you go, there you are 
No matter where you go, there you are 
My karma ran over my dogma 
No matter where you go, there you are 
No matter where you go, there you are 
Just say "Yo " 


通道工具类:NIO 通道提供了一个全新的类似流的 I/O 隐喻,但是我们所熟悉的字节流以及字符读写器仍然存在并被广泛使用。通道可能最终会改进加入到 java.io类中(这是一个实现细节),但是java.io流所代表的 API 和读写器却不会很快消失(它们也不应该消失)。

一个工具类(java.nio.channels.Channels 的一个稍微重复的名称)定义了几种静态的工厂方法以使通道可以更加容易地同流和读写器互联。表 3-2 对这些方法做了一个汇总。



常规的流仅传输字节,readers 和writers 则作用于字符数据。表 3-2 的前四行描述了用于连接流、通道的方法。因为流和通道都是运行在字节流基础上的,所以这四个方法直接将流封装在通道上,反之亦然。


Readers 和Writers 运行在字符的基础上,在 Java 的世界里字符同字节是完全不同的。将一个通道(仅了解字节)连接到一个 reader 或writer需要一个中间对话来处理字节/字符(byte/char)阻抗失配。为此,表 3-2 的后半部分描述的工厂方法使用了字符集编码器和解码器。

这些方法返回的包封 Channel对象可能会也可能不会实现 InterruptibleChannel 接口,它们也可能不是从 SelectableChannel 引申而来。因此,可能无法将这些包封通道同 java.nio.channels包中定义的其他通道类型交换使用。细节是依赖实现的。如果您的程序依赖这些语义,那么请使用操作器实例测试一下返回的通道对象






















































































































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值