前言
- 在將NIO之前,我們必須要了解一下Java的IO部分知識。
- BIO(Blocking IO)
- 阻塞IO,在Java中主要就是通過ServerSocket.accept()實現的。
- NIO(Non-Blocking IO)
- 非阻塞IO,在Java主要是通過NIOSocketChannel + Seletor實現的。
- AIO(Asyc IO)
- 異步IO,目前不做學習。
BIO
簡單實現服務器和客戶端
<code>package net.io; import net.ByteUtil; import java.io.*; import java.net.ServerSocket; import java.net.Socket; //NIO(NonBlocking IO)非阻塞IO //通過一個事件監聽器,吧這些客戶端的連接保存起來,如果有時間發生再去處理,沒時間發生不處理 public class Server { public Server(int port) { try { //創建服務器端,監聽端口port ServerSocket serverSocket = new ServerSocket(port); //對客戶端進行一個監聽操作,如果有連接過來,就將連接返回(socket)-----阻塞方法 while (true) { //監聽,阻塞方法 Socket socket = serverSocket.accept(); //每個服務器和客戶端的通信都是針對與socket進行操作 System.out.println("客戶端" + socket.getInetAddress()); InputStream inputStream = socket.getInputStream(); ObjectInputStream ois = new ObjectInputStream(inputStream); //獲取客戶端發送的message Object get = ois.readObject(); System.out.println("接收到的消息為:" + get); //服務器需要給客戶端進行一個回應 OutputStream outputStream = socket.getOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(outputStream); String message = "客戶端你好,我是服務器端"; //我這裡寫了,不代表發送了,知識寫到了輸出流的緩衝區 oos.writeObject(message); //發送並清空 oos.flush(); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { new Server(7000); } } /<code>
<code>package net.io; import net.ByteUtil; import java.io.*; import java.net.Socket; public class Client { public Client(int port){ try { Socket socket = new Socket("localhost",port); //inputStream是輸入流,從外面接收信息 //outpurStream是輸出流, 往外面輸出信息 OutputStream outputStream = socket.getOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(outputStream); //發送的信息 String message = "服務器你好,我是客戶端"; //我這裡寫了,不代表發送了,知識寫到了輸出流的緩衝區 oos.writeObject(message); //發送並清空 oos.flush(); //接收服務器的回應 InputStream inputStream = socket.getInputStream(); ObjectInputStream ois = new ObjectInputStream(inputStream); Object get = ois.readObject(); System.out.println("接收的信息為:" + get); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { new Client(7000); } } /<code>
針對於BIO,為什麼是阻塞IO,是因為BIO是基於Socket完成數據的讀寫操作,Server調用accept()方法持續監聽socket連接,所以就阻塞在accept()這裡(後面的操作如果沒有socket連接則無法執行),那這樣就代表服務器只能同時處理一個socket的操作。
所以後序我們通過為socket連接創建一個線程執行,這樣就可以增大服務器處理連接的數量了。
但是新的問題也就出來了,如果客戶端的連接很多,那麼就會導致服務器創建很多的線程
對socket進行處理,如果服務器端不斷開連接的話,那麼對應的線程也不會被銷燬,這樣大數量的線程的維護十分消耗資源。針對於這種情況設計出了Java的NIO。
NIO
首先我們需要介紹一下NIO。如果說BIO是面向socket進行讀寫操作的話,那麼NIO則是面向channel進行讀寫操作(或者說面向buffer)。
這裡我們在講解NIO之前,需要先講解一下這個buffer。眾所周知這就是一個緩衝,並且我們知道socket具有輸入流inputStream和輸出流outputStream(讀寫分開的),但是我們的channel是同時具有read和write兩個方法,而且兩個方法都是基於buffer進行操作(這裡就可以說明channel僅能比普通輸入輸出流好,相當於channel是一條雙向,輸入輸出流是兩條單向),所以我們可以知道buffer的重要性。
Buffer
諸如ByteBuffer,IntBuffer等都是Buffer的派生抽象類,需要調用抽象類的靜態方法allocate(X capacity)方法進行一個初始化操作,該方法就是初始化buffer的大小,或者使用wrap(X x)方法,該方法相當於直接將信息存入緩衝中。至於存入buffer的put()方法和取出緩存的get()方法在下面代碼中我就詳細介紹(有底層知識,具有源碼閱讀能力的可以根據我的註釋進行閱讀),最關鍵的還有flip()方法,它是作為一個讀寫切換的作用,他使的緩存可又讀又寫,又使得讀寫相互隔離(需要注意的是使用buffer儘量是依次寫完然後再一次讀完,最後在調用clear()方法進行復位,不然會導致buffer容量越來越小,具體解釋在下面代碼)。
<code>package net.nio.buffer; import java.nio.IntBuffer; public class TestBuffer { public static void main(String[] args) { /* *IntBuffer有四個重要參數 * 1.mark 標記 * 2.position 相當於當前下標、索引 * 3.limit 代表緩衝的終點,讀取不能超過該下標,當然也不能超過最大容量。(在調用flip時候會將當前下標position值賦值給limit,然後position置0) * 4.Capacity 最大容量,在初始化IntBuffer對象時候就定義好了,不能改變(IntBuffer.allocate(int capacity) ) * * ctrl+h 可以查看該類的子類 */ //intBuffer初始化 IntBuffer intBuffer = IntBuffer.allocate(5); //放數據到緩衝區中 intBuffer.put(10); intBuffer.put(11); intBuffer.put(12); // intBuffer.put(13); // intBuffer.put(14); /* *這裡的讀寫反轉的實現機制是: * 例如我們緩衝區容量為5,調用方法put()將數據寫入緩衝區中,假如我們寫入三個此時position為3,此時limit = capacity * 如果我們調用flip方法使得limit = 3 ,position = 0 ,mark我們現在先不管(下圖源碼已說明) * public Buffer flip() { * limit = position; * position = 0; * mark = -1; * return this; * } * * 此時我們調用get()方法時候,取得下標是position的值,即從0下標讀取。直到讀取到position = limit = 3時候停止(不包括3) * 1.如果我們這個時候不調用flip()方法直接再次put()往緩衝區寫入數據(即沒從讀狀態切換到寫狀態),那麼就會報錯超過下標overflow * 2.如果我們調用一次flip()(即進入寫狀態)寫入一個數據後,那麼此時position = 0,limit = 3,此時我們最多存放3個數據(即下標0,1,2) * 如果我們不再次調用flip()切換狀態那麼就會導致,讀取到錯誤數據,(即只存入了一個數據,但是卻取出來了3個數據) * * 上述說明了一個問題,如果我們存取的數據越來越小,那麼這個緩衝區逐漸縮小,導致並不能存取他的最大容量,可能會浪費內存, *(因為position是不能超過limit的,然而調用flip()方法後會使的limit = position(賦值操作),那麼如果數據越來越少, * 就會導致緩衝區能使用的部分越來越小) * * 總結:緩衝區的大小設置應該根據實際使用進行設置(並且要及時調用clear() ),否則可能會導致緩衝區的內存浪費。 */ intBuffer.flip(); //切換讀寫狀態 //判斷緩存區是否還有剩餘 while (intBuffer.hasRemaining()) { System.out.println(intBuffer.get()); } } } /<code>
Channel
Channel是NIO實現的基礎,對於NIO,Channel的地位相當於BIO的socket。
Channel具有非常多方法,其中使用最多的就是兩個方法write(ByteBuffer buf)和read(ByteBuffer buf)方法。
(這裡需要注意的是這個read和write是buffer作為主體的,即read()方法是channel往buffer裡寫數據,而write()方法是指buffer向channel寫數據)
<code>package net.nio.channel; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class TestChannel { public static void main(String[] args) throws Exception{ String abc = "我寫入文件了"; //寫入的文件地址與文件名 FileOutputStream fileOutputStream = new FileOutputStream("C:\\xxx\\xxx\\xxx\\test.txt"); //從輸出流中獲取channel實例 FileChannel channel = fileOutputStream.getChannel(); //創建字節緩衝區 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //將字符串轉化成字節數組並放入緩衝區中,然後緩衝區轉換讀寫狀態,由寫入狀態變為讀取狀態 byteBuffer.put(abc.getBytes()); byteBuffer.flip(); //將緩衝區數據寫入到channel中(這裡write代表從緩衝區寫入,read代表從channel讀取到緩衝區) channel.write(byteBuffer); //關閉通道和輸出流 channel.close(); fileOutputStream.close(); } } /<code>
簡單NIO實現
上面在介紹NIO時候講過,NIO是需要一個Selector線程去監聽那些客戶端有實現發生,從而在進行處理,而不是BIO的一個線程維護一個socket。
下面針對於NIO我們先不引入Selector,就用BIO的方式實現一個客戶端和服務器端。(相當於作為一個練手)
Server
<code>package net.nio.socket; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; public class Server { public static void main(String[] args) throws Exception { //開啟nio的服務器端,並且綁定8000端口進行監聽 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(8000); serverSocketChannel.bind(inetSocketAddress); //創建緩衝區數組 ByteBuffer byteBuffer = ByteBuffer.allocate(40); //服務器端接收來自客戶端的請求,創建客戶端的socket的實例 SocketChannel socketChannel = serverSocketChannel.accept(); //將客戶端發送的數據讀取到buffer數組中 socketChannel.read(byteBuffer); byte[] array = byteBuffer.array(); String msg = new String(array); System.out.println("服務器收到信息 : " + msg); //對buffer數組進行讀寫反轉,由讀狀態到寫狀態 byteBuffer.flip(); //將數據回顯到客戶端去 byteBuffer.put("ok".getBytes()); //做完一套讀寫操作後,需要進行clear byteBuffer.clear(); } } /<code>
Client
<code>package net.nio.serverclient; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class NIOClient { public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000); socketChannel.configureBlocking(false); //如果客戶端未連接上服務器 if (!socketChannel.connect(inetSocketAddress)) { System.out.println("客戶端連接不上服務器。。。。"); //如果客戶端沒有完成連接 while (!socketChannel.finishConnect()) { System.out.println("連接中。。。。"); } } //進入到這裡說明連接成功 String message = "hello , Server!"; ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); //將buffer中的數據寫入socketChannel socketChannel.write(byteBuffer); System.out.println("發送完畢"); } } /<code>
NIO實現(基於Selector)
首先我們需要知道Selector是什麼?
Selector是一個選擇器,既然是一個選擇器,那麼肯定是先有選項再有選擇,理解這個後就知道channel肯定有的就是rigister()方法(因為需要將自己註冊到Selector中)。
既然選項有了,那麼如何選擇呢?
Selector是針對已註冊的channel中對有事件(例如:服務器:接受,讀寫,客戶端:讀寫,服務器是在服務器開始就將自己註冊,客戶端是連接成功後由服務器將其註冊)發生的channel進行處理。
Selector註冊的不是簡單的channel,而是將channel和其監聽事件封裝成一個SelectionKey保存在Selector底層的Set集合中。
Selector的keys()和selectedKeys()兩個方法需要注意:
keys()方法是返回已註冊的所有selectionKey。
selectedKeys()方法是返回有事件發生的selectionKey。
上面就是Selector的簡單工作流程,下面我將附上代碼,因為有較詳細的註釋,所以除了重要知識點我不再多介紹。
Server
<code>package net.nio.serverclient; import java.net.InetSocketAddress; 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; public class NIOServer { public static void main(String[] args) throws Exception { //開啟ServerSocketChannel的監聽 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //綁定端口8000 serverSocketChannel.bind(new InetSocketAddress(8000)); //創建Selector對象 Selector selector = Selector.open(); //設置監聽為非阻塞 serverSocketChannel.configureBlocking(false); //將ServerSocketChannel註冊到Selector中(註冊事件為ACCEPT) serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //Selector監聽ACCEPT時間 while (true) { //未有事件發生(下面是等待),返回值為int,代表事件發生個數 if (selector.select(1000) == 0) { System.out.println("服務器等待了1S,無事件發生。。。。"); continue; } //有客戶端請求過來,就獲取到相關的selectionKeys集合 Set selectionKeys = selector.selectedKeys(); Iterator iterator = selectionKeys.iterator(); while (iterator.hasNext()) { //獲取到事件 SelectionKey selectionKey = iterator.next(); //移出讀取過的事件 iterator.remove(); //根據對應事件對應處理 if (selectionKey.isAcceptable()) { //有新的客戶端連接服務器 SocketChannel socketChannel = serverSocketChannel.accept(); //給客戶端設置非阻塞 socketChannel.configureBlocking(false); //設置該SocketChannel為讀事件,併為它綁定一個Buffer socketChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024)); } if (selectionKey.isReadable()) { //通過Key反向獲取到事件的channel SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); //獲取到事件綁定的buffer ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment(); socketChannel.read(byteBuffer); //重置緩衝 byteBuffer.clear(); String message = new String(byteBuffer.array()); System.out.println("接收到客戶端信息為: "+ message); } } } } } /<code>
Client
<code>package net.nio.serverclient; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Scanner; public class NIOClient { public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost",8000); socketChannel.configureBlocking(false); //如果客戶端未連接上服務器 if (!socketChannel.connect(inetSocketAddress)) { System.out.println("客戶端連接不上服務器。。。。"); //如果客戶端沒有完成連接 while (!socketChannel.finishConnect()) { System.out.println("連接中。。。。"); } } //進入到這裡說明連接成功 while(true) { Scanner scanner = new Scanner(System.in); String message = scanner.nextLine(); ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes()); //將buffer中的數據寫入socketChannel socketChannel.write(byteBuffer); System.out.println("發送完畢"); } } } /<code>
我們這裡可以發現,只需要主函數中進行一個死循環,死循環中對selector註冊的channel進行監聽(select()方法),有事件發生則根據channel註冊的監聽事件對應進行處理。
這裡需要注意的是需要將ServerSocketChannel和SocketChannel編程非阻塞(調用configureBlocking(false)),不然是無法註冊到Selector中。
還有一件事需要注意:我們每次是通過iterator(迭代器)遍歷發生時間的Set ,為了避免重複處理時間,我們在獲取發生時間的selctionKey以後,就將其remove()。
最後
感謝你看到這裡,看完有什麼的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!
關鍵字: socket public ByteBuffer