NIO - Socket 通道

来源:互联网 发布:傲剑绿色版数据 编辑:程序博客网 时间:2024/04/20 04:46

http://zachary-guo.iteye.com/blog/1563417

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

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

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

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

Java代码  收藏代码
  1. public abstract class SelectableChannel extends AbstractChannel implements Channel {  
  2.     // This is a partial API listing  
  3.     public abstract void configureBlocking (boolean block) throws IOException;  
  4.     public abstract boolean isBlocking();  
  5.     public abstract Object blockingLock();  
  6. }  

        设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking() 方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。真的,就这么简单!调用isBlocking() 方法来判断某个 socket 通道当前处于哪种模式: 
Java代码  收藏代码
  1. SocketChannel sc = SocketChannel.open();  
  2. sc.configureBlocking (false); // nonblocking  
  3. ...  
  4. if ( ! sc.isBlocking( )) {  
  5.     doSomething (cs);  
  6. }  

        偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个 blockingLock()方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。使用 blockingLock() 的示例: 
Java代码  收藏代码
  1. Socket socket = null;  
  2. Object lockObj = serverChannel.blockingLock();  
  3. // have a handle to the lock object, but haven't locked it yet may block here until lock is acquired  
  4. synchronize (lockObj) {  
  5.     // This thread now owns the lock; mode can't be changed  
  6.     boolean prevState = serverChannel.isBlocking();  
  7.     serverChannel.configureBlocking(false);  
  8.     socket = serverChannel.accept();  
  9.     serverChannel.configureBlocking(prevState);  
  10. }  
  11. // lock is now released, mode is allowed to change  
  12. if (socket != null) {  
  13.     doSomethingWithTheSocket(socket);  
  14. }  


    ◇ ServerSocketChannel 
        让我们从最简单的 ServerSocketChannel 来开始对 socket 通道类的讨论。以下是 ServerSocketChannel 的完整API: 
Java代码  收藏代码
  1. public abstract class ServerSocketChannel extends AbstractSelectableChannel {  
  2.     public static ServerSocketChannel open() throws IOException  
  3.     public abstract ServerSocket socket();  
  4.     public abstract ServerSocket accept() throws IOException;  
  5.     public final int validOps();  
  6. }  

        ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。 

        用静态的 open() 工厂方法创建一个新的 ServerSocketChannel 对象,将会返回同一个未绑定的 java.net.ServerSocket 关联的通道。该对等 ServerSocket 可以通过在返回的 ServerSocketChannel 上调用 socket() 方法来获取。作为 ServerSocketChannel 的对等体被创建的 ServerSocket 对象依赖通道实现。这些 socket 关联的 SocketImpl 能识别通道。通道不能被封装在随意的 socket 对象外面。由于 ServerSocketChannel 没有 bind() 方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。使用对等 ServerSocket 的 API 来根据需要设置其他的 socket 选项: 
Java代码  收藏代码
  1. ServerSocketChannel ssc = ServerSocketChannel.open();  
  2. // 取到 ServerSocketChannel 对等的 serverSocket 对象  
  3. ServerSocket serverSocket = ssc.socket();  
  4. // Listen on port 1234  
  5. serverSocket.bind (new InetSocketAddress (1234));  

        ServerSocketChannel 和 serverSocket 均有 accept() 方法,你可以在其中一个上调用 accept()。如果你选择在 ServerSocket 上调用 accept() 方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。如果你选择在 ServerSocketChannel 上调用 accept() 方法则会返回 SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。 

    ◇ SocketChannel 
        下面开始学习 SocketChannel,它是使用最多的 socket 通道类: 
Java代码  收藏代码
  1. public abstract class SocketChannel extends AbstractSelectableChannel  
  2.     implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {  
  3.   
  4.     // This is a partial API listing  
  5.     public static SocketChannel open() throws IOException  
  6.     public static SocketChannel open (InetSocketAddress remote) throws IOException  
  7.     public abstract Socket socket();  
  8.     public abstract boolean connect (SocketAddress remote) throws IOException;  
  9.     public abstract boolean isConnectionPending();  
  10.     public abstract boolean finishConnect() throws IOException;  
  11.     public abstract boolean isConnected();  
  12.     public final int validOps();  
  13. }  

        每个 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 当前是否已连接。创建 SocketChannel 时创建连接: 
Java代码  收藏代码
  1. SocketChannel socketChannel =   
  2.     SocketChannel.open(new InetSocketAddress ("somehost", somePort));  
  3.   
  4. 等价于:  
  5.   
  6. SocketChannel socketChannel = SocketChannel.open();  
  7. 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 值。finishConnect() 示例: 
Java代码  收藏代码
  1. InetSocketAddress addr = new InetSocketAddress (host, port);  
  2. SocketChannel sc = SocketChannel.open();  
  3. sc.configureBlocking(false);  
  4. sc.connect(addr);  
  5. while ( ! sc.finishConnect( )) {  
  6.     doSomethingElse();  
  7. }  
  8. doSomethingWithChannel(sc);  
  9. sc.close();  

        上面的代码中,我们通过 while 轮询并在连接进行过程中判断通道所处的状态,后续我们将了解到如何使用选择器来避免进行轮询并在异步连接建立之后收到通知。如果尝试异步连接失败,那么下次调用 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): 
