当前位置: 首页 > news >正文

NIO Selector选择器解析

选择器以及注册

        选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系,是监控和被监控的关系。

        选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件,包括读写就绪的IO操作事件。

        在NIO编程中,一般是一个单线程处理一个选择器,一个选择器可以监控很多通道。所以,通过选择器,一个单线程可以处理数百、数千、数万、甚至更多的通道。在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上下文切换的开销。

        通道和选择器之间的关联,通过register(注册)的方式完成。调用通道的Channel.register(Selector sel,int ops)方法,可以将通道实例注册到一个选择器中。register方法有两个参数:第一个参数,指定通道注册到的选择器实例;第二个参数,指定选择器要监控的IO事件类型。

可供选择器监控的通道IO事件类型,包括以下四种:

  1. 可读:SelectionKey.OP_READ
  2. 可写:SelectionKey.OP_WRITE
  3. 连接:SelectionKey.OP_CONNECT
  4. 接收:SelectionKey.OP_ACCEPT

        以上的事件类型常量定义在SelectionKey类中。如果选择器要监控通道的多种事件,可以用“按位或”运算符来实现。例如,同时监控可读和可写IO事件:

//监控通道的多种事件,用“按位或”运算符来实现
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

注:IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件。比方说:

  1. 某个SocketChannel传输通道,如果完成了和对端的三次握手过程,则会发生“连接就绪”(OP_CONNECT)的事件。
  2. 某个ServerSocketChannel服务器连接监听通道,在监听到一个新连接的到来时,则会发生“接收就绪”(OP_ACCEPT)的事件。
  3. 一个SocketChannel通道有数据可读,则会发生“读就绪”(OP_READ)事件;
  4. 一个等待写入数据的SocketChannel通道,会发生写就绪(OP_WRITE)事件。

SelectableChannel可选择通道

        并不是所有的通道,都是可以被选择器监控或选择的。比方说,FileChannel文件通道就不能被选择器复用。判断一个通道能否被选择器监控或选择,有一个前提:判断它是否继承了抽象类SelectableChannel(可选择通道),如果是则可以被选择,否则不能。

        简单地说,一条通道若能被选择,必须继承SelectableChannel类。

        SelectableChannel类它提供了实现通道的可选择性所需要的公共方法。Java NIO中所有网络链接Socket套接字通道,都继承了SelectableChannel类,都是可选择的。而FileChannel文件通道,并没有继承SelectableChannel,因此不是可选择通道。

SelectionKey选择键

        通道和选择器的监控关系,本质是一种多对一的关联关系。这种关联关系,非常类似于数据库两个主表之间的关联关系,通道(Channel)和选择器(Selector)类似于数据库的主表,而选择键(SelectionKey)就类似于关联表,具体如下图所示:

         Selector并不直接去管理Channel,而是直接管理SelectionKey,通过SelectionKey与Channel发生关系。而Java NIO源码中规定了,一个Channel最多能向Selector注册一次,注册之后就形成了唯一的SelectionKey,然后被Selector管理起来。

        Selector有一个核心成员keys,专门用于管理注册上来的SelectionKey,Channel注册到Selector后所创建的那一个唯一的SelectionKey,添加在这个keys成员中,这是一个HashSet类型的集合。除了成员keys之外,Selector还有一个核心成员selectedKeys,用于存放已经发生了IO事件的SelectionKey。

        两核心成员keys、selectedKeys定义在Selector的抽象实现类SelectorImpl中,代码如下:

public abstract class SelectorImpl extends AbstractSelector {
    /**发生了  IO 事件的  Channel 的选择键**/
    protected Set<SelectionKey> selectedKeys = new HashSet();
    /**Channel 注册之后的选择键,一个  channel 在一个  selector 上有一个唯一的 Key**/ 
    protected HashSet<SelectionKey> keys = new HashSet();
    …… 
}

        SelectionKey是IO事件的记录者(或存储者),SelectionKey  有两个核心成员,存储着自己关联的Channel上的感兴趣IO事件和已经发生的IO事件。这两个核心成员定义在实现类SelectionKeyImpl中,代码如下:

