天天看点

SocketChannelSocketChannel

SocketChannel

(1)新的Socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。再也没有必要为每个socket连接使用一个线程,也避免了管理大量线程所需的上下文切换的开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了,并且只有很少甚至可能没有性能损失。所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择。

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

(3)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通道当前处于那种模式。

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

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

ServerSocketChannel

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

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

同java.net.ServerSocket一样,ServerSocketChannel也有accept()方法。一旦创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept()。如果您选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。如果您选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。

换句话说,ServerSocketChannel的accept()方法会返回SocketChannel类型对象,SocketChannel可以在非阻塞模式下运行。

其他Socket的accept()方法会阻塞返回一个socket对象。如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册ServerSocketChannel对象以实现新连接到达时自动通知的功能。

以下代码演示了如何使用一个非阻塞的accept()方法:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class FileChannelAccept {

    public static void main(String[] argv) throws IOException, InterruptedException {
        int port = 8888;
        ByteBuffer buf = ByteBuffer.wrap("Hello java nio.".getBytes());

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ServerSocket socket = ssc.socket();
        socket.bind(new InetSocketAddress(port));

        //设置非阻塞模式
        ssc.configureBlocking(false);

        while (true) {
            System.out.println("waiting for connections");
            SocketChannel sc = ssc.accept();
            if (sc == null) { //没有连接传入
                System.out.println("null");
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from: "+sc.socket().getRemoteSocketAddress());
                buf.rewind(); //指针0
                sc.write(buf);
                sc.close();
            }
        }

    }

}

           

SocketChannel

SocketChannel介绍

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

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

  • SocketChannel是用来连接Socket套接字。
  • SocketChannel主要用途用来处理网络I/O的通道。
  • SocketChannel是基于TCP连接传输。
  • SocketChannel实现了可选择通道,可以被多路复用的。

SocketChannel特征:

(1)对于已经存在的socket不能创建SocketChannel。

(2)SocketChannel中提供的open接口创建的Channel并没有网络连接,需要使用connect接口连接到指定地址。

(3)未进行连接的SocketChannel执行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 保活链接
  • SO_REUSEADDR 复用地址
  • SO_LINGER 有数据传输时延缓关闭Channel(只有在非阻塞模式下有用)
  • TCP_NODELAY 禁用Nagle算法
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class SocketChannelDemo {

    public static void main(String[] args) throws IOException {

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

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

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

        //读写模式
        socketChannel.configureBlocking(false);

        //设置和获取参数
        socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE);
        

        //读写
        ByteBuffer buf = ByteBuffer.allocate(48);
        socketChannel.read(buf);
        socketChannel.close();
        System.out.println("read over!");
    }

}

           

DatagramChannel

正如SocketChannel对应Socket,ServerSocketChannel对应ServerSocket,每一个DatagramChannel对象也有一个关联的DatagramSocket对象。正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)。DatagramChannel是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的地址及不依赖其他数据报的数据负载。与面向流的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都包含有关于它来自何处的信息(源地址)。

基本使用:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class DatagramChannelDemo {

    public static void main(String[] args) throws IOException {
        //打开DatagramChannel
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.socket().bind(new InetSocketAddress(8888));
        //接收数据
        ByteBuffer buf = ByteBuffer.allocate(64);
        buf.clear();
        SocketAddress socketAddress = datagramChannel.receive(buf);

        //发送数据
        ByteBuffer buffer = ByteBuffer.wrap("data...".getBytes());
        datagramChannel.send(buffer,new InetSocketAddress("127.0.0.1",8888));

        //连接
        datagramChannel.connect(new InetSocketAddress("127.0.0.1",8888));
        int readSize = datagramChannel.read(buffer);
        datagramChannel.write(buffer);
    }

}

           

示例:

发送端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;

public class DatagramChannelDemo1 {

    //发送
    public static void main(String[] args) throws IOException, InterruptedException {
        //打开DatagramChannel
        DatagramChannel datagramChannel = DatagramChannel.open();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        //发送
        while (true) {
            ByteBuffer buffer = ByteBuffer.wrap("send data".getBytes(StandardCharsets.UTF_8));
            datagramChannel.send(buffer,address);
            System.out.println("finished!");
            Thread.sleep(1000);
        }
    }

}

           

接收端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;

public class DatagramChannelDemo2 {

    //接收
    public static void main(String[] args) throws IOException {
        //打开
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.bind(new InetSocketAddress("127.0.0.1",8888));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            buffer.clear();
            SocketAddress receive = datagramChannel.receive(buffer);
            buffer.flip();
            System.out.println(receive.toString());
            System.out.println(Charset.forName("UTF-8").decode(buffer));
        }
    }

}

           

Scatter/Gather

Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作。

分散(scatter):从Channel中读取是指读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散scatter”到多个buffer中。

聚集(gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel将多个Buffer中的数据“聚集(gather)”后发送到Channel。

scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。

Scattering Reads

Scattering Reads是指数据从一个channel读取到多个buffer中。如图:

SocketChannelSocketChannel
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.read(bufferArray);
           

注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧跟着向另一个buffer中写。

Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充。Scattering Reads才能正常工作。

Gathering Writes

Gathering Writes是指数据从多个buffer写入到同一个channel。如图:

SocketChannelSocketChannel
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.write(bufferArray);
           

buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。