Java代码  收藏代码
  1. public abstract class DatagramChannel extends AbstractSelectableChannel  
  2.     implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {  
  3.   
  4.     // This is a partial API listing  
  5.     public static DatagramChannel open() throws IOException;  
  6.     public abstract DatagramSocket socket();  
  7.     public abstract DatagramChannel connect(SocketAddress remote) throws IOException;  
  8.     public abstract boolean isConnected();  
  9.     public abstract DatagramChannel disconnect() throws IOException;  
  10.     public abstract SocketAddress receive(ByteBuffer dst)  
  11. throws IOException;  
  12.     public abstract int send(ByteBuffer src, SocketAddress target);  
  13.     public abstract int read(ByteBuffer dst) throws IOException;  
  14.     public abstract long read(ByteBuffer [] dsts) throws IOException;  
  15.     public abstract long read(ByteBuffer [] dsts, int offset, int length) throws IOException;  
  16.     public abstract int write(ByteBuffer src) throws IOException;  
  17.     public abstract long write(ByteBuffer[] srcs) throws IOException;  
  18.     public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;  
  19. }  

        创建 DatagramChannel 的模式和创建其他 socket 通道是一样的:调用静态的 open() 方法来创建一个新实例。新 DatagramChannel 会有一个可以通过调用 socket() 方法获取的对等 DatagramSocket 对象。DatagramChannel 对象既可以充当服务器(监听者)也可以充当客户端(发送者)。如果你希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。绑定 DatagramChannel 同绑定一个常规的 DatagramSocket 没什么区别,都是委托对等 socket 对象上的API实现的: 
Java代码  收藏代码
  1. DatagramChannel channel = DatagramChannel.open();  
  2. DatagramSocket socket = channel.socket();  
  3. socket.bind(new InetSocketAddress (portNumber));  

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

        一个未绑定的 DatagramChannel 仍能接收数据包。当一个底层 socket 被创建时,一个动态生成的端口号就会分配给它,绑定行为要求通道关联的端口被设置为一个特定的值(此过程可能涉及安全检查或其他验证)。不论通道是否绑定,所有发送的包都含有 DatagramChannel 的源地址(带端口号)。未绑定的 DatagramChannel 可以接收发送给它的端口的包,通常是来回应该通道之前发出的一个包。已绑定的通道接收发送给它们所绑定的熟知端口(wellknown port)的包。数据的实际发送或接收是通过 send() 和 receive() 方法来实现的: 
Java代码  收藏代码
  1. public abstract class DatagramChannel extends AbstractSelectableChannel  
  2.     implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {  
  3.   
  4.     // This is a partial API listing  
  5.     public abstract SocketAddress receive(ByteBuffer dst) throws IOException;  
  6.     public abstract int send(ByteBuffer src, SocketAddress target);  
  7. }  

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

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

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

        我们可以通过调用带 SocketAddress 对象的 connect() 方法来连接一个 DatagramChannel,该 SocketAddress 对象描述了 DatagramChannel 远程对等体的地址。 

        不同于流 socket,数据报 socket 的无状态性质不需要同远程系统进行对话来建立连接状态。没有实际的连接,只有用来指定允许的远程地址的本地状态信息。由于此原因,DatagramChannel 上也就没有单独的 finishConnect() 方法。我们可以使用 isConnected() 方法来测试一个数据报通道的连接状态。 

        不同于 SocketChannel(必须连接了才有用并且只能连接一次),DatagramChannel 对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。当一个 DatagramChannel 处于已连接状态时,发送数据将不用提供目的地址而且接收时的源地址也是已知的。这意味着 DatagramChannel 已连接时可以使用常规的 read() 和 write() 方法,包括 scatter/gather 形式的读写来组合或分拆包的数据: 
Java代码  收藏代码
  1. public abstract class DatagramChannel extends AbstractSelectableChannel  
  2.     implements ByteChannel, ScatteringByteChannel, GatheringByteChannel {  
  3.   
  4.     // This is a partial API listing  
  5.     public abstract int read(ByteBuffer dst) throws IOException;  
  6.     public abstract long read(ByteBuffer [] dsts) throws IOException;  
  7.     public abstract long read(ByteBuffer [] dsts, int offset, int length) throws IOException;  
  8.     public abstract int write(ByteBuffer src) throws IOException;  
  9.     public abstract long write(ByteBuffer[] srcs) throws IOException;  
  10.     public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;  
  11. }  

        read() 方法返回读取字节的数量,如果通道处于非阻塞模式的话这个返回值可能是“0”。write() 方法的返回值同 send() 方法一致:要么返回你的缓冲区中的字节数量,要么返回“0”(如果由于通道处于非阻塞模式而导致数据报不能被发送)。当通道不是已连接状态时调用 read() 或 write() 方法,都将产生 NotYetConnectedException 异常。

0 0
原创粉丝点击