最近工做中,接觸到了Java網絡編程方面的東西:Socket、NIO、MongoDB等,也看了tomcat的源碼,也增強了線程方面的知識,也使用了MINA這樣的框架。感受獲益良多,本來技術上的薄弱環節也在慢慢提升,不少想寫的東西,也在慢慢規劃整理。 html
想了好久,決定先寫寫IO中常常被提到的概念——「同步與異步、阻塞與非阻塞」以及在Java網絡編程中的簡單運用。 linux
想達到的目的有兩個: 編程
1。深刻的理解同步與異步、阻塞與非阻塞,這看似爛大街的詞彙不少人已經習慣不停的說,但卻說不出其中的因此然,包括我。 windows
2。理解各類IO模型在Java網絡IO中的運用,可以根據不一樣的應用場景選擇合適的交互方式。瞭解不一樣的交互方式對IO性能的影響。 緩存
前提
tomcat
首先先強調上下文:下面提到了同步與異步、阻塞與非阻塞的概念都是在IO的場合下。它們在其它場合下有着不一樣的含義,好比操做系統中,通訊技術上。 服務器
而後借鑑下《Unix網絡編程卷》中的理論: 網絡
IO操做中涉及的2個主要對象爲程序進程、系統內核。以讀操做爲例,當一個IO讀操做發生時,一般經歷兩個步驟: 多線程
1,等待數據準備 併發
2,將數據從系統內核拷貝到操做進程中
例如,在socket上的讀操做,步驟1會等到網絡數據包到達,到達後會拷貝到系統內核的緩衝區;步驟2會將數據包從內核緩衝區拷貝到程序進程的緩衝區中。
阻塞(blocking)與非阻塞(non-blocking)IO
IO的阻塞、非阻塞主要表如今一個IO操做過程當中,若是有些操做很慢,好比讀操做時須要準備數據,那麼當前IO進程是否等待操做完成,仍是得知暫時不能操做後先去作別的事情?一直等待下去,什麼事也不作直到完成,這就是阻塞。抽空作些別的事情,這是非阻塞。
非阻塞IO會在發出IO請求後當即獲得迴應,即便數據包沒有準備好,也會返回一個錯誤標識,使得操做進程不會阻塞在那裏。操做進程會經過屢次請求的方式直到數據準備好,返回成功的標識。
想象一下下面兩種場景:
A 小明和小剛兩我的都很耿直內向,一天小明來找小剛借書:「小剛啊,你那本XXX借我看看」。 因而小剛就去找書,小明就等着,找了半天找到了,把書給了小明。
B 小明和小剛兩我的都很活潑外向,一天小明來找小剛借書:「嘿小剛,你那本XXX借我看看」。 小剛說:「我得找一會」,小明就去打球去了。過會又來,此次書找到了,把書給了小明。
結論:A是阻塞的,B是非阻塞的。
從CPU角度能夠看出非阻塞明顯提升了CPU的利用率,進程不會一直在那等待。可是一樣也帶來了線程切換的增長。增長的 CPU 使用時間能不能補償系統的切換成本須要好好評估。
同步(synchronous)與異步(asynchronous)IO
先來看看正式點的定義,POSIX標準將IO模型分爲了兩種:同步IO和異步IO,Richard Stevens在《Unix網絡編程卷》中也總結道:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
能夠看出,判斷同步和異步的標準在於:一個IO操做直到完成,是否致使程序進程的阻塞。若是阻塞就是同步的,沒有阻塞就是異步的。這裏的IO操做指的是真實的IO操做,也就是數據從內核拷貝到系統進程(讀)的過程。
繼續前面借書的例子,異步借書是這樣的:
C 小明很懶,一天小明來找小剛借書:「嘿小剛,你那本XXX借我看看」。 小剛說:「我得找一會」,小明就出去打球了而且讓小剛若是找到了就把書拿給他。小剛是個負責任的人,找到了書送到了小明手上。
A和B的借書方式都是同步的,有人要問了B不是非阻塞嘛,怎麼仍是同步?
前面說了IO操做的2個步驟:準備數據和把數據從內核中拷貝到程序進程。映射到這個例子,書便是準備的數據,小剛是內核,小明是程序進程,小剛把書給小明這是拷貝數據。在B方式中,小剛找書這段時間小明的確是沒閒着,該幹嗎幹嗎,可是小剛找到書把書給小明的這個過程也就是拷貝數據這個步驟,小明仍是得乖乖的回來候着小剛把書遞手上。因此這裏就阻塞了,根據上面的定義,因此是同步。
在涉及到 IO 處理時一般都會遇到一個是同步仍是異步的處理方式的選擇問題。同步可以保證程序的可靠性,而異步能夠提高程序的性能。小明本身去取書無論等着不等着早晚拿到書,期望小剛找到了送來,萬一小剛忘了或者有急事忙別的了,那書就沒了。
討論
說實話,網上關於同步與異步、阻塞與非阻塞的文章多之又多,大部分是拷貝的,也有些寫的很是好的。參考了許多,也借鑑了許多,也通過本身的思考。
同步與異步、阻塞與非阻塞之間確實有不少類似的地方,很容易混淆。wiki更是把異步與非阻塞畫上了等號,更多的人仍是認爲他們是不一樣的。緣由可能有不少,每一個人的知識背景不一樣,設定的上下文也不一樣。
個人見解是:在IO中,根據上面同步異步的概念,也能夠看出來同步與異步每每是經過阻塞非阻塞的形式來表達的,而且是經過一種中間處理機制來達到異步的效果。同步與異步每每是IO操做請求者和迴應者之間在IO實際操做階段的協做方式,而阻塞非阻塞更確切的說是一種自身狀態,當前進程或者線程的狀態。
在發出IO讀請求後,阻塞IO會一直等待有數據可讀,當有數據可讀時,會等待數據從內核拷貝至系統進程;而非阻塞IO都會當即返回。至於數據怎麼處理是程序進程本身的事情,無關同步和異步。
兩種方式的組合
組合的方式固然有四種,分別是:同步阻塞、同步非阻塞、異步阻塞、異步非阻塞。
Java網絡IO實現和IO模型
不一樣的操做系統上有不一樣的IO模型,《Unix網絡編程卷》將unix上的IO模型分爲5類:blocking I/O、nonblocking I/O、I/O multiplexing (select and poll)、signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)。具體可參考《Unix網絡編程卷1》6.2章節。
在windows上IO模型也是有5種:select 、WSAAsyncSelect、WSAEventSelect、Overlapped I/O 事件通知以及IOCP。具體可參考windows五種IO模型。
Java是平臺無關的語言,在不一樣的平臺上會調用底層操做系統的不一樣的IO實現,下面就來講一下Java提供的網絡IO的工具和實現,爲了擴大阻塞非阻塞的直觀感覺,我都使用了長鏈接。
阻塞IO
同步阻塞最經常使用的一種用法,使用也是最簡單的,可是 I/O 性能通常不好,CPU 大部分在空閒狀態。下面是一個簡單的基於TCP的同步阻塞的Socket服務端例子:
1 @Test 2 public void testJIoSocket() throws Exception 3 { 4 ServerSocket serverSocket = new ServerSocket(10002); 5 Socket socket = null; 6 try 7 { 8 while (true) 9 { 10 socket = serverSocket.accept(); 11 System.out.println("socket鏈接:" + socket.getRemoteSocketAddress().toString()); 12 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); 13 while(true) 14 { 15 String readLine = in.readLine(); 16 System.out.println("收到消息" + readLine); 17 if("end".equals(readLine)) 18 { 19 break; 20 } 21 //客戶端斷開鏈接 22 socket.sendUrgentData(0xFF); 23 } 24 } 25 } 26 catch (SocketException se) 27 { 28 System.out.println("客戶端斷開鏈接"); 29 } 30 catch (IOException e) 31 { 32 e.printStackTrace(); 33 } 34 finally 35 { 36 System.out.println("socket關閉:" + socket.getRemoteSocketAddress().toString()); 37 socket.close(); 38 } 39 }
使用SocketTest做爲客戶端工具進行測試,同時開啓2個客戶端鏈接Server端併發送消息,以下圖:
再看下後臺的打印
socket鏈接:/127.0.0.1:54080 收到消息hello! 收到消息my name is client1
因爲服務器端是單線程的,在第一個鏈接的客戶端阻塞了線程後,第二個客戶端必須等待第一個斷開後才能鏈接。當輸入「end」字符串斷開客戶端1,這時候看到後臺繼續打印:
socket鏈接:/127.0.0.1:54080 收到消息hello! 收到消息my name is client1 收到消息end socket關閉:/127.0.0.1:54080 socket鏈接:/127.0.0.1:54091 收到消息hello! 收到消息my name is client2
全部的客戶端鏈接在請求服務端時都會阻塞住,等待前面的完成。即便是使用短鏈接,數據在寫入 OutputStream 或者從 InputStream 讀取時都有可能會阻塞。這在大規模的訪問量或者系統對性能有要求的時候是不能接受的。
阻塞IO + 每一個請求建立線程/線程池
一般解決這個問題的方法是使用多線程技術,一個客戶端一個處理線程,出現阻塞時只是一個線程阻塞而不會影響其它線程工做;爲了減小系統線程的開銷,採用線程池的辦法來減小線程建立和回收的成本,模式以下圖:
簡單的實現例子以下,使用一個線程(Accptor)接收客戶端請求,爲每一個客戶端新建線程進行處理(Processor),線程池的我就不弄了:
public class MultithreadJIoSocketTest { @Test public void testMultithreadJIoSocket() throws Exception { ServerSocket serverSocket = new ServerSocket(10002); Thread thread = new Thread(new Accptor(serverSocket)); thread.start(); Scanner scanner = new Scanner(System.in); scanner.next(); } public class Accptor implements Runnable { private ServerSocket serverSocket; public Accptor(ServerSocket serverSocket) { this.serverSocket = serverSocket; } public void run() { while (true) { Socket socket = null; try { socket = serverSocket.accept(); if(socket != null) { System.out.println("收到了socket:" + socket.getRemoteSocketAddress().toString()); Thread thread = new Thread(new Processor(socket)); thread.start(); } } catch (IOException e) { e.printStackTrace(); } } } } public class Processor implements Runnable { private Socket socket; public Processor(Socket socket) { this.socket = socket; } @Override public void run() { try { BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String readLine; while(true) { readLine = in.readLine(); System.out.println("收到消息" + readLine); if("end".equals(readLine)) { break; } //客戶端斷開鏈接 socket.sendUrgentData(0xFF); Thread.sleep(5000); } } catch (InterruptedException e) { e.printStackTrace(); } catch (SocketException se) { System.out.println("客戶端斷開鏈接"); } catch (IOException e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
使用2個客戶端鏈接,此次沒有阻塞,成功的收到了2個客戶端的消息。
收到了socket:/127.0.0.1:55707 收到了socket:/127.0.0.1:55708 收到消息hello! 收到消息hello!
在單個線程處理中,我人爲的使單個線程read後阻塞5秒,就像前面說的,出現阻塞也只是在單個線程中,沒有影響到另外一個客戶端的處理。
這種阻塞IO的解決方案在大部分狀況下是適用的,在出現NIO以前是最一般的解決方案,Tomcat裏阻塞IO的實現就是這種方式。可是若是是大量的長鏈接請求呢?不可能建立幾百萬個線程保持鏈接。再退一步,就算線程數不是問題,若是這些線程都須要訪問服務端的某些競爭資源,勢必須要進行同步操做,這自己就是得不償失的。
非阻塞IO + IO multiplexing
Java從1.4開始提供了NIO工具包,這是一種不一樣於傳統流IO的新的IO方式,使得Java開始對非阻塞IO支持;NIO並不等同於非阻塞IO,只要設置Blocking屬性就能夠控制阻塞非阻塞。至於NIO的工做方式特色原理這裏一律不說,之後會寫。模式以下圖:
下面是簡單的實現:
public class NioNonBlockingSelectorTest { Selector selector; private ByteBuffer receivebuffer = ByteBuffer.allocate(1024); @Test public void testNioNonBlockingSelector() throws Exception { selector = Selector.open(); SocketAddress address = new InetSocketAddress(10002); ServerSocketChannel channel = ServerSocketChannel.open(); channel.socket().bind(address); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_ACCEPT); while(true) { selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); iterator.remove(); handleKey(selectionKey); } } } private void handleKey(SelectionKey selectionKey) throws IOException { ServerSocketChannel server = null; SocketChannel client = null; if(selectionKey.isAcceptable()) { server = (ServerSocketChannel)selectionKey.channel(); client = server.accept(); System.out.println("客戶端: " + client.socket().getRemoteSocketAddress().toString()); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } if(selectionKey.isReadable()) { client = (SocketChannel)selectionKey.channel(); receivebuffer.clear(); int count = client.read(receivebuffer); if (count > 0) { String receiveText = new String( receivebuffer.array(),0,count); System.out.println("服務器端接受客戶端數據--:" + receiveText); client.register(selector, SelectionKey.OP_READ); } } } }
Java NIO提供的非阻塞IO並非單純的非阻塞IO模式,而是創建在Reactor模式上的IO複用模型;在IO multiplexing Model中,對於每個socket,通常都設置成爲non-blocking,可是整個用戶進程實際上是一直被阻塞的。只不過進程是被select這個函數阻塞,而不是被socket IO給阻塞,因此仍是屬於非阻塞的IO。
這篇文章中把這種模式歸爲了異步阻塞,我實際上是認爲這是同步非阻塞的,可能看的角度不同。
異步IO
Java1.7中提供了異步IO的支持,暫時尚未看過,因此之後再討論。
網絡IO優化
對於網絡IO有一些基本的處理規則以下:
1。減小交互的次數。好比增長緩存,合併請求。
2。減小傳輸數據大小。好比壓縮後傳輸、約定合理的數據協議。
3。減小編碼。好比提早將字符轉化爲字節再傳輸。
4。根據應用場景選擇合適的交互方式,同步阻塞,同步非阻塞,異步阻塞,異步非阻塞。
就說到這裏吧,感受有點亂,有些地方仍是找不到更貼切的語言來描述。