public class SelectionKeyImpl extends AbstractSelectionKey {
    final SelChImpl channel; //关联的channel
    public final SelectorImpl selector; //关联的选择键
    private int index;
    private volatile int interestOps;//关联的Channel上的感兴趣IO事件
    private int readyOps;//已经发生的IO事件, 来自关联的Channel
    .....
}

        Channel通道上可以发生多种IO事件,比如说读就绪事件、写就绪事件、新连接就绪事件,但是SelectionKey记录事件的成员却是一个整数类型。这样问题就来了,一个整数如何记录多个事件呢?答案是,通过比特位来完成的。具体的IO事件所占用的哪一个比特位, 通过常量的方式定义在SelectionKey中,如下:

    /**
     * Operation-set bit for read operations.
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_READ</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding channel is ready for reading, has reached
     * end-of-stream, has been remotely shut down for further reading, or has
     * an error pending, then it will add <tt>OP_READ</tt> to the key's
     * ready-operation set and add the key to its selected-key&nbsp;set.  </p>
     */
    public static final int OP_READ = 1 << 0;

    /**
     * Operation-set bit for write operations.
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_WRITE</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding channel is ready for writing, has been
     * remotely shut down for further writing, or has an error pending, then it
     * will add <tt>OP_WRITE</tt> to the key's ready set and add the key to its
     * selected-key&nbsp;set.  </p>
     */
    public static final int OP_WRITE = 1 << 2;

    /**
     * Operation-set bit for socket-connect operations.
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_CONNECT</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding socket channel is ready to complete its
     * connection sequence, or has an error pending, then it will add
     * <tt>OP_CONNECT</tt> to the key's ready set and add the key to its
     * selected-key&nbsp;set.  </p>
     */
    public static final int OP_CONNECT = 1 << 3;

    /**
     * Operation-set bit for socket-accept operations.
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_ACCEPT</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding server-socket channel is ready to accept
     * another connection, or has an error pending, then it will add
     * <tt>OP_ACCEPT</tt> to the key's ready set and add the key to its
     * selected-key&nbsp;set.  </p>
     */
    public static final int OP_ACCEPT = 1 << 4;

        通过SelectionKey的interestOps成员上相应的比特位,可以设置、查询关联的Channel所感兴趣的IO事件;通过SelectionKey的readyOps上相应的比特位,可以查询关联Channel所已经发生的IO事件。对于interestOps成员上的比特位,应用程序是可以设置的;但是对于readyOps上的比特位,应用程序只能查询,不能设置。为啥呢?readyOps上的比特位代表了已经发生的IO事件,是由选择器Selector去设置的,应用程序只能获取。

        通道和选择器的监控关系注册成功后,Selector就可以查询就绪事件。具体的查询操作,是通过调用选择器Selector的select( )系列方法来完成。通过select系列方法,选择器会通过JNI,去进行底层操作系统的系统调用(比如select/epoll),可以不断地查询通道中所发生操作的就绪状态(或者IO事件),并且把这些发生了底层IO事件,转换成Java NIO中的IO事件,记录在的通道关联的SelectionKey的readyOps上。除此之外,发生了IO事件的选择键,还会记录在Selector内部selectedKeys集合中。

        简单来说,一旦在通道中发生了某些IO事件(就绪状态达成),这个事件就被记录在SelectionKey的readyOps上,并且这个SelectionKey被记录在Selector内部的selectedKeys集合中。当然,这里有两个前提:

  1. 通道必须在Selector注册过;
  2. 所发生的事件必须是SelectionKey上interestOps成员记录的事件。

        在实际编程时,选择键的功能是很强大的。通过SelectionKey选择键,不仅仅可以获得通道的IO事件类型,比方说SelectionKey.OP_READ;还可以获得发生IO事件所在的通道;另外,也可以获得Selector选择器实例。所以,这个一个非常重要的中间类或者胶水类。

选择器使用流程

使用选择器,主要有以下三步:

  1. 获取选择器实例;
  2. 将通道注册到选择器中;
  3. 轮询感兴趣的IO就绪事件(选择键集合)。

