BIO与NIO比较
文章目錄
- BIO 同步阻塞
- BIO介紹
- BIO的編程流程
- BIO實現通信
- 實現思路:
- 服務器:
- 客戶端:
- NIO 同步非阻塞
- NIO中重要組件
- channel:通道
- Buffer緩沖區
- 基本用法
- Buffer實現原理
- Buffer常見方法
- Buffer的分配
- selector:選擇器
- Selector概述
- selector的使用
- NIO非阻塞式網絡通信原理分析
- NIO實現
- 服務端實現
- 客戶端
- BIO與AIO區別
BIO 同步阻塞
服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動
一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷
BIO介紹
Java BIO 就是傳統的 java io 編程,其相關的類和接口在 java.ioBIO(blocking I/O) : 同步阻塞,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善(實現多個客戶連接服務器).
BIO的編程流程
法監聽客戶端的Socket連接。
需要對每個客戶 建立一個線程與之通訊
BIO實現通信
通過BIO+線程池完成少量用戶的通信架構
實現思路:
通過線程池控制解決為每個請求創建一個獨立線程造成線程資源耗盡的問題。
存在的問題:
但由于底層依然是采用的同步阻塞模型,因此無法從根本上解決問題。如果單個消息處理的緩慢,或者服務器線程池中的全部線程都被阻塞,那么后續socket的i/o消息都將在隊列中排隊。新的Socket請求將被拒絕,客戶端會發生大量連接超時。
服務器:
public class Server {public static void main(String[] args) {try {//注冊端口ServerSocket serverSocket = new ServerSocket(9999);//初始化一個線程對象HandlerSocketServerPool pool = new HandlerSocketServerPool(5, 20);//循環接受客戶端的請求while (true){Socket socket = serverSocket.accept();//將socket封裝成一個Runnable線程交給線程池ServerRunnableTarget runnable = new ServerRunnableTarget(socket);pool.execute(runnable);}} catch (Exception e) {e.printStackTrace();}} } //線程池類 class HandlerSocketServerPool{private ExecutorService executorService;//創建類的對象的時候初始化線程池對象public HandlerSocketServerPool(int maxThreadNum,int queuSize){executorService = new ThreadPoolExecutor(3, maxThreadNum, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queuSize));}/*** 提交一個方法來提交任務給線程池的任務隊列來暫時存儲,等著線程池的處理** */public void execute(Runnable target){executorService.execute(target);} } class ServerRunnableTarget implements Runnable{private Socket socket;public ServerRunnableTarget(Socket socket){this.socket=socket;}@Overridepublic void run() {//從Sacket得到一個字節輸入流try {InputStream inputStream = socket.getInputStream();//使用緩沖字符輸入流BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));String s;while ((s=bufferedReader.readLine())!=null){System.out.println(s);}} catch (Exception e) {e.printStackTrace();}} }客戶端:
public class Client {public static void main(String[] args) throws IOException {System.out.println("客戶端啟動");Socket socket = new Socket("127.0.0.1",9999);OutputStream outputStream = socket.getOutputStream();//打印流PrintStream printStream = new PrintStream(outputStream);Scanner scanner = new Scanner(System.in);while (true){System.out.println("請說:");String s = scanner.nextLine();printStream.println(s);printStream.flush();}} }NIO 同步非阻塞
NIO中重要組件
channel:通道
channel和用戶操作IO相連,但通道的使用是不能直接訪問數據的需要和緩沖區Buffer相連
讀數據:將數據從channel中讀取到Buffer,從Buffer在獲取到數據
寫數據:將數據線寫入Buffer,Buffer中的數據寫入到通道
channel與流stream的區別:
主要的實現類:
FileChannel:用于讀取、寫入、映射和操作文件的通道 DatagramChannel:通過UDP讀寫網絡中的數據通道 SocketChannel:通過TCP讀寫網絡中的數據,一般是客戶端的實現 ServerSocketChannel:監聽新進來的TCP連接,對每一個連接創建一個SocketChannel,一般是服務端的實現基于SocketChannel和ServerSocketChannel實現C/S大致流程:
服務端
1.通過ServerSocketChannel 綁定ip地址和端口號
2.通過ServerSocketChannelImpl的accept()方法創建一個SocketChannel對象用戶從客戶端讀/寫數據
3.創建讀數據/寫數據緩沖區對象來讀取客戶端數據或向客戶端發送數據
4. 關閉SocketChannel和ServerSocketChannel
Scatter / Gather( 散射/采集 )
Buffer緩沖區
Java NIO 的 Buffer 用于和 NIO Channel(通道)交互。數據是從通道讀入緩沖區,從緩沖區寫入到通道中。緩沖區本質上是塊可以寫入數據,再從中讀數據的內存。該內存被包裝成 NIO 的 Buffer 對象,并提供了一系列方法,方便開發者訪問該塊內存
基本用法
使用Buffer讀寫數據一般四步走:
1、寫數據到 Buffer 2、調用buffer.flip切換為讀模式 3、從Buffer中讀取數據 4、調用clear()或者compact()清除數據當向 buffer 寫數據時,buffer 會記錄寫了多少數據。一旦要讀取數據,需通過 flip() 將 Buffer 從寫模式切到讀模式。在讀模式下,可讀之前寫到 buffer 的所有數據。一旦讀完數據,就需要清空緩沖區,讓它可以再次被寫入。有兩種方式能清空緩沖區:調用 clear() 或 compact() 方法。
clear() 會清空整個緩沖區
compact() 只會清除已經讀過的數據。任何未讀數據都被移到緩沖區的起始處,新寫入的數據將放到緩沖區未讀數據的后面。
Buffer實現原理
Buffer就像一個數組,可以保存多個相同類型的數據。根據
數據類型不同 ,有以下 Buffer 常用子類:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer的實現底層是通過特定類型(byte、long…)數組來存儲數據
數組中數據的操作需要借助4個指針來操作:
容量 (capacity) :作為一個內存塊,Buffer具有一定的固定大小,
也稱為"容量",緩沖區容量不能為負,并且創建后不能更改。
限制 (limit):表示緩沖區中可以操作數據的大小
(limit 后數據不能進行讀寫)。緩沖區的限制不能
為負,并且不能大于其容量。 寫入模式,限制等于
buffer的容量。讀取模式下,limit等于寫入的數據量。
位置 (position):下一個要讀取或寫入的數據的索引。
緩沖區的位置不能為 負,并且不能大于其限制
標記 (mark)與重置 (reset):標記是一個索引,
通過 Buffer 中的 mark() 方法 指定 Buffer 中一個
特定的 position,之后可以通過調用 reset() 方法恢
復到這 個 position.
標記、位置、限制、容量遵守以下不變式:
0 <= mark <= position <= limit <= capacity
Buffer常見方法
Buffer clear() 清空緩沖區并返回對緩沖區的引用 Buffer flip() 為 將緩沖區的界限設置為當前位置,并將當前位置充值為 0 int capacity() 返回 Buffer 的 capacity 大小 boolean hasRemaining() 判斷緩沖區中是否還有元素 int limit() 返回 Buffer 的界限(limit) 的位置 Buffer limit(int n) 將設置緩沖區界限為 n,并返回一個具有新 limit 的緩沖區對象 Buffer mark() 對緩沖區設置標記 int position() 返回緩沖區的當前位置 position Buffer position(int n) 將設置緩沖區的當前位置為 n,并返回修改后的 Buffer 對象 int remaining() 返回 position 和 limit 之間的元素個數 Buffer reset() 將位置 position 轉到以前設置的mark 所在的位置 Buffer rewind() 將位置設為為 0, 取消設置的 markBuffer的分配
要想獲得一個Buffer對象首先要進行分配。每個Buffer類都有一個allocate方法。
直接與非直接緩沖區
ByteBufferbyte byffer可以是兩種類型,一種是基于直接內存(也就是
非堆內存);另一種是非直接內存(也就是堆內存)。對于直
接內存來說,JVM將會在IO操作上具有更高的性能,因為它
直接作用于本地系統的IO操作。而非直接內存,也就是堆內
存中的數據,如果要作IO操作,會先從本進程內存復制到直接
內存,再利用本地IO處理。
從數據流的角度,非直接內存是下面這樣的作用鏈:
本地IO–>直接內存–>非直接內存–>直接內存–>本地IO
而直接內存是:
本地IO–>直接內存–>本地IO
很明顯,在做IO處理時,比如網絡發送大量數據時,直接內
存會具有更高的效率。直接內存使用allocateDirect創建,但
是它比申請普通的堆內存需要耗費更高的性能。不過,這
部分的數據是在JVM之外的,因此它不會占用應用的內
存。所以呢,當你有很大的數據要緩存,并且它的生命
周期又很長,那么就比較適合使用直接內存。只是一般
來說,如果不是能帶來很明顯的性能提升,還是推薦直接
使用堆內存。字節緩沖區是直接緩沖區還是非直接緩沖
區可通過調用其 isDirect() 方法來確定。
Buffer的創建:
ByteBuffer為例:
ByteBuffer allocate(int capacity):在堆上創建指定大小的緩沖
ByteBuffer allocateDirect(int capacity):在堆外空間創建指定大小的緩沖
ByteBuffer wrap(byte[] array):通過byte數組實例創建一個緩沖區
ByteBuffer wrap(byte[] array, int offset, int length) 指定byte數據中的內容寫入到一個新的緩沖區
向Buffer寫數據
寫數據到Buffer有兩種方式:
1、從Channel寫到Buffer
2、通過Buffer的put()方法寫到Buffer里
buf.put(127);flip()方法:
flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,并將limit設置成之前position的值。換句話說,position現在用于標記讀的位置,limit表示之前寫進了多少個byte、char等 —— 現在能讀取多少個byte、char等。
從Buffer讀數據
兩種方式:
1、從Buffer讀取數據到Channel。
2、使用get()方法從Buffer中讀取數據。
byte aByte = buf.get();get方法有很多版本,允許你以不同的方式從Buffer中讀取數據。例如,從指定position讀取,或者從Buffer中讀取數據到字節數組。
mark()與reset()方法
通過調用Buffer.mark()方法,可以標記Buffer中的一個特定position。之后可以通過調用Buffer.reset()方法恢復到這個position。例如:
selector:選擇器
Selector概述
選擇器(Selector) 是 SelectableChannle 對象的多路復用器,Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的線程管理多個 Channel。Selector 是非阻塞 IO 的核心
Java 的 NIO,用非阻塞的 IO 方式。可以用一個線程,處理多個的客戶端連接,就會使用到 Selector(選擇器)Selector 能夠檢測多個注冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以注冊到同一個Selector),如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。只有在 連接/通道 真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,且不必為每個連接都創建一個線程,不用去維護多個線程避免了多線程之間的上下文切換導致的開銷
selector優勢:
selector的使用
創建 Selector :
通過調用 Selector.open() 方法創建一個 Selector。
向選擇器注冊通道:
SelectableChannel.register(Selector sel, int ops)
舉例:
//1. 獲取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); //2. 切換非阻塞模式 ssChannel.configureBlocking(false); //3. 綁定連接 ssChannel.bind(new InetSocketAddress(9898)); //4. 獲取選擇器 Selector selector = Selector.open(); //5. 將通道注冊到選擇器上, 并且指定“監聽接收事件” ssChannel.register(selector, SelectionKey.OP_ACCEPT);當調用 register(Selector sel, int ops) 將通道注冊選擇
器時,選擇器對通道的監聽事件,需要通過第二個參
數 ops 指定。可以監聽的事件類型(用 可使
用 SelectionKey 的四個常量 表示):
讀 : SelectionKey.OP_READ (1)
寫 : SelectionKey.OP_WRITE (4)
連接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注冊時不止監聽一個事件,則可以使用“位或”操作符連接。
如:
NIO非阻塞式網絡通信原理分析
Selector可以實現: 一個 I/O 線程可以并發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O 一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。
NIO實現
編寫一個 NIO 群聊系統,實現客戶端與客戶端的通信需求(非阻塞)
服務器端:可以監測用戶上線,離線,并實現消息轉發功能
客戶端:通過 channel 可以無阻塞發送消息給其它所有客戶端用戶,同時可以接受其它客戶端用戶通過服務端轉發來的消息
服務端實現
//服務端群聊系統實現 public class Server {//定義一些成員屬性:選擇器 服務器通道 端口private Selector selector;private ServerSocketChannel serverSocketChannel;private static final int PORT = 9999;//定義初始化代碼邏輯public Server(){//接受選擇器try {//初始化選擇器selector = Selector.open();//初始化通道serverSocketChannel=ServerSocketChannel.open();//綁定端口serverSocketChannel.bind(new InetSocketAddress(PORT));//通道切換為非阻塞模式serverSocketChannel.configureBlocking(false);//將通道注冊到選擇器上,并且開始指定監聽接收事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (IOException e) {e.printStackTrace();}}/*** 監聽客戶端各種消息事件:連接、群聊、離線* */private void listen(){try {//循環判斷是否存在就緒事件while (selector.select()>0){//獲取選擇器中的所有注冊的通道中已經就緒好的事件Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//開始遍歷這些準備好的事件while (iterator.hasNext()){//提取當前事件SelectionKey sk = iterator.next();//判斷是否為可接收事件if (sk.isAcceptable()){//獲取當前接入的客戶端通道SocketChannel socketChannel = serverSocketChannel.accept();//通道切換為非阻塞模式socketChannel.configureBlocking(false);//將本客戶端的通道注冊到選擇器上System.out.println(socketChannel.getRemoteAddress() + " 上線 ");socketChannel.register(selector,SelectionKey.OP_READ);}//判斷是否為可讀事件if(sk.isReadable()){//讀操作和轉發給其他客戶端readClientData(sk);}iterator.remove();//處理完畢移除當前事件}}}catch (Exception e){e.printStackTrace();}}/*** 接收當前客戶端發送的消息,并轉發給全部客戶端通道* */private void readClientData(SelectionKey sk){SocketChannel socketChannel = null;try {//取到該讀操作的通道socketChannel = (SocketChannel) sk.channel();//創建緩沖區對象開始接收客戶端發送的消息ByteBuffer buffer = ByteBuffer.allocate(1024);int count = socketChannel.read(buffer);if(count>0){//設置為讀模式buffer.flip();//提取讀取到的信息String msg = new String(buffer.array(),0,count);System.out.println("接收到了客戶端信息:"+msg);//返回給其他在線客戶端消息sendMsgToAllClient(msg,socketChannel);}}catch (Exception e){//該客戶斷開連接會拋出異常,異常發出下線通知try {System.out.println(socketChannel.getRemoteAddress()+"下線了");sk.channel();//關閉通道socketChannel.close();} catch (IOException ioException) {ioException.printStackTrace();}}}//發送消息給所有在線人private void sendMsgToAllClient(String msg,SocketChannel socketChannel) throws Exception{System.out.println("服務端開始轉發消息,當前處理的線程" + Thread.currentThread().getName());//循環給所有在線通道發送消息for (SelectionKey key:selector.keys()){Channel channel = key.channel();//不要把數據發送服務器和自己if(channel instanceof SocketChannel && socketChannel!=channel){//將消息存儲到buffer緩存ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());//將緩存寫入到通道( (SocketChannel)channel).write(buffer);}}}public static void main(String[] args) {//創建服務端對象Server server = new Server();//開始監聽客戶端各種消息事件:連接、群聊、離線server.listen();} }客戶端
//客戶端群聊系統實現 public class Client {private Selector selector;private static final int PDRT = 9999;private SocketChannel socketChannel;public Client(){try {//初始化選擇器selector = Selector.open();//初始化通道,并綁定通信地址與端口socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",PDRT));//通道設置為非阻塞模式socketChannel.configureBlocking(false);//將通道加載到選擇器上,并開始指定監聽讀事件socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("當前客戶端準備完成");}catch (Exception e){e.printStackTrace();}}public static void main(String[] args) {Client client = new Client();//定義一個線程負責監聽服務端發來的線程消息new Thread(new Runnable() {@Overridepublic void run() {try {while (true){//接收讀事件client.readInfo();}} catch (IOException e) {e.printStackTrace();}}}).start();//主線程進行發送消息Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()){String s= scanner.nextLine();//數據發送client.sendTOServer(s);}}private void sendTOServer(String s) {try {//數據經過緩存加載到通道上socketChannel.write(ByteBuffer.wrap(s.getBytes()));} catch (IOException e) {e.printStackTrace();}}/*** ** */private void readInfo() throws IOException {//判斷選擇器是是否有就緒事件if(selector.select()>0){//循環處理這些準備就緒的事件Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()){//取得當前就緒事件SelectionKey key = iterator.next();//判斷當前是否為讀事件if(key.isReadable()){//取得當前通道SocketChannel selectableChannel = (SocketChannel) key.channel();//創建buffer緩存接收事件ByteBuffer buffer = ByteBuffer.allocate(1024);//讀取通道上的數據socketChannel.read(buffer);//輸出緩存中數據System.out.println(new String(buffer.array()).trim());}//處理完關閉該通道iterator.remove();}}} }
BIO與AIO區別
BIO與NIO一個比較重要的不同,是我們使用BIO的時候往往會引入多線程,每個連接一個單獨的線程;
而NIO則是使用單線程或者只使用少量的多線程,每個連接共用一個線程。NIO的最重要的地方是當一個連接創建后,不需要對應一個線程,這個連接會被注冊到多路復用器上面,所以所有的連接只需要一個線程就可以搞定,當這個線程中的多路復用器進行輪詢的時候,發現連接上有請求的話,才開啟一個線程進行處理,也就是一個請求一個線程模式。
NIO比BIO最大的好處是,一個線程可以處理多個socket(channel),這樣NIO+多線程會提高網絡服務器的性能,最主要是大大降低線程的數量
服務器線程數量過多對系統有什么影響?
1.java里面創建進程和線程,最終映射到本地操作系統上創建進程和線程,拿Linux來說,fork(進程創建函數)和pthread_create(線程創建函數)都是重量級的函數,調用它們開銷很大
2.多線程隨著CPU的調度,會有上下文切換,如果線程過多,線程上下文切換的時間花費慢慢趨近或者大于線程本身執行指令的時間,那么CPU就完全被浪費掉了,大大降低了系統的性能
3.線程的開辟伴隨著線程私有內存的分配,如果線程數量過多,為線程運行準備的內存占去很多,真正能用來分配做業務處理的內存大大減少,系統運行不可靠
4.數量過多的線程,阻塞等待網絡事件發生,如果一瞬間客戶請求量比較大,系統會瞬間喚醒很多數量的線程,造成系統瞬間的內存使用率和CPU使用率居高不下,服務器系統不應該總是出現鋸齒狀的系統負載,內存使用率和CPU使用率應該持續的保證平順運行
總結
- 上一篇: String、StringBuffer、
- 下一篇: netty使用