借助NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现在都有getChannel()方法。
如果您用传统方式(直接实例化)创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel( )方法将总是返回null。
Socket通道将与通信协议相关的操作委托给相应的socket对象。 socket的方法看起来好像在通道类中重复了一遍, 但实际上通道类上的方法会有一些新的或者不同的行为。
非阻塞I/O是许多复杂的、高性能的程序构建的基础。要把一个socket通道置于非阻塞模式,我们要依靠所有socket通道类的公有超级类:SelectableChannel。
socketChannel.configureBlocking(false) // nonblocking 就是这么简单就配置了非阻塞
对于确保在执行代码的关键部分时socket通道的阻塞模式不会改变以及在不影响其他线程的前提下暂时改变阻塞模式来说,这个方法都是非常方便的。
lockobj = serverChannel.blockingLock();synchronize(lockobj){ prevState = serverChannel.isBlocking(); serverChannel.configureBlocking(false); socket = serverChannel.accept(); serverChannel.configureBlocking(prevState);}ServerSocketChannel是一个基于通道的socket监听器。它同我们所熟悉的java.net.ServerSocket执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
// 静态open方法创建ServerSocketChannel对象
ServerSocketChannel ssc = ServerSocketChannel.open();// ServerSocketChannel没有bind方法,只能获取socket对象进行绑定ServerSocket serverSocket = ssc.socket( );// Listen on port 1234serverSocket.bind (new InetSocketAddress (1234));只有在ServerSocketChannel上调用accept( )方法才可能使返回的对象能够在非阻塞模式下运行。
如果以非阻塞模式被调用,当没有传入连接在等待时, ServerSocketChannel.accept()会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册一个ServerSocketChannel对象以实现新连接到达时自动通知的功能。
新创建的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));在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值。
Socket通道是线程安全的。并发访问时无需特别措施来保护发起访问的多个线程,不过任何时候都只有一个读操作和一个写操作在进行中。请记住,sockets是面向流的而非包导向的。它们可以保证发送的字节会按照顺序到达但无法承诺维持字节分组。某个发送器可能给一个socket写入了20个字节而接收器调用read()方法时却只收到了其中的3个字节。剩下的17个字节还是传输中。由于这个原因,让多个不配合的线程共享某个流socket的同一侧绝非一个好的设计选择。
connect( )和finishConnect( )方法是互相同步的,并且只要其中一个操作正在进行,任何读或写的方法调用都会阻塞,即使是在非阻塞模式下。如果此情形下您有疑问或不能承受一个读或写操作在某个通道上阻塞,请用isConnected()方法测试一下连接状态。
正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)
DatagramChannel对象既可以充当服务器(监听者)也可以充当客户端(发送者)。如果您希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。绑定DatagramChannel同绑定一个常规的DatagramSocket没什么区别,都是委托对等socket对象上的API实现的:
DatagramChannel channel = DatagramChannel.open( );DatagramSocket socket = channel.socket( );socket.bind (new InetSocketAddress (portNumber));一个未绑定的DatagramChannel仍能接收数据包。当一个底层socket被创建时,一个动态生成的端口号就会分配给它。绑定行为要求通道关联的端口被设置为一个特定的值(此过程可能涉及安全检查或其他验证)。不论通道是否绑定,所有发送的包都含有DatagramChannel的源地址(带端口号)。未绑定的DatagramChannel可以接收发送给它的端口的包,通常是来回应该通道之前发出的一个包。已绑定的通道接收发送给它们所绑定的熟知端口(wellknownport)的包。数据的实际发送或接收是通过send()和receive()方法来实现的。
receive()方法将下次将传入的数据报的数据净荷复制到预备好的ByteBuffer中并返回一个SocketAddress对象以指出数据来源。如果通道处于阻塞模式,receive()可能无限期地休眠直到有包到达。如果是非阻塞模式,当没有可接收的包时则会返回null。如果包内的数据超出缓冲区能承受的范围,多出的数据都会被悄悄地丢弃。
调用send( )会发送给定ByteBuffer对象的内容到给定SocketAddress对象所描述的目的地址和端口,内容范围为从当前position开始到末尾处结束。如果DatagramChannel对象处于阻塞模式,调用线程可能会休眠直到数据报被加入传输队列。如果通道是非阻塞的,返回值要么是字节缓冲区的字节数,要么是“0”。发送数据报是一个全有或全无(all-or-nothing)的行为。如果传输队列没有足够空间来承载整个数据报,那么什么内容都不会被发送。
数据报协议的不可靠性是固有的,它们不对数据传输做保证。 send()方法返回的非零值并不表示数据报到达了目的地,仅代表数据报被成功加到本地网络层的传输队列。此外,传输过程中的协议可能将数据报分解成碎片。例如,以太网不能传输超过 1,500 个字节左右的包。如果您的数据报比较大,那么就会存在被分解成碎片的风险,成倍地增加了传输过程中包丢失的几率。被分解的数据报在目的地会被重新组合起来,接收者将看不到碎片。但是,如果有一个碎片不能按时到达,那么整个数据报将被丢弃。