第一步:获取选择器实例。选择器实例是通过调用静态工厂方法open()来获取的,具 
体如下:

//调用静态工厂方法  open()来获取  Selector 实例 
Selector selector = Selector.open();

        Selector选择器的类方法open( )的内部,是向选择器SPI(SelectorProvider)发出请求,通过默认的SelectorProvider(选择器提供者)对象,获取一个新的选择器实例。Java中SPI全称为(Service Provider Interface,服务提供者接口),是JDK的一种可以扩展的服务提供和发现机制。Java通过SPI的方式,提供选择器的默认实现版本。也就是说,其他的服务提供商可以通过SPI的方式,提供定制化版本的选择器的动态替换或者扩展。 

        第二步:将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应 
的选择器上,简单的示例代码如下:

// 2.获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false); 
// 4.绑定连接
serverSocketChannel.bind(new InetSocketAddress(18899));
// 5.将通道注册到选择器上,并制定监听事件为:“接收连接”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

     上面通过调用通道的register(…)方法,将ServerSocketChannel通道注册到了一个选择器 
上。当然,在注册之前,首先需要准备好通道。

注:

  1. 注册到选择器的通道,必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常。这意味着,FileChannel文件通道不能与选择器一起使用,因为FileChannel文件通道只有阻塞模式,不能切换到非阻塞模式;而Socket套接字相 关的所有通道都可以。
  2. 一个通道,并不一定要支持所有的四种IO事件。例如服务器监听通ServerSocketChannel,仅仅支持Accept(接收到新连接)IO事件;而传输通道SocketChannel则不同,该类型通道不支持Accept类型的IO事件。
  3. 可以在注册之前,可以通过通道的validOps()方法,来获取该通道所有支持的IO事件集合。

        第三步:选出感兴趣的IO就绪事件(选择键集合)。通过Selector选择器的select()方 
法,选出已经注册的、已经就绪的IO事件,并且保存到SelectionKey选择键集合中。 
SelectionKey集合保存在选择器实例内部,其元素为SelectionKey类型实例。调用选择器的 
selectedKeys()方法,可以取得选择键集合。

        接下来,需要迭代集合的每一个选择键,根据具体IO事件类型,执行对应的业务操 
作。大致的处理流程如下:

//轮询,选择感兴趣的  IO 就绪事件(选择键集合) 
while (selector.select() > 0) {
    Set selectedKeys = selector.selectedKeys(); 
    Iterator keyIterator = selectedKeys.iterator(); 
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next(); 
        //根据具体的  IO 事件类型,执行对应的业务操作
        if(key.isAcceptable()) {
        // IO 事件:ServerSocketChannel 服务器监听通道有新连接 
        } else if (key.isConnectable()) {
        // IO 事件:传输通道连接成功
        } else if (key.isReadable()) { 
        // IO 事件:传输通道可读
        } else if (key.isWritable()) { 
        // IO 事件:传输通道可写
        }
        //处理完成后,移除选择键 
        keyIterator.remove();
    } 
}

        处理完成后,需要将选择键从这个SelectionKey集合中移除,防止下一次循环的时候,被重复的处理。SelectionKey集合不能添加元素,如果试图向SelectionKey选择键集合中添加元素,则将抛出java.lang.UnsupportedOperationException异常。

用于选择就绪的IO事件的select()方法,有多个重载的实现版本,具体如下:

  1. select():阻塞调用,一直到至少有一个通道发生了注册的IO事件。
  2. select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。
  3. selectNow():非阻塞,不管有没有IO事件,都会立刻返回。

        select()方法的返回值的是整数类型(int),表示发生了IO事件的数量。更准确地说, 
是从上一次select到这一次select之间,有多少通道发生了IO事件,更加准确地说,是指发 
生了选择器感兴趣(注册过)的IO事件数。

使用NIO实现Discard服务器的实践案例

        Discard服务器的功能很简单:仅仅读取客户端通道的输入数据,读取完成后直接关闭 
客户端通道;并且读取到的数据直接抛弃掉(Discard)。Discard服务器足够简单明了,作 
为第一个学习NIO的通信实例,较有参考价值。

