NIO学习之ServerSocketChannel和SocketChannel

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO学习之ServerSocketChannel和SocketChannel相关的知识,希望对你有一定的参考价值。


Socket 通道

(1)新的 socket 通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。

借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失。

所有的 socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中的 AbstractSelectableChannel。

这意味着我们可以用一个 Selector 对象来执行socket 通道的就绪选择(readiness selection)。

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

(3)在我们具体讨论每一种 socket 通道前,您应该了解 socket 和 socket 通道之间的关系。通道是一个连接 I/O 服务导管并提供与该服务交互的方法。就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API,而 java.net 中已经存在的 socket 通道都可以被大多数协议操作重复使用。

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

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

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

AbstractSelectableChannel.java 中实现的 configureBlocking()方法如下:

    /**
     * Adjusts this channel's blocking mode.
     *
     * <p> If the given blocking mode is different from the current blocking
     * mode then this method invokes the @link #implConfigureBlocking
     * implConfigureBlocking method, while holding the appropriate locks, in
     * order to change the mode.  </p>
     */
    public final SelectableChannel configureBlocking(boolean block)
        throws IOException
    
        synchronized (regLock) 
            if (!isOpen())
                throw new ClosedChannelException();
            boolean blocking = !nonBlocking;
            if (block != blocking) 
                if (block && haveValidKeys())
                    throw new IllegalBlockingModeException();
                implConfigureBlocking(block);
                nonBlocking = !block;
            
        
        return this;
    

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

偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个blockingLock( )方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。

下面分别介绍这 3 个通道


ServerSocketChannel

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

由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocketAPI 来根据需要设置其他的 socket 选项。

java.net.ServerSocket 一样,ServerSocketChannel 也有 accept( )方法。一旦创建了一个 ServerSocketChannel 并用对等 socket 绑定了它,然后您就可以在其中一个上调用 accept()

如果您选择在 ServerSocket 上调用 accept( )方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。

如果您选择在 ServerSocketChannel 上调用 accept( )方法则会返回SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。

换句话说:

ServerSocketChannelaccept()方法会返回 SocketChannel 类型对象,SocketChannel 可以在非阻塞模式下运行。

其它 Socketaccept()方法会阻塞返回一个 Socket 对象。

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

可选择性也因此得到实现。我们可以使用一个选择器实例来注册 ServerSocketChannel 对象以实现新连接到达时自动通知的功能。


代码演示

    //测试ServerSocketChannel的使用
    public static void serverSocketChannel()
    
        SocketChannel socketChannel=null;
        try 
            //创建缓冲区,初始化缓冲区里面的数据
            ByteBuffer byteBuffer=ByteBuffer.wrap("大忽悠".getBytes(StandardCharsets.UTF_8));
            //创建serverSocketChannel对象
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //绑定对应的端口号---先将一个ServerSocket关联到当前通道,然后给这个serverSocket对象绑定一个端口号
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            //设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //等待接收客户端的连接
            while(true)
            
                //因为设置了非阻塞模式,没有客户端连接的时候,会立即返回null
                 socketChannel = serverSocketChannel.accept();
                if(socketChannel==null)
                
                    System.out.println("等待客户端连接中....");
                    //睡2秒,等待客户端连接
                    Thread.sleep(2000);
                
                else
                
                    //获取和当前客户端建立连接的socket对象,并且这个socket对象和当前的通道关联
                    Socket socket = socketChannel.socket();
                    System.out.println("当前客户端["+socket.getRemoteSocketAddress()+"]");
                    //position指针置0,limit指针不变
                    byteBuffer.rewind();
                    //从缓冲区中读取数据,写入通道中
                    socketChannel.write(byteBuffer);
                
            
         catch (IOException e) 
            e.printStackTrace();
         catch (InterruptedException e) 
            e.printStackTrace();
        finally 
            //关闭连接
            try 
                socketChannel.close();
             catch (IOException e) 
                e.printStackTrace();
            
        
    

ServerSocketChannel相关函数讲解

(1)打开 ServerSocketChannel

通过调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel.

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

(2)关闭 ServerSocketChannel

通过调用 ServerSocketChannel.close() 方法来关闭 ServerSocketChannel.

