NIO入門實例-寫一個客戶端/服務器例程

注:該文章實際爲《java網絡編程》例11-1和例11-2的源碼勘誤!原例程經過實際測試並不能實現期望的功能,在分析代碼邏輯後勘誤如下!

在勘誤之前貼出書中原始例程(僅服務器有誤):

package org.nioTest;

import sun.rmi.runtime.Log;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

public class Servser {

  private final static Logger log = Logger.getLogger("Server.class");
  private static int client_num = 0;

  public static void main(String[] args) {
    byte[] rotation = new byte[95 * 2];
    for (byte i = ' '; i <= '~'; i++) {
      rotation[i - ' '] = i;
      rotation[i + 95 - ' '] = i;
    }

    ServerSocketChannel serverSocketChannel;
    Selector selector;
    try {
      serverSocketChannel = ServerSocketChannel.open();
      ServerSocket serverSocket = serverSocketChannel.socket();
      InetSocketAddress address = new InetSocketAddress("localhost", 5000);
      serverSocket.bind(address);
      serverSocketChannel.configureBlocking(false);

      selector = Selector.open();
      serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
      log.info("服務器通道" + client_num++ + "註冊完成->" + serverSocketChannel.toString());
    } catch (IOException e) {
      e.printStackTrace();
      return;
    }

    /**
    錯誤一、當遍歷選擇器後,起初result = 1,而後result將爲0,
    然後無限循環;並不能跳出循環執行接下來的代碼!
    **/

    while (true) {
      try {
        int result = selector.select();
        log.info("開始遍歷當前註冊爲選擇器的通道" + result);
      } catch (IOException e) {
        e.printStackTrace();
        break;
      }
    }

    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    log.info("獲取所有現在註冊的通道:");
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    while (iterator.hasNext()) {
      SelectionKey selectionKey = iterator.next();
      log.info("當前通道:" + selectionKey.toString());
      iterator.remove();
      try {
        if (selectionKey.isAcceptable()) {
          log.info("當前通道是處理連接的通道,準備取出該通道創建對等端通道(該對等通道是寫通道)");
          ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
          try {
            SocketChannel client = server.accept();
            log.info("接收客戶端的連接:" + client);
            client.configureBlocking(false);
            /**
            錯誤二:即使錯誤一解決掉,也就是說跳出循環(解決方式實例:
            if (result != 0) {
                  log.info("已經有選擇器就緒");
                  break;
            }
            )
            代碼原意是當建立一個連接後就繼續註冊一個“寫通道”,
            這樣當循環後就註冊器列表就會多出一個通道(即不爲0),
            就可以繼續判斷當前通道屬性,然後讀數據!但是回過頭
            看該代碼所在的循環就會發現,註冊器列表Set只生成一次:
            Set<SelectionKey> selectionKeys =   selector.selectedKeys();
            log.info("獲取所有現在註冊的通道:");
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            即使此次又註冊了一個註冊器(通道),並不會更新此時
            註冊器列表的迭代器,這個時候執行完此次循環後迭代器
            爲0,直接跳出循環了!所以正確的做法是:每次重新注
            冊一個選擇器後就要重新遍歷所有的選擇器,然後形成新
            的註冊器列表Set!所以,這個while循環其實應該放在
            上一個while循環裏!
            **/
            SelectionKey selectionKey_write = client.register(selector, SelectionKey.OP_WRITE);
            log.info("把該通道繼續註冊到選擇器中");
            ByteBuffer byteBuffer = ByteBuffer.allocate(74);
            byteBuffer.put(rotation, 0, 72);
            byteBuffer.put((byte) '\r');
            byteBuffer.put((byte) '\n');
            byteBuffer.flip();
            log.info("把待發出的數據綁定到寫選擇器(寫通道)上");
            selectionKey_write.attach(byteBuffer);
          } catch (IOException e) {
            e.printStackTrace();
          }
        } else if (selectionKey.isWritable()) {
          log.info("當前通道是處理寫數據的通道,準備取出該通道和該通道綁定的數據(用於輸出數據)");
          SocketChannel client = (SocketChannel) selectionKey.channel();
          ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
          if (!byteBuffer.hasRemaining()) {
            byteBuffer.rewind();
            int first = byteBuffer.get();
            byteBuffer.rewind();
            int postion = first - ' ' + 1;
            byteBuffer.put(rotation, postion, 72);
            byteBuffer.put((byte) '\r');
            byteBuffer.put((byte) '\n');
            byteBuffer.flip();
          }
          client.write(byteBuffer);
        }
      } catch (IOException e) {
        selectionKey.cancel();
        try {
          selectionKey.channel().close();
        } catch (IOException e1) {
          e1.printStackTrace();
        }
        e.printStackTrace();
      }
    }
  }
}