下面的Discard服务器代码,其中将选择器使用流程中的步骤进行了进一步细化:

public class NioDiscardServer {
    public static void startServer() throws IOException { 
        // 1.获取选择器
        Selector selector = Selector.open(); 
        // 2.获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false); 
        // 4.绑定连接
        serverSocketChannel.bind(newInetSocketAddress(18899)); 
        Logger.info("服务器启动成功");
        // 5.将通道注册的“接收新连接”IO 事件,注册到选择器上 
        serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
        // 6.轮询感兴趣的  IO 就绪事件(选择键集合) 
        while (selector.select() > 0) {
            // 7.获取选择键集合
            Iterator<SelectionKey> selectedKeys =selector.selectedKeys().iterator();
            while (selectedKeys.hasNext()) { 
            // 8.获取单个的选择键,并处理
            SelectionKey selectedKey = selectedKeys.next(); 
            // 9.判断  key 是具体的什么事件
            if (selectedKey.isAcceptable()) {
                // 10.若选择键的  IO 事件是“连接就绪”事件,就获取客户端连接 
                SocketChannel socketChannel =serverSocketChannel.accept();
                // 11.将新连接切换为非阻塞模式
                socketChannel.configureBlocking(false);
                // 12.将该新连接的通道的可读事件,注册到选择器上 
                socketChannel.register(selector, SelectionKey.OP_READ);
            } else if (selectedKey.isReadable()) {
                // 13.若选择键的  IO 事件是“可读”事件, 读取数据 
                SocketChannelsocketChannel =(SocketChannel) selectedKey.channel(); 
                // 14.读取数据,然后丢弃
                ByteBufferbyteBuffer = ByteBuffer.allocate(1024); 
                int length = 0;
                while ((length =socketChannel.read(byteBuffer)) >0) 
                {
                    byteBuffer.flip();
                    Logger.info(new String(byteBuffer.array(), 0, length));
                    byteBuffer.clear(); 
                }
                socketChannel.close(); 
            }
            // 15.移除选择键 
            selectedKeys.remove();
        } 
    }
    // 16.关闭连接
    serverSocketChannel.close(); 
    }
    public static void main(String[] args) throws IOException {
        startServer(); 
    }
}

        实现DiscardServer丢弃服务一共分为16步,其中第7到第15步是循环执行的,不断查询选择感兴趣的IO事件到选择键集合中,然后通过selector.selectedKeys()获取该选择键集合,并且进行迭代处理。在事件处理过程中,对于新建立的socketChannel客户端传输通道,也要注册到同一个选择器上,这样就能使用同一个选择线程,不断地对所有的注册通道进行选择键的查询。

        在DiscardServer程序中,涉及到两次选择器注册:一次是注册serverChannel服务器通道;另一次,注册接收到的socketChannel客户端传输通道。前者serverChannel服务器通道所注册的,是新连接的IO事件SelectionKey.OP_ACCEPT;后者客户端传输通道socketChannel所注册的,是可读IO事件SelectionKey.OP_READ。

        注册完成后如果有事件发生,也就是DiscardServer在对选择键进行处理时,通过对类型进行判断,然后进行相应的处理:

  1. 如果是SelectionKey.OP_ACCEPT新连接事件类型,代表serverChannel服务器通道接收到新的客户端连接,发生了新连接事件,则通过服务器通道的accept方法,获取新的socketChannel传输通道,并且将新通道注册到选择器;
  2. 如果是SelectionKey.OP_READ可读事件类型,代表某个客户端通道有数据可读,则读取选择键中socketChannel传输通道的数据,进行业务处理,这里是直接丢弃数据。

客户端的DiscardClient代码,则更为简单。客户端首先建立到服务器的连接,发送一些简单的数据,然后直接关闭连接。代码如下:

