傳統IO的種類linux
內核空間與用戶空間
設計模式
內核態與用戶態的切換安全
//當前線程處於用戶態 String str = "string"; int x = 2; //切換至內核態 FileOutputStream fop = new FileOutputStream(new File("a.txt")); OutputStreamWrite out = new OutputStreamWrite(fop, "GBK"); out.write("...."); out.append('\r\n'); out.close(); //用戶態 int y = x + 2;
socket通訊
服務器
- 服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
- 數據傳輸的過程:
創建鏈接後,TCP協議提供全雙工的通訊服務,可是通常的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。所以,服務器從accept()返回後馬上調用read(),讀socket就像讀管道同樣,若是沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。- 若是客戶端沒有更多的請求了,就調用close()關閉鏈接,就像寫端關閉的管道同樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了鏈接,也調用close()關閉鏈接。注意,任何一方調用close()後,鏈接的兩個傳輸方向都關閉,不能再發送數據了。若是一方調用shutdown()則鏈接處於半關閉狀態,仍可接收對方發來的數據。
public class EchoClient { public static int DEFAULT_PORT = 9999; public static void main(String[] args) throws IOException { int port; try { port = Integer.parseInt(args[0]); } catch(RuntimeException e) { port = DEFAULT_PORT; } Socket socket = new Socket("127.0.0.1", port); //鍵盤輸入 BufferedReader buff = new BufferedReader(new InputStreamReader(System.in)); //Socket輸出流,自動刷新 PrintWriter out = new PrintWriter(socket.getOutputStream(), true); //Socket輸入流,讀取服務端的數據並返回的大寫數據 BufferedReader buffin = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line = null; while((line = buff.readLine()) != null) { if("stop".equals(line)) { break; } out.println(line); // 讀取服務端返回的一行大寫數據 System.out.println(buffin.readLine()); } } }
也可以使用linux下的nc命令代替客戶端網絡
public class EchoServer { public static int DEFAULT_PORT = 9999; public static void main(String[] args){ int port; try { port = Integer.parseInt(args[0]); } catch(RuntimeException e){ port = DEFAULT_PORT; } try { ServerSocket serverSocket = new ServerSocket(port); Socket clientSocket = serverSocket.accept(); String ip = clientSocket.getInetAddress().getHostAddress(); System.out.println("port : " + port + '\t' + "ipaddress : " + ip); //server 輸出流對應client輸入流,反之亦然 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String inputline ; while((inputline = in.readLine()) != null){ System.out.println(inputline); out.println(inputline.toUpperCase()); } } catch (IOException e) { System.out.println("Exception caught when trying to listen on port" + port + "or listening for a connection"); e.printStackTrace(); } } }
同步與異步多線程
描述的是用戶線程與內核的交互方式或者說關注的是消息通訊機制:app
堵塞與非堵塞異步
關注的是用戶線程調用內核 I/O 操做時,用戶線程等待I/O操做完成前是否能作其餘的事情:socket
阻塞與非阻塞主要是程序(線程)等待消息通知時的狀態角度,同步與異步主要是從消息機制角度來講,這兩組概念組合爲四種狀況,下面舉幾個網上的例子:函數
IO操做發生時會經歷兩個階段:
下面簡單介紹常見的五種 I/O 模型:
本節中將recvfrom函數視爲系統調用。通常recvfrom函數的實現都有一個從應用程序進程中運行到內核中運行的切換,一段時間後再跟一個返回應用進程的切換。
阻塞 I/O
請求沒法當即完成則保持阻塞。
進程阻塞的整段時間是指從調用recvfrom函數開始到它返回的這段時間,當進程返回成功提示時,應用進程開始處理數據報。
非阻塞 I/O
I/O 複用(異步堵塞I/O)
I/O 多路複用會用到 select 或者 poll 函數,這兩個函數也會使進程阻塞,可是和阻塞 I/O 所不一樣的的,這兩個函數能夠同時阻塞多個 I/O 操做。並且能夠同時對多個讀操做,多個寫操做的 I/O 函數進行檢測,直到有數據可讀或可寫時,才真正調用 I/O 操做函數。
從流程上看,使用select函數進行I/O請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操做,效率更差。可是,使用select函數的優點是用戶能夠在一個線程內同時處理多個socket的I/O請求。用戶能夠註冊多個socket,而後不斷地調用select來讀取被激活的socket,達到在同一個線程內同時處理多個I/O請求的目的。而在同步阻塞模型中,必須經過多線程的方式才能達到這個目的。
I/O複用模型使用Reactor設計模式實現了這一機制。
調用select或poll函數的方法由一個用戶態線程負責輪詢多個socket,直到階段1的數據就緒,再通知實際的用戶線程執行階段2的複製操做。經過一個專職的用戶態線程執行非阻塞I/O輪詢,模擬實現階段1的異步化。
信號驅動I/O
首先,咱們容許socket進行信號驅動I/O,並經過調用sigaction來安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好後,進程會收到一個SIGIO信號,能夠在信號處理函數中調用recvfrom來讀取數據報,並通知主循環數據已準備好被處理,也能夠通知主循環,讓它來讀取數據報。
異步 I/O
調用 aio_read 函數,告訴內核描述字,緩衝區指針,緩衝區大小,文件偏移以及通知的方式,而後當即返回。當內核將數據拷貝到緩衝區後,再通知應用程序。
異步I/O模型使用Proactor設計模式實現了這一機制。
異步I/O模型告知內核:當整個過程(包括階段1和階段2)所有完成時,通知應用程序來讀數據。
幾種 I/O 模型的比較
前四種模型的區別是階段1不相同,階段2基本相同,都是將數據從內核拷貝到調用者的緩衝區。而異步 I/O 的兩個階段都不一樣於前四個模型。
同步 I/O 操做引發請求進程阻塞,直到 I/O 操做完成。異步 I/O 操做不引發請求進程阻塞。
關於這些模型的具體實現我打算放到Java I/O中進行討論。