目錄
1. Single Thread Socket Server
2. Multiple Thread Socket Server
一.Linux IO模型
在《Unix網絡編程》這本書中將IO模型劃分爲以下五種:
- 阻塞式IO模型Blocking IO
- 非阻塞式IO模型Non-Blocking IO/New IO
- IO複用
- 信號驅動式IO模型
- 異步IO模型Asynchronous IO
其中前四種都是同步模式。
二. 瞭解Linux IO流程
在Linux中運行的應用程序如果需要進行IO操作,需要涉及到兩個空間的概念,用戶空間和內核空間。
上圖表示的是一個數據讀取的過程:
- DMA先從將磁盤數據拷貝到內核空間,該過程也稱爲數據準備過程;
- 應用程序拷貝內核空間數據到用戶空間。
- 寫數據的過程與之相反。
從應用程序角度來看,分爲兩步:
- 等待內核將數據準備好(Waiting for the data to be ready)
- 從內核向進程複製數據(Copying the data from the kernel to the process)
三. 各IO模型執行過程
(一)阻塞式IO模型
應用程序發起一次系統調用,即IO請求,詢問內核數據是否準備好,發現沒有,則進程一直等待數據準備好爲止;在接收到系統調用後內核開始準備數據報,準備好後進行內核態到用戶態的數據拷貝,拷貝完成後,返回準備就緒信息給調用進程。
(二)非阻塞式IO模型
應用進程輪詢詢問內核數據是否準備好。當有數據報準備好時就進行數據報的拷貝操作,當沒有準備好時,內核直接返回未準備就緒的信號,不讓進程阻塞,並且開始準備數據,等待進程下一次詢問。
(三)IO多路複用模型
IO多路複用模型會通過一個select函數對多個文件描述符(集合)進行循環監聽,當某個文件描述符就緒時,就對這個文件描述符的數據進行處理。
(四)信號驅動式IO模型
應用程序通知內核,當數據準備就緒時給它發送一個SIGIO信號,應用程序會對這個信號進行捕捉,並且調用相應的信號處理函數執行數據拷貝的過程。
(五)異步IO模型
當應用程序調用aio_read時,內核一邊開始準備數據,另一邊將程序控制權返回給應用進程,讓應用進程處理其他事情;當內核中有數據報準備就緒時,由內核將數據報拷貝到用戶空間,這也是與其他四種模型的最大區別,拷貝完成後返回aio_read中定義好的函數處理程序。
(六)各IO模型對比
從下圖中可以看出阻塞程度是:阻塞式IO >非阻塞式IO > IO複用>信號驅動式IO >異步IO。
四. BIO下的Socket處理方式
下圖是Socket和ServerSocket通信模型:
1. Single Thread Socket Server
public void startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Listening for connection on port 8080...");
while (!Thread.interrupted()) {
final Socket socket = serverSocket.accept();
// 1. Read request from the socket of client.
// 2. Prepare a response.
// 3. Send response to the client.
// 4. Close the socket.
}
}
2. Multiple Thread Socket Server
public void startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Listening for connection on port 8080...");
while (!Thread.interrupted()) {
final Socket socket = serverSocket.accept();
new Thread(() -> {
// 1. Read request from the socket of client.
// 2. Prepare a response.
// 3. Send response to the client.
// 4. Close the socket.
}).start();
}
}
3. Thread Pool Socket Server
public void startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(8080);
// 爲了代碼簡潔,這裏直接通過工具類創建一個線程池
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("Listening for connection on port 8080...");
while (!Thread.interrupted()) {
final Socket socket = serverSocket.accept();
executor.execute(() -> {
// 1. Read request from the socket of client.
// 2. Prepare a response.
// 3. Send response to the client.
// 4. Close the socket.
});
}
}
五. Java NIO
Java IO和Java NIO的區別
Java NIO三個關鍵對象
- Buffer
- Channel
- Selector
(一)Java NIO Buffer
1. 一個Buffer本質上是內存的一個內存塊,允許對這塊內存進行數據讀寫操作,在Java NIO中定義了以下幾種Buffer實現:
2. Java NIO的Buffer主要有三個核心屬性:
(1)Capacity:
緩衝區容量,一旦設定就不可更改,比如capacity爲1024的IntBuffer,代表其最大可以存放1024個int類型的數據。
(2)Position:
記錄下一個可操作(可讀/可寫)地址。
(3)Limit:
Java Buffer是通過一個指針來維護讀寫操作的,從寫操作模式切換到讀操作模式,position都會歸零,這些可以保證從頭開始讀寫,讀寫模式切換必須使用flip方法。
在初始化後默認是寫操作模式,此時limit代表的是最大能寫入數據,即limit = capacity;
最大能寫入的數據,初始狀態下limit = capacity。
寫操作結束後,flip切換到讀模式下,此時limit等於Buffer中實際寫入數據的大小,比如在寫模式下寫入了10個int類型的數據,那麼此時limit=10。
(4)Mark:
標記當前position位置,在執行reset方法後將pisition恢復到標記位置,mark的位置必須小於等於position,它們之間的關係:0<=mark<=position<=limit<=capacity
3. ByteBuffer實現
Java NIO中ByteBuffer有兩種具體實現Direct ByteBuffer和Heap ByteBuffer,其中HeapByteBuffer也被認爲是Non-Direct ByteBuffer。
|
DirectByteBuffer |
HeapByteBuffer |
創建開銷 |
大 |
小 |
存儲位置 |
Native heap |
JVM heap 維護一個字節數組byte[] |
數據拷貝 |
無需臨時緩衝區做拷貝 |
先拷貝到DirectByteBuffer類型的臨時緩衝區,並且這個緩衝區具有緩存功能。 |
GC影響 |
每次創建或者釋放時都調用一次System.gc() |
無 |
4. Buffer操作方法
方法名稱 |
作用 |
allocate |
創建一個Heap ByteBuffer類型的緩衝區 |
allocateDirect |
創建一個Direct ByteBuffer類型的緩衝區 |
wrap |
通過外部傳入一個數組創建Heap ByteBuffer類型的緩衝區 |
flip |
切換讀寫操作模式 |
put |
將單個字節寫入緩衝區position位置 |
get |
從緩衝區中讀取單個字節 |
mark |
標記當前position |
reset |
恢復position到mark標記的位置 |
rewind |
將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變。 |
clear |
清空緩存區,這裏的清空並不是清除緩衝區數據,而是position被重置爲0,limit被重置爲capacity。 |
compact |
Compact與clear不同,compact是將所有未讀的數據拷貝到Buffer起始處。然後將position設到最後一個未讀數據下一位。limit屬性依然像clear()方法一樣設置成capacity。 |
remaining |
計算並返回Buffer緩衝區中剩餘數據大小 |
(二)Channel
所有的NIO操作都是基於通道Channel的,通道是數據來源和數據寫入的目標,Java NIO中主要實現了以下幾類Channel:
- FileChannel:文件通道,用於文件讀寫
- SocketChannel:可以將它理解爲TCP協議方式的連接通道,也可以簡單理解爲TCP客戶端
- ServerSocketChannel:TCP協議方式的服務端,用於監聽具體端口的數據接受請求
- DatagramChannel:用於基於UDP協議連接的數據接收和發送
(三)Selector
Selector是Java NIO規範中十分重要的組件,它的作用是用來檢查一個或多個NIO Channel通道的狀態是否處於可讀或可寫,實現了單線程可以管理多個Channel的目標。
六. 代碼實例
package com.zjhuang.socket.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* 實現Java NIO處理socket請求
*
* @author
* @create 2018/11/20 19:21
**/
public class NioSocketServer implements Runnable {
/**
* 選擇器
*/
private Selector selector;
/**
* Server端Socket通道
*/
private ServerSocketChannel channel;
public NioSocketServer(int port) throws IOException {
// 創建選擇器
selector = Selector.open();
// 打開一個server端socket通道
channel = ServerSocketChannel.open();
// 設置爲non-blocking模式
channel.configureBlocking(false);
// 通道綁定到指定端口
channel.socket().bind(new InetSocketAddress(port), 1024);
// 將通道註冊到selector上,並且對Accept事件感興趣
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("正在監聽" + port + "端口請求...");
}
@Override
public void run() {
while (!Thread.interrupted()) {
try {
// 檢查就緒的通道
if (selector.select(1000) == 0) {
continue;
}
// 返回就緒通道集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍歷集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
handle(key);
// 移除已處理通道
iterator.remove();
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (null != selector) {
try {
// 關閉Selector,並使註冊到該Selector上的所有SelectionKey實例無效
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 處理通道的讀寫事件
*
* @param key SelectionKey對象
* @throws IOException
*/
private void handle(SelectionKey key) throws IOException {
// 判斷新接入的通道是否有效
if (key.isValid()) {
if (key.isAcceptable()) {
// 當通道就緒時註冊一個新的socketChannel,並對讀取事件感興趣
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
if (readBytes == -1) {
socketChannel.close();
key.cancel();
} else {
// 切換到讀模式
readBuffer.flip();
// 創建一個readBuffer鍾剩餘數據大小的字節數組
byte[] bytes = new byte[readBuffer.remaining()];
// 從readBuffer中讀取指定大小的數據到字節數組中
readBuffer.get(bytes);
// 打印接收到的數據
System.out.println("服務端接收到數據:" + new String(bytes, "UTF-8"));
// 回顯數據
socketChannel.write(ByteBuffer.wrap("success\r\n".getBytes()));
}
}
}
}
public static void main(String[] args) throws Exception {
new Thread(new NioSocketServer(8080)).start();
}
}