網路程式設計NIO:BIO和NIO
BIO
BIO(Blocking I/O),同步阻塞,實現模式為一個連線一個執行緒,即當有客戶端連線時,伺服器端需為其單獨分配一個執行緒,如果該連線不做任何操作就會造成不必要的執行緒開銷。BIO是傳統的Java io程式設計,其相關的類和介面在java.io 包下。
BIO適用於連線數目較小且固定的架構,對伺服器資源的要求較高,是JDK1.4以前的唯一選擇,但程式簡單易理解。
BIO程式設計流程
-
伺服器端啟動一個SeverSocket
-
客戶端啟動Socket對伺服器端發起通訊,預設情況下伺服器端需為每個客戶端建立一個執行緒與之通訊
-
客戶端發起請求後,先諮詢伺服器端是否有執行緒響應,如果沒有則會等待或被拒絕
-
如果有執行緒響應,客戶端執行緒會等待請求結束後,再繼續執行
簡單程式碼實現
//BIO-伺服器端 public class BIOSever { public static void main(String[] args) throws IOException { //在BIO中,可以使用執行緒池進行優化 ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); ServerSocket serverSocket = new ServerSocket(6666); System.out.println("伺服器已啟動"); while (true){ System.out.println("等待客戶端連線.....(阻塞中)"); Socket socket = serverSocket.accept(); System.out.println("客戶端連線"); cachedThreadPool.execute(new Runnable() { public void run() { handler(socket); } }); } } //從客服端socket讀取資料 public static void handler(Socket socket){ try{ InputStream inputStream = socket.getInputStream(); byte[] b = new byte[1024]; while (true){ System.out.println("等待客戶端輸入.....(阻塞中)"); int read = inputStream.read(b); if (read != -1){ System.out.println(new String(b, 0, read)); }else { break; } } inputStream.close(); }catch (Exception e){ e.printStackTrace(); }finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
//BIO-客戶端 public class BIOClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("localhost", 6666); OutputStream outputStream = socket.getOutputStream(); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()){ String message = scanner.nextLine(); if ("exit".equals(message)) { break; } outputStream.write(message.getBytes()); } outputStream.close(); socket.close(); } }
BIO問題分析
從上面程式碼中可以看出BIO程式設計的兩個問題:
-
伺服器端在監聽客戶端連線時(serverSocket.accept()),伺服器端處於阻塞狀態,不能處理其他事務
-
伺服器端需要為每個客戶端建立一個執行緒,雖然可以用執行緒池來優化,但在併發較大時,執行緒開銷依舊很大
-
當連線的客戶端沒有傳送資料時,伺服器端會阻塞在read操作上,等待客戶端輸入,造成執行緒資源浪費
NIO
從JDK1.4開始,java提供了一系列改進輸入/輸出的新特性,統稱為NIO,全稱n為new I/O,是同步非阻塞的,所以也有人稱為non-blocking I/O。NIO的相關類都放在java.nio包或其子包下,並對原先java.io包中許多類進行了改寫。
NIO的三大核心
緩衝區(Buffer)
NIO是面向緩衝區, 或者說是面向塊程式設計的。在NIO的IO傳輸中,資料會先讀入到緩衝區,當需要時再從緩衝區寫出,這樣減少了直接讀寫磁碟的次數,提高了IO傳輸的效率。
緩衝區(buffer)本質上是一個可以讀寫資料的記憶體塊,即在記憶體空間中預留了一定的儲存空間,這些儲存空間用來緩衝輸入和輸出的資料,這部分預留的儲存空間就叫緩衝區。
在NIO程式中,通道channel雖然負責資料的傳輸,但是輸入和輸出的資料都必須經過緩衝區buffer。
在java中,緩衝區的相關類都在java.nio包下,其最頂層的類是 Buffer,它是一個抽象類。
Buffer類的4個重要屬性:
-
mark:標記
-
position:位置,下一個要被讀或寫的元素的索引,每次讀寫緩衝區都會改變該值,為下次讀寫做準備
-
limit:表示緩衝區的終點,不能對緩衝區中超過極限的位置進行讀寫操作,且極限是可修改的
-
capacity:容量,即緩衝區的最多可容納的資料量,該值在建立緩衝區時被設立,且不可修改
Buffer類常用方法:
Buffer的常用子類(它們之間最大區別在於底層實現陣列的資料型別):
-
ByteBuffer:儲存位元組資料到緩衝區
-
CharBuffer:儲存字元資料到緩衝區
-
IntBuffer:儲存整型資料到緩衝區
-
ShortBuffer:儲存短整型資料到緩衝區
-
LongBuffer:儲存長整型資料到緩衝區
-
FloatBuffer:儲存浮點型資料到緩衝區
-
DoubleBuffer:儲存雙精度浮點型資料到緩衝區
ByteBuffer
在Buffer的所有子類中,最常用的還是ByteBuffer,它的常用方法:
通道(Channel)
在NIO程式中伺服器端和客戶端之間的資料讀寫不是通過流,而是通過通道來讀寫的。
通道類似於流,都是用來讀寫資料的,但它們之間也是有區別的:
-
通道是雙向的,即可以讀也可以寫,而流是單向的,只能讀或寫
-
通道可以實現非同步讀寫資料
-
通道可以從緩衝區讀資料,也可以把資料寫入緩衝區
java中channel的相關類在java.nio.channel包下。Channel是一個介面,其常用的實現類有:
-
FileChannel:用於檔案的資料讀寫,其真正的實現類為FileChannelImpl
-
DatagramChannel:用於UDP的資料讀寫,其真正的實現類為DatagramChannelImpl
-
ServerSocketChannel:用於監聽TCP連線,每當有客戶端連線時都會建立一個SocketChannel,功能類似ServerSocket,其真正的實現類為ServerSocketChannelImpl
-
SocketChannel:用於TCP的資料讀寫,功能類似節點流+Socket,其真正的實現類為SocketChannelImpl
FileChannel
FileChannel主要用於對本地檔案進行IO操作,如檔案複製等。它的常用方法有:
在檔案傳輸流中有個屬性channel,它預設是空的,可以通過流中的getChanel()方法根據當前檔案流的屬性生成對應的FileChannel。
public FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, false, true, append, this); } return channel; } } }
下面是通道使用的程式碼例項:
public class NIOChannel { public static void main(String[] args) throws IOException { } //將資料寫入目標檔案 public static void writeFile() throws IOException{ String str = "Hello, gofy"; //建立檔案輸出流 FileOutputStream fileOutputStream = new FileOutputStream("f:\\file.txt"); //根據檔案輸出流生成檔案通道 FileChannel fileChannel = fileOutputStream.getChannel(); //建立位元組緩衝區,並將字串轉成位元組存入 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put(str.getBytes()); //注意,在存入後需要進行寫出操作時,需將緩衝區翻轉 byteBuffer.flip(); //將緩衝區資料寫入通道 fileChannel.write(byteBuffer); //將檔案輸出流關閉(該方法同時會關閉通道) fileOutputStream.close(); } //從檔案中讀取資料 public static void readFile() throws IOException{ //建立檔案輸入流 File file = new File("f:\\file.txt"); FileInputStream fileInputStream = new FileInputStream(file); //根據檔案輸入流生成檔案通道 FileChannel fileChannel = fileInputStream.getChannel(); //建立位元組緩衝區,大小為檔案大小 ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length()); //將通道資料讀入緩衝區 fileChannel.read(byteBuffer); //同樣,在讀入後需要取出緩衝區內所有資料時,需將緩衝區翻轉 byteBuffer.flip(); System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } //將檔案資料傳輸到另一個檔案 public static void readAndWriteFile() throws IOException{ //建立檔案輸入流和檔案輸出流,並生成對應的通道 FileInputStream fileInputStream = new FileInputStream("file1.txt"); FileChannel inputStreamChannel= fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("file2.txt"); FileChannel outputStreamChannel = fileOutputStream.getChannel(); //建立位元組緩衝區 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //進行資料讀取 while (true){ //在讀取前需清除緩衝區 byteBuffer.clear(); //將檔案輸入的通道的資料讀入緩衝區 int read = inputStreamChannel.read(byteBuffer); //當read為-1時,即通道資料已讀取完畢 if (read == -1){ break; } //將緩衝區翻轉後,將緩衝區資料寫入檔案輸出的通道 byteBuffer.flip(); outputStreamChannel.write(byteBuffer); } fileInputStream.close(); fileOutputStream.close(); } //檔案的複製貼上 public static void copyAndPaste() throws IOException{ //複製的檔案輸入流 FileInputStream fileInputStream = new FileInputStream("f:\\a.jpg"); FileChannel srcChannel = fileInputStream.getChannel(); //貼上的檔案輸出流 FileOutputStream fileOutputStream = new FileOutputStream("f:\\b.jpg"); FileChannel targetChannel = fileOutputStream.getChannel(); //使用transferFrom進行復制貼上 targetChannel.transferFrom(srcChannel, 0, srcChannel.size()); fileInputStream.close(); fileOutputStream.close(); } }
選擇器(Selector)
在NIO程式中,可以用選擇器Selector實現一個選擇器處理多個通道,即一個執行緒處理多個連線。只要把通道註冊到Selector上,就可以通過Selector來監測通道,如果通道有事件發生,便獲取事件通道然後針對每個事件進行相應的處理。這樣,只有在通道(連線)有真正的讀/寫事件發生時,才會進行讀寫操作,大大減少了系統開銷,並且不必為每個連線建立單獨執行緒,就不用去維護過多的執行緒。
選擇器的相關類在java.nio.channels包和其子包下,頂層類是Selector,它是一個抽象類,它的常用方法有:
通道的註冊
在ServerSocketChannel和SocketChannel類裡都有一個註冊方法 register(Selector sel, int ops),sel為要註冊到的選擇器,ops為該通道監聽的操作事件的型別,可以通過該方法將ServerSocketChannel或SocketChannel註冊到目標選擇器中,該方法會返回一個SelectionKey(真正實現類為SelectionKeyImpl)儲存在註冊的Selector的publicKeys集合屬性裡。SelectionKey儲存了通道的事件型別和該註冊的通道物件,可以通過SelectionKey.channel()方法獲取SelectionKey對應的通道。
每個註冊到選擇器的通道都需定義需進行的操作事件型別,通過檢視SelectionKey類的屬性可以知道操作事件的型別有4種:
public static final int OP_READ = 1 << 0; //讀操作 public static final int OP_WRITE = 1 << 2; //寫操作 public static final int OP_CONNECT = 1 << 3; //連線操作 public static final int OP_ACCEPT = 1 << 4; //接收操作
選擇器的檢查
我們可以通過選擇器的檢查方法,如select()來得知發生事件的通道數量,當該數量大於為0時,即至少有一個通道發生了事件,就可以使用selectedKeys()方法來獲取所有發生事件的通道對應的SelectionKey,通過SelectionKey中的方法來判斷對應通道中需處理的事件型別是什麼,在根據事件做出相應的處理。
public final boolean isReadable() { //判斷是否是讀操作 return (readyOps() & OP_READ) != 0; } public final boolean isWritable() { //判斷是否是寫操作 return (readyOps() & OP_WRITE) != 0; } public final boolean isConnectable() { //判斷是否是連線操作 return (readyOps() & OP_CONNECT) != 0; } public final boolean isAcceptable() { //判斷是否是接收操作 return (readyOps() & OP_ACCEPT) != 0; }
NIO實現簡單的聊天群
//伺服器端 public class GroupChatSever { private final static int PORT = 6666;//監聽埠 private Selector selector;//選擇器 private ServerSocketChannel serverSocketChannel; public GroupChatSever(){ try{ selector = Selector.open();//開啟選擇器 serverSocketChannel = ServerSocketChannel.open();//開啟通道 serverSocketChannel.configureBlocking(false);//將通道設為非阻塞狀態 serverSocketChannel.socket().bind(new InetSocketAddress(PORT));//通道繫結監聽埠 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//將通道註冊到選擇器上,事件型別為接收 listen(); }catch (IOException e){ e.printStackTrace(); } } //對埠進行監聽 public void listen(){ try { while (true){ //檢查註冊通道是否有事件發生,檢查時長為2秒 int count = selector.select(2000); if (count > 0){//如果註冊通道有事件發生則進行處理 //獲取所有發生事件的通道對應的SelectionKey Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); if (key.isAcceptable()){//判斷該key對應的通道是否需進行接收操作 //雖然accept()方法是阻塞的,但是因為對通道進行過判斷, //可以確定是有客戶端連線的,所以此時呼叫accept並不會阻塞 SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); //接收後,將獲取的客戶端通道註冊到選擇器上,事件型別為讀 socketChannel.register(selector, SelectionKey.OP_READ); System.out.println(socketChannel.getRemoteAddress() + "上線!"); } if (key.isReadable()){//判斷該key對應的通道是否需進行讀操作 readFromClient(key); } //注意當處理完一個通道key時,需將它從迭代器中移除 keyIterator.remove(); } } } }catch (IOException e){ e.printStackTrace(); } } /** * 讀取客戶端發來的訊息 * @param key 需讀取的通道對應的SelectionKey */ public void readFromClient(SelectionKey key){ SocketChannel socketChannel = null; try{ //通過SelectionKey獲取對應通道 socketChannel = (SocketChannel)key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int read = socketChannel.read(byteBuffer); if (read > 0){ String message = new String(byteBuffer.array()); System.out.println("客戶端: " + message); sendToOtherClient(message, socketChannel); } }catch (IOException e){ //這裡做了簡化,將所有異常都當做是客戶端斷開連線觸發的異常,實際專案中請不要這樣做 try{ System.out.println(socketChannel.getRemoteAddress() + "下線"); key.cancel();//將該SelectionKey撤銷 socketChannel.close();//再關閉對應通道 }catch (IOException e2){ e2.printStackTrace(); } } } /** * 將客戶端傳送的訊息轉發到其他客戶端 * @param message 轉發的訊息 * @param from 傳送訊息的客戶端通道 * @throws IOException */ public void sendToOtherClient(String message, SocketChannel from) throws IOException{ System.out.println("訊息轉發中......"); for (SelectionKey key : selector.keys()){//遍歷選擇器中所有SelectionKey Channel channel = key.channel();//根據SelectionKey獲取對應通道 //排除掉髮送訊息的通道,將訊息寫入到其他客戶端通道 if (channel instanceof SocketChannel && channel != from){ SocketChannel socketChannel = (SocketChannel)channel; ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); socketChannel.write(byteBuffer); } } } public static void main(String[] args) { GroupChatSever groupChatSever = new GroupChatSever(); } }
//客戶端 public class GroupChatClient { private final static String SEVER_HOST = "127.0.0.1";//連線的客戶端主機 private final static int SEVER_PORT = 6666;//連線的客戶端埠 private Selector selector;//選擇器 private SocketChannel socketChannel; private String username;//儲存客戶端ip地址 public GroupChatClient(){ try { selector = Selector.open();//開啟選擇器 socketChannel = SocketChannel.open(new InetSocketAddress(SEVER_HOST, SEVER_PORT));//開啟通道 socketChannel.configureBlocking(false);//將通道設為非阻塞 socketChannel.register(selector, SelectionKey.OP_READ);//將通道註冊在選擇器上,事件型別為讀 username = socketChannel.getLocalAddress().toString().substring(1);//獲取客戶端ip地址 String message = " 進入聊天群!"; sendMessage(message); }catch (IOException e){ e.printStackTrace(); } } //傳送訊息 public void sendMessage(String message){ message = username+": "+message; try{ ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); socketChannel.write(byteBuffer); }catch (IOException e){ e.printStackTrace(); } } //讀取從伺服器轉發送過來的訊息 public void readMessage(){ try{ int read = selector.select(); if (read > 0){ Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); if (key.isReadable()){ SocketChannel socketChannel = (SocketChannel)key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); socketChannel.read(byteBuffer); System.out.println(new String(byteBuffer.array())); } keyIterator.remove(); } } }catch (IOException e){ e.printStackTrace(); } } public static void main(String[] args) { final GroupChatClient groupChatClient = new GroupChatClient(); //客戶端開啟一個執行緒來監聽是否有伺服器轉發來訊息 new Thread(){ @Override public void run() { while (true){ groupChatClient.readMessage(); try { Thread.currentThread().sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } }.start(); Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()){ String message = scanner.nextLine(); groupChatClient.sendMessage(message); } } }
&n