public class NioDiscardClient {
    public static void startClient() throws IOException {
        InetSocketAddress address =new InetSocketAddress("127.0.0.1",18899);
        // 1.获取通道
        SocketChannel socketChannel = SocketChannel.open(address); 
        // 2.切换成非阻塞模式
        socketChannel.configureBlocking(false); 
        //不断地自旋、等待连接完成,或者做一些其他的事情
        while (!socketChannel.finishConnect()) { 
        }
        Logger.info("客户端连接成功"); 
        // 3.分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 
        byteBuffer.put("hello world".getBytes()); 
        byteBuffer.flip();
        //发送到服务器
        socketChannel.write(byteBuffer);
        socketChannel.shutdownOutput();
        socketChannel.close();
    }
    public static void main(String[] args) throws IOException {
        startClient(); 
    }
}

使用SocketChannel在服务器端接收文件的实践案例

服务器端接收文件的示例代码如下所示:

public class NioReceiveServer
{
    //接受文件路径
    private static final String RECEIVE_PATH = NioDemoConfig.SOCKET_RECEIVE_PATH;
    private Charset charset = Charset.forName("UTF-8");
    /**
    * 服务器端保存的客户端对象,对应一个客户端文件 
    */
    static class Client 
    {
    //文件名称 
    String fileName; 
    //长度
    long fileLength; 
    //开始传输的时间
    long startTime; 
    //客户端的地址
    InetSocketAddress remoteAddress; 
    //输出的文件通道
    FileChannel outChannel; 
    //接收长度
    long receiveLength; 
    public boolean isFinished()
    {
        return receiveLength >= fileLength; 
    }
    private ByteBuffer buffer= ByteBuffer.allocate(NioDemoConfig.SERVER_BUFFER_SIZE);
    //使用  Map 保存每个客户端传输
    //当  OP_READ 通道可读时,根据  channel 找到对应的对象
    Map<SelectableChannel, Client> clientMap = new HashMap<SelectableChannel, Client>();
    public void startServer() throws IOException 
    {
        // 1、获取  Selector 选择器
        Selector selector = Selector.open();
        // 2、获取通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverChannel.socket();
        // 3.设置为非阻塞
        serverChannel.configureBlocking(false); 
        // 4、绑定连接
        InetSocketAddress address= new InetSocketAddress(18899); 
        serverSocket.bind(address);
        // 5、将通道注册到选择器上,并注册的  IO 事件为:“接收新连接” 
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 6、轮询感兴趣的  I/O 就绪事件(选择键集合) 
        while (selector.select() > 0)
        {
            // 7、获取选择键集合
            Iterator<SelectionKey> it =selector.selectedKeys().iterator();
            while (it.hasNext()) 
            {
                // 8、获取单个的选择键,并处理 
                SelectionKey key = it.next();
                // 9、判断  key 是具体的什么事件,是否为新连接事件 
                if (key.isAcceptable())
                {
                    // 10、若接受的事件是“新连接”事件,就获取客户端新连接 
                    ServerSocketChannel server =(ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = server.accept(); 
                    if (socketChannel == null) continue;
                        // 11、客户端新连接,切换为非阻塞模式 
                        socketChannel.configureBlocking(false); 
                        // 12、将客户端新连接通道注册到  selector 选择器上 
                        SelectionKey selectionKey =socketChannel.register(selector, SelectionKey.OP_READ);
                        // 余下为业务处理
                        Client client = new Client(); 
                        client.remoteAddress  =(InetSocketAddress) socketChannel.getRemoteAddress(); 
                        clientMap.put(socketChannel, client);
                        Logger.debug(socketChannel.getRemoteAddress() + "连接成功...");
                    } else if (key.isReadable()) 
                    {
                        processData(key);
                    }
                    // NIO 的特点只会累加,已选择的键的集合不会删除 
                    // 如果不删除,下一次又会被  select 函数选中 
                    it.remove();
                } 
        }
    
    /**
    * 处理客户端传输过来的数据 
    */
    private void processData(SelectionKey key) throws IOException 
    {
        Client client = clientMap.get(key.channel());
        SocketChannel socketChannel = (SocketChannel) key.channel(); 
        int num = 0;
        try 
        {
            buffer.clear();
            while ((num = socketChannel.read(buffer)) > 0) 
            {
                buffer.flip();
                //客户端发送过来的,首先处理文件名 
                if (null == client.fileName) 
                {
                    if (buffer.capacity() < 4) 
                    {
                        continue; 
                    }
                    int fileNameLen = buffer.getInt();
                    byte[] fileNameBytes = new byte[fileNameLen]; 
                    buffer.get(fileNameBytes);
                    // 文件名
                    String fileName = new String(fileNameBytes, charset);
                    File directory = new File(RECEIVE_PATH); 
                    if (!directory.exists())
                    {
                        directory.mkdir(); 
                    }
                    Logger.info("NIO  传输目标  dir:", directory);
                    client.fileName = fileName; 
                    String fullName = directory.getAbsolutePath() + File.separatorChar + fileName;
                    Logger.info("NIO  传输目标文件:", fullName);
                    File file = new File(fullName.trim()); 
                    if (!file.exists())
                    {
                        file.createNewFile(); 
                    }
                    FileChannel fileChannel =new FileOutputStream(file).getChannel(); 
                    client.outChannel = fileChannel;
                    if (buffer.capacity() < 8) 
                    {
                        continue; 
                    }
                    // 文件长度
                    long fileLength = buffer.getLong(); 
                    client.fileLength = fileLength;
                    client.startTime = System.currentTimeMillis(); 
                    Logger.debug("NIO  传输开始:");
                    client.receiveLength += buffer.capacity(); 
                    if (buffer.capacity() > 0)
                    {
                        // 写入文件
                        client.outChannel.write(buffer); 
                    }
                    if (client.isFinished()) 
                    {
                        finished(key, client); 
                    }
                    buffer.clear();
                }
                //客户端发送过来的,最后是文件内容 
                else
                {
                    client.receiveLength += buffer.capacity(); 
                    // 写入文件
                    client.outChannel.write(buffer); 
                    if (client.isFinished())
                    {
                        finished(key, client); 
                    }
                    buffer.clear(); 
                }
             }
                key.cancel();
            } catch (IOException e)
            {
                key.cancel();
                e.printStackTrace(); 
                return;
            }
            // 调用  close 为-1 到达末尾 
            if (num == -1)
            {
                finished(key, client); 
                buffer.clear();
            } 
        }
    private void finished(SelectionKey key, Client client) 
    {
        IOUtil.closeQuietly(client.outChannel); 
        Logger.info("上传完毕");
        key.cancel();
        Logger.debug("文件接收成功,File Name:" + client.fileName); 
        Logger.debug(" Size:" +
        IOUtil.getFormatFileSize(client.fileLength));
        long endTime = System.currentTimeMillis();
        Logger.debug("NIO IO 传输毫秒数:" +
        (endTime - client.startTime));
    }
    /** 
    * 入口 
    */
    public static void main(String[] args) throws Exception 
    {
        NioReceiveServer server = new NioReceiveServer(); 
        server.startServer();
    } 
    }
}

由于客户端每次传输文件,都会分为多次传输:

  1. 首先传入文件名称;
  2. 其次是文件大小;
  3. 然后是文件内容。

        对应于每一个客户端socketChannel,创建一个Client客户端对象,用于保存客户端状态,分别保存文件名、文件大小和写入的目标文件通道outChannel。

        socketChannel和Client对象之间是一对一的对应关系:建立连接的时候,以键值对的形式保存Client实例在map中,其中socketChannel作为键(Key),Client对象作为值(Value)。当socketChannel传输通道有数据可读时,通过选择键key.channel()方法,取出IO事件所在socketChannel通道。然后通过socketChannel通道,从map中取到对应的Client对象。

        接收到数据时,如果文件名为空,先处理文件名称,并把文件名保存到Client对象,同时创建服务器上的目标文件;接下来再读到数据,说明接收到了文件大小,把文件大小保存到Client对象;接下来再接到数据,说明是文件内容了,则写入Client对象的outChannel文件通道中,直到数据读取完毕。

        运行方式:启动这个NioReceiveServer服务器程序后,再启动前面介绍的客户端程序NioSendClient,即可以完成文件的传输。

        由于NIO传输是非阻塞的、异步的,所以,在传输过程中会出现“粘包”和“半包”问题。正因为这个原因,无论是前面NIO文件传输实例、还是Discard服务器程序,都会在传输过程中的出现异常现象(偶现)。

小结