serverSocketChannel.close();

(3)监听新的连接

通过 ServerSocketChannel.accept() 方法监听新进的连接。当accept()方法返回时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。通常不会仅仅只监听一个连接,在 while 循环中调用 accept()方法. 如下面的例子:

非阻塞模式

ServerSocketChannel 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。 因此,需要检查返回的SocketChannel 是否是 null.如:


SocketChannel

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。

SocketChannel 是一种面向流连接sockets 套接字的可选择通道。从这里可以看出:

  • SocketChannel 是用来连接 Socket 套接字,即通过一个通道与之前的BIO中的Socket对象相关联
  • SocketChannel 主要用途用来处理网络 I/O 的通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用的

SocketChannel 特征

  • (1)对于已经存在的 socket 不能创建 SocketChannel
  • (2)SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使 用 connect接口连接到指定地址
  • (3)未进行连接的 SocketChannle 执行 I/O 操作时,会抛出 NotYetConnectedException
  • (4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式
  • (5)SocketChannel 支持异步关闭。如果 SocketChannel 在一个线程上 read 阻塞,另 一个线程对该SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1 表示没有 读取任何数据;如果SocketChannel 在一个线程上 write 阻塞,另一个线程对该 SocketChannel 调用shutdownWrite,则写阻塞的线程将抛出 AsynchronousCloseException
  • (6)SocketChannel 支持设定参数
SO_SNDBUF 套接字发送缓冲区大小
SO_RCVBUF 套接字接收缓冲区大小
SO_KEEPALIVE 保活连接
O_REUSEADDR 复用地址
SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)
TCP_NODELAY 禁用 Nagle 算法

SocketChannel 的使用

(1)创建 SocketChannel

方式一:

SocketChannel socketChannel = SocketChannel.open(new 
InetSocketAddress("www.baidu.com", 80));

方式二:

SocketChannel socketChanne2 = SocketChannel.open();
socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));

直接使用有参 open api 或者使用无参 open api,但是在无参 open 只是创建了一个SocketChannel 对象,并没有进行实质的 tcp 连接。

(2)连接校验

socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态
socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接
socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行
连接
socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel
是否已经完成连接

(3)读写模式

前面提到 SocketChannel 支持阻塞和非阻塞两种模式:

socketChannel.configureBlocking(false);

通过以上方法设置 SocketChannel 的读写模式。false 表示非阻塞,true 表示阻塞。

(4)读写

SocketChannel socketChannel = SocketChannel.open(
 new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");

以上为阻塞式读,当执行到 read 出,线程将阻塞,控制台将无法打印 read over

SocketChannel socketChannel = SocketChannel.open(
 new InetSocketAddress("www.baidu.com", 80));
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");

以上为非阻塞读,控制台将打印 read over
读写都是面向缓冲区,这个读写方式与前文中的 FileChannel 相同

(5)设置和获取参数

socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, 
Boolean.TRUE)
 .setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);

通过 setOptions 方法可以设置 socket 套接字的相关参数

socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);

可以通过 getOption 获取相关参数的值。如默认的接收缓冲区大小是 8192byte。

SocketChannel 还支持多路复用,但是多路复用在后续内容中会介绍到。


ServerSocket,Socket与ServerSocketChannel,SocketChannel区别

  • (1)SocketServerSocket 是一对 他们是java.net下面实现socket通信的类
  • (2) SocketChannelServerSocketChannel是一对 他们是java.nio下面实现通信的类支持异步通信
  • (3)服务器必须先建立ServerSocket或者ServerSocketChannel 来等待客户端的连接
  • (4)客户端必须建立相对应的Socket或者SocketChannel来与服务器建立连接
  • (5)服务器接受到客户端的连接受,再生成一个Socket或者SocketChannel与此客户端通信

以上是关于NIO学习之ServerSocketChannel和SocketChannel的主要内容,如果未能解决你的问题,请参考以下文章

ServerSocketChannel和SocketChannel

NIO学习之NIO概述和FileChannel详解

nio学习之Selector选择器

NIO学习之ByteBuffer理解篇

NIO学习之ByteBuffer理解篇

26 Java学习之NIO和IO得比较