代碼錯誤原因已經在源碼中說明!這是修改過的例程:
客戶端

package org.nioTest;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.logging.Logger;

public class Client {
  private final static Logger log = Logger.getLogger("Client.class");
  public static void main(String[] args) {
    try{
      SocketAddress address = new InetSocketAddress("localhost",5000);
      SocketChannel socketChannel_client = SocketChannel.open(address);
      log.info("綁定服務器完成"+socketChannel_client.toString());

      ByteBuffer byteBuffer = ByteBuffer.allocate(74);
      WritableByteChannel out = Channels.newChannel(System.out);
      while(socketChannel_client.read(byteBuffer) != -1){
        byteBuffer.flip();
        out.write(byteBuffer);
        byteBuffer.clear();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

服務器:

package org.nioTest;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Logger;

public class Servser {

  private final static Logger log = Logger.getLogger("Server.class");
  private static int client_num = 0;

  public static void main(String[] args) {
    byte[] rotation = new byte[95 * 2];
    for (byte i = ' '; i <= '~'; i++) {
      rotation[i - ' '] = i;
      rotation[i + 95 - ' '] = i;
    }

    ServerSocketChannel serverSocketChannel;
    Selector selector;
    try {
      serverSocketChannel = ServerSocketChannel.open();
      ServerSocket serverSocket = serverSocketChannel.socket();
      InetSocketAddress address = new InetSocketAddress("localhost", 5000);
      serverSocket.bind(address);
      serverSocketChannel.configureBlocking(false);

      selector = Selector.open();
      serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
      log.info("服務器通道" + client_num++ + "註冊完成->" + serverSocketChannel.toString());
    } catch (IOException e) {
      e.printStackTrace();
      return;
    }

    while (true) {
      try {
        int result = selector.select();
        log.info("開始遍歷當前註冊爲選擇器的通道" + result);
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        log.info("獲取所有現在註冊的通道:");
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        /*注意此時的while循環放在這裏*/
        while (iterator.hasNext()) {
          SelectionKey selectionKey = iterator.next();
          log.info("當前通道:" + selectionKey.toString());
          iterator.remove();
          try {
            if (selectionKey.isAcceptable()) {
              log.info("當前通道是處理連接的通道,準備取出該通道創建對等端通道(該對等通道是寫通道)");
              ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
              try {
                SocketChannel client = server.accept();
                log.info("接收客戶端的連接:" + client);
                client.configureBlocking(false);
                SelectionKey selectionKey_write = client.register(selector, SelectionKey.OP_WRITE);
                iterator = selector.selectedKeys().iterator();
                log.info("把該通道繼續註冊到選擇器中");
                ByteBuffer byteBuffer = ByteBuffer.allocate(74);
                byteBuffer.put(rotation, 0, 72);
                byteBuffer.put((byte) '\r');
                byteBuffer.put((byte) '\n');
                byteBuffer.flip();
                log.info("把待發出的數據綁定到寫選擇器(寫通道)上");
                selectionKey_write.attach(byteBuffer);
              } catch (IOException e) {
                e.printStackTrace();
              }
            } else if (selectionKey.isWritable()) {
              log.info("當前通道是處理寫數據的通道,準備取出該通道和該通道綁定的數據(用於輸出數據)");
              SocketChannel client = (SocketChannel) selectionKey.channel();
              ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
              if (!byteBuffer.hasRemaining()) {
                byteBuffer.rewind();
                int first = byteBuffer.get();
                byteBuffer.rewind();
                int postion = first - ' ' + 1;
                byteBuffer.put(rotation, postion, 72);
                byteBuffer.put((byte) '\r');
                byteBuffer.put((byte) '\n');
                byteBuffer.flip();
              }
              client.write(byteBuffer);
            }
          } catch (IOException e) {
            selectionKey.cancel();
            try {
              selectionKey.channel().close();
            } catch (IOException e1) {
              e1.printStackTrace();
            }
            e.printStackTrace();
          }
        }
      } catch (IOException e) {
        e.printStackTrace();
        break;
      }
    }
  }
}

修改過後就可正常運行了!可以試着開一個服務器,同時開n個客戶端來驗證非阻塞模式下單線程也能同時處理多個請求的效果!
這裏寫圖片描述

這裏並沒有詳細敘述NIO的相關知識,所以關於NIO的基本概念、入門教程請參考:
(1)Java NIO系列教程
(2)JAVA網絡編程第四版中文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章