        在编程难度上,Java NIO编程的难度比同步阻塞Java OIO编程大很多。与Java OIO相比,Java NIO编程大致的特点如下:

  1. 在NIO中,服务器接收新连接的工作,是异步进行的。不像Java的OIO那样,服务器监听连接,是同步的、阻塞的。NIO可以通过选择器(也可以说成:多路复用器),后续不断地轮询选择器的选择键集合,选择新到来的连接。
  2. 在NIO中,SocketChannel传输通道的读写操作都是异步的。如果没有可读写的数据,负责IO通信的线程不会同步等待。这样,线程就可以处理其他连接的通道;不需要像OIO那样,线程一直阻塞,等待所负责的连接可用为止。
  3. 在NIO中,一个选择器线程可以同时处理成千上万的客户端连接,性能不会随着客户端的增加而线性下降。

        总之,有了Linux底层的epoll支持,以及Java NIO Selector选择器等等应用层IO复用技术,Java程序从而可以实现IO通信的高TPS、高并发,使服务器具备并发数十万、数百万的连接能力。Java的NIO技术非常适合用于高性能、高负载的网络服务器。鼎鼎大名的通信服务器中间件Netty,就是基于Java的NIO技术实现的。

相关文章:

  • Lock和synchronized的区别
  • 全球元宇宙市场投融资总额超320亿元 资本争相涌入 元宇宙探索仍在路上
  • 谷粒学苑_第一天
  • 北大肖臻老师《区块链技术与应用》系列课程学习笔记[28]以太坊-美链
  • 【Unity】U3D ARPG游戏制作实例(二)人物基本动作切换
  • https证书怎么做?
  • Everything 全局搜索之正则表达式Regex
  • python常见错误类型
  • 22-09-01 西安 JUC(04)java内存模型JMM、volatile关键字、原子性类、CAS比较并交换、AQS锁原理
  • 加湿器芯片方案:不要让空调带走你的水分
  • Machine learning week 10(Andrew Ng)
  • spring-cloud-alibaba-Nacos2.0.3:注册中心和配置中心框架学习
  • android studio教程,Android Studio一个完整的APP实例
  • jumpserver堡垒机界面设置及界面功能
  • LeetCode---SQL刷题6
  • 实现windows 窗体的自己画,网上摘抄的,学习了
  • 【划重点】MySQL技术内幕:InnoDB存储引擎
  • es6(二):字符串的扩展
  • Golang-长连接-状态推送
  • HTTP中GET与POST的区别 99%的错误认识
  • JAVA多线程机制解析-volatilesynchronized
  • jquery cookie
  • Mocha测试初探
  • Solarized Scheme
  • Vue2.x学习三:事件处理生命周期钩子
  • vue数据传递--我有特殊的实现技巧
  • 动态规划入门(以爬楼梯为例)
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 写代码的正确姿势
  • 新手搭建网站的主要流程
  • PostgreSQL之连接数修改
  • scrapy中间件源码分析及常用中间件大全
  • 国内唯一,阿里云入选全球区块链云服务报告,领先AWS、Google ...
  • # .NET Framework中使用命名管道进行进程间通信
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #我与Java虚拟机的故事#连载01:人在JVM,身不由己
  • #周末课堂# 【Linux + JVM + Mysql高级性能优化班】(火热报名中~~~)
  • (09)Hive——CTE 公共表达式
  • (C++17) optional的使用
  • (Spark3.2.0)Spark SQL 初探: 使用大数据分析2000万KF数据
  • (附源码)springboot 智能停车场系统 毕业设计065415
  • (十八)SpringBoot之发送QQ邮件
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (五)网络优化与超参数选择--九五小庞
  • (一) storm的集群安装与配置
  • (转)http协议
  • (转)winform之ListView
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .axf 转化 .bin文件 的方法
  • .MyFile@waifu.club.wis.mkp勒索病毒数据怎么处理|数据解密恢复
  • .Net 6.0 处理跨域的方式
  • .NET BackgroundWorker
  • .NET Core中的去虚
  • .Net7 环境安装配置
  • .net流程开发平台的一些难点(1)