最近工作中接觸到了Java網絡編程方面的東西SocketNIOMongoDB等也看了tomcat的源碼也加強了線程方面的知識也使用了MINA這樣的框架感覺獲益良多原本技術上的薄弱環節也在慢慢提高很多想寫的東西也在慢慢規劃整理無奈最近在籌備婚禮的事情顯得有些耽擱
想了很久決定先寫寫IO中經常被提到的概念——同步與異步阻塞與非阻塞以及在Java網絡編程中的簡單運用
想達到的目的有兩個
深入的理解同步與異步阻塞與非阻塞這看似爛大街的詞匯很多人已經習慣不停的說但卻說不出其中的所以然包括我
理解各種IO模型在Java網絡IO中的運用能夠根據不同的應用場景選擇合適的交互方式了解不同的交互方式對IO性能的影響
前提
首先先強調上下文下面提到了同步與異步阻塞與非阻塞的概念都是在IO的場合下它們在其它場合下有著不同的含義比如操作系統中通信技術上
然後借鑒下《Unix網絡編程卷》中的理論
IO操作中涉及的個主要對象為程序進程系統內核以讀操作為例當一個IO讀操作發生時通常經歷兩個步驟
等待數據准備
將數據從系統內核拷貝到操作進程中
例如在socket上的讀操作步驟會等到網絡數據包到達到達後會拷貝到系統內核的緩沖區步驟會將數據包從內核緩沖區拷貝到程序進程的緩沖區中
阻塞(blocking)與非阻塞(nonblocking)IO
IO的阻塞非阻塞主要表現在一個IO操作過程中如果有些操作很慢比如讀操作時需要准備數據那麼當前IO進程是否等待操作完成還是得知暫時不能操作後先去做別的事情?一直等待下去什麼事也不做直到完成這就是阻塞抽空做些別的事情這是非阻塞
非阻塞IO會在發出IO請求後立即得到回應即使數據包沒有准備好也會返回一個錯誤標識使得操作進程不會阻塞在那裡操作進程會通過多次請求的方式直到數據准備好返回成功的標識
想象一下下面兩種場景
A 小明和小剛兩個人都很耿直內向一天小明來找小剛借書小剛啊你那本XXX借我看看 於是小剛就去找書小明就等著找了半天找到了把書給了小明
B 小明和小剛兩個人都很活潑外向一天小明來找小剛借書嘿小剛你那本XXX借我看看 小剛說我得找一會小明就去打球去了過會又來這次書找到了把書給了小明
結論A是阻塞的B是非阻塞的
從CPU角度可以看出非阻塞明顯提高了CPU的利用率進程不會一直在那等待但是同樣也帶來了線程切換的增加增加的 CPU 使用時間能不能補償系統的切換成本需要好好評估
同步(synchronous)與異步(asynchronous)IO
先來看看正式點的定義POSIX標准將IO模型分為了兩種同步IO和異步IORichard Stevens在《Unix網絡編程卷》中也總結道
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
可以看出判斷同步和異步的標准在於一個IO操作直到完成是否導致程序進程的阻塞如果阻塞就是同步的沒有阻塞就是異步的這裡的IO操作指的是真實的IO操作也就是數據從內核拷貝到系統進程(讀)的過程
繼續前面借書的例子異步借書是這樣的
C 小明很懶一天小明來找小剛借書嘿小剛你那本XXX借我看看 小剛說我得找一會小明就出去打球了並且讓小剛如果找到了就把書拿給他小剛是個負責任的人找到了書送到了小明手上
A和B的借書方式都是同步的有人要問了B不是非阻塞嘛怎麼還是同步?
前面說了IO操作的個步驟准備數據和把數據從內核中拷貝到程序進程映射到這個例子書即是准備的數據小剛是內核小明是程序進程小剛把書給小明這是拷貝數據在B方式中小剛找書這段時間小明的確是沒閒著該干嘛干嘛但是小剛找到書把書給小明的這個過程也就是拷貝數據這個步驟小明還是得乖乖的回來候著小剛把書遞手上所以這裡就阻塞了根據上面的定義所以是同步
在涉及到 IO 處理時通常都會遇到一個是同步還是異步的處理方式的選擇問題同步能夠保證程序的可靠性而異步可以提升程序的性能小明自己去取書不管等著不等著遲早拿到書指望小剛找到了送來萬一小剛忘了或者有急事忙別的了那書就沒了
討論
說實話網上關於同步與異步阻塞與非阻塞的文章多之又多大部分是拷貝的也有些寫的非常好的參考了許多也借鑒了許多也經過自己的思考
同步與異步阻塞與非阻塞之間確實有很多相似的地方很容易混淆wiki更是把異步與非阻塞畫上了等號更多的人還是認為他們是不同的原因可能有很多每個人的知識背景不同設定的上下文也不同
我的看法是在IO中根據上面同步異步的概念也可以看出來同步與異步往往是通過阻塞非阻塞的形式來表達的並且是通過一種中間處理機制來達到異步的效果同步與異步往往是IO操作請求者和回應者之間在IO實際操作階段的協作方式而阻塞非阻塞更確切的說是一種自身狀態當前進程或者線程的狀態
在發出IO讀請求後阻塞IO會一直等待有數據可讀當有數據可讀時會等待數據從內核拷貝至系統進程而非阻塞IO都會立即返回至於數據怎麼處理是程序進程自己的事情無關同步和異步
兩種方式的組合
組合的方式當然有四種分別是同步阻塞同步非阻塞異步阻塞異步非阻塞
Java網絡IO實現和IO模型
不同的操作系統上有不同的IO模型《Unix網絡編程卷》將unix上的IO模型分為類blocking I/Ononblocking I/OI/O multiplexing (select and poll)signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)具體可參考《Unix網絡編程卷》章節
在windows上IO模型也是有種select WSAAsyncSelectWSAEventSelectOverlapped I/O 事件通知以及IOCP具體可參考windows五種IO模型
Java是平台無關的語言在不同的平台上會調用底層操作系統的不同的IO實現下面就來說一下Java提供的網絡IO的工具和實現為了擴大阻塞非阻塞的直觀感受我都使用了長連接
阻塞IO
同步阻塞最常用的一種用法使用也是最簡單的但是 I/O 性能一般很差CPU 大部分在空閒狀態下面是一個簡單的基於TCP的同步阻塞的Socket服務端例子
@Test
public void testJIoSocket() throws Exception
{
ServerSocket serverSocket = new ServerSocket();
Socket socket = null;
try
{
while (true)
{
socket = serverSocketaccept();
Systemoutprintln(socket連接 + socketgetRemoteSocketAddress()toString());
BufferedReader in = new BufferedReader(new InputStreamReader(socketgetInputStream()));
while(true) {
String readLine = inreadLine();
Systemoutprintln(收到消息 + readLine);
if(endequals(readLine))
{
break; }
//客戶端斷開連接 socketsendUrgentData(xFF);
}
}
}
catch (SocketException se)
{ Systemoutprintln(客戶端斷開連接);
}
catch (IOException e)
{
eprintStackTrace();
}
finally
{
Systemoutprintln(socket關閉 + socketgetRemoteSocketAddress()toString());
socketclose();
}
}
使用SocketTest作為客戶端工具進行測試同時開啟個客戶端連接Server端並發送消息如下圖
再看下後台的打印
socket連接
/
:
收到消息hello!收到消息my name is client
由於服務器端是單線程的
在第一個連接的客戶端阻塞了線程後
第二個客戶端必須等待第一個斷開後才能連接
當輸入
end
字符串斷開客戶端
這時候看到後台繼續打印
socket連接
/
:
收到消息hello!收到消息my name is client
收到消息endsocket關閉
/
:
socket連接
/
:
收到消息hello!收到消息my name is client
所有的客戶端連接在請求服務端時都會阻塞住
等待前面的完成
即使是使用短連接
數據在寫入 OutputStream 或者從 InputStream 讀取時都有可能會阻塞
這在大規模的訪問量或者系統對性能有要求的時候是不能接受的
阻塞IO + 每個請求創建線程/線程池
通常解決這個問題的方法是使用多線程技術
一個客戶端一個處理線程
出現阻塞時只是一個線程阻塞而不會影響其它線程工作
為了減少系統線程的開銷
采用線程池的辦法來減少線程創建和回收的成本
模式如下圖
簡單的實現例子如下
使用一個線程(Accptor)接收客戶端請求
為每個客戶端新建線程進行處理(Processor)
線程池的我就不弄了
public class MultithreadJIoSocketTest{
@Test
public void testMultithreadJIoSocket() throws Exception
{
ServerSocket serverSocket = new ServerSocket(
)
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(
xFF)
Thread
sleep(
)
}
}
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()
}
}
}
}}
使用
個客戶端連接
這次沒有阻塞
成功的收到了
個客戶端的消息
收到了socket:/
:
收到了socket:/
:
收到消息hello!收到消息hello!
在單個線程處理中
我人為的使單個線程read後阻塞
秒
就像前面說的
出現阻塞也只是在單個線程中
沒有影響到另一個客戶端的處理
這種阻塞IO的解決方案在大部分情況下是適用的
在出現NIO之前是最通常的解決方案
Tomcat裡阻塞IO的實現就是這種方式
但是如果是大量的長連接請求呢?不可能創建幾百萬個線程保持連接
再退一步
就算線程數不是問題
如果這些線程都需要訪問服務端的某些競爭資源
勢必需要進行同步操作
這本身就是得不償失的
非阻塞IO + IO multiplexing
Java從
開始提供了NIO工具包
這是一種不同於傳統流IO的新的IO方式
使得Java開始對非阻塞IO支持
NIO並不等同於非阻塞IO
只要設置Blocking屬性就可以控制阻塞非阻塞
至於NIO的工作方式特點原理這裡一概不說
以後會寫
模式如下圖
下面是簡單的實現
public class NioNonBlockingSelectorTest{
Selector selector;
private ByteBuffer receivebuffer = ByteBuffer
allocate(
)
@Test
public void testNioNonBlockingSelector()
throws Exception
{
selector = Selector
open()
SocketAddress address = new InetSocketAddress(
)
ServerSocketChannel channel = ServerSocketChannel
open()
channel
socket()
bind(address)
nfigureBlocking(false)
channel
register(selector
SelectionKey
OP_ACCEPT)
while(true)
{
selector
select()
Iterator 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())
nfigureBlocking(false)
client
register(selector
SelectionKey
OP_READ)
}
if(selectionKey
isReadable())
{
client = (SocketChannel)selectionKey
channel()
receivebuffer
clear()
int count = client
read(receivebuffer)
if (count >
) {
String receiveText = new String( receivebuffer
array()
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
Java
中提供了異步IO的支持
暫時還沒有看過
所以以後再討論
網絡IO優化
對於網絡IO有一些基本的處理規則如下
減少交互的次數
比如增加緩存
合並請求
減少傳輸數據大小
比如壓縮後傳輸
約定合理的數據協議
減少編碼
比如提前將字符轉化為字節再傳輸
根據應用場景選擇合適的交互方式
同步阻塞
同步非阻塞
異步阻塞
異步非阻塞
就說到這裡吧
感覺有點亂
有些地方還是找不到更貼切的語言來描述
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26001.html