章節概覽
1、概述
關於多路複用的基本原理,在大白話分析BIO,NIO,AIO中簡單的介紹了關於多路複用技術的理解。這章節,我們深入理解分析多路複用技術。以及JDK的部分源碼作爲參考。
2、多路複用快速認知
爲了快速理解多路複用技術,我們以生活中的小案例進行說明。老張開大排檔,剛剛起步的時候,客人比較少。接待,炒菜,上菜都是老張一個人負責。老張的手藝不錯,炒出來的菜味道可以。客人越來越多,每來個客人,老張都得花時間去接待,忙不過來。於是老張就招了服務員,服務員收集每桌需要點的菜,然後把菜單交給老張,老張只負責做菜即可。在這裏,服務員就充當了選擇器,客戶把自己的要求告訴服務員,服務員告訴老張。
3、深入理解Linux底層epoll的實現原理
首先我們觀察下Linux底層epoll的3個實現函數:
- int epoll_create(int size);
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epoll_create:創建一個epoll對象。參數size是內核保證能處理最大的文件句柄數,在socket編程裏面就是處理的最大連接數。返回的int代表當前的句柄指針,當然創建一個epoll對象的時候,也會相應的消耗一個fd,所以在使用完成的時候,一定要關閉,不然會耗費大量的文件句柄資源。
epoll_ctl:可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。其中epfd,就是創建的文件句柄指針,op是要做的操作,例如刪除,更新等,event 就是我們需要監控的事件。
epoll_wait:在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。
epoll的高效就在於,當我們調用epoll_ctl往裏塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄發送給用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。
那麼,這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。(當網卡里面有數據的時候,會發起硬件中斷,提醒內核有數據到來可以拷貝數據。當網卡通知內核有數據的時候,會產生一個回調函數,這個回調函數是epoll_ctl創建的時候,向內核裏面註冊的。回調函數會把當前有數據的socket(文件句柄)取出,放到list列表中。這樣就可以把存放着數據的socket發送給用戶態,減少遍歷的時間,和數據的拷貝)
4、java NIO 編程詳解
4.1 NIOClient
public class NIOClient {
/*標識數字*/
private static int flag = 0;
/*緩衝區大小*/
private static int BLOCK = 4096;
/*接受數據緩衝區*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*發送數據緩衝區*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*服務器端地址*/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 8888);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
// 打開socket通道
SocketChannel socketChannel = SocketChannel.open();
// 設置爲非阻塞方式
socketChannel.configureBlocking(false);
// 打開選擇器
Selector selector = Selector.open();
// 註冊連接服務端socket動作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 連接
socketChannel.connect(SERVER_ADDRESS);
// 分配緩衝區大小內存
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。
//此方法執行處於阻塞模式的選擇操作。
selector.select();
//返回此選擇器的已選擇鍵集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判斷此通道上是否正在進行連接操作。
// 完成套接字通道的連接過程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成連接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//將緩衝區清空以備下次讀取
receivebuffer.clear();
//讀取服務器發送來的數據到緩衝區中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客戶端接受服務器端數據--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
//將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客戶端向服務器端發送數據--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
4.2、NIOServer
public class NIOServer {
/*標識數字*/
private int flag = 0;
/*緩衝區大小*/
private int BLOCK = 4096;
/*接受數據緩衝區*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*發送數據緩衝區*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
public NIOServer(int port) throws IOException {
/**
* 以下的所有說明均已linux系統底層進行說明:
* nio 的底層實現是 epoll 模式,採用多路複用技術,對nio的代碼進行深入分析,結合epoll的底層實現
* 進行詳細的說明
* 1.linux網絡編程是兩個進程之間的通信,跨集羣合網絡
* 2.開啓一個socket線程,在linux系統上任何操作均以文件句柄數表示,默認情況下
* 一個線程可以打開1024個句柄,也就說最多同時支持1024個網絡連接請求。阿里雲默認打開65535個文件
* 句柄,通常情況下,1G內存最多可以打開10w個句柄數
*
*
*/
// 打開服務器套接字通道
// 底層: 在linux上面開啓socket服務,啓動一個線程。綁定ip地址和端口號
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服務器配置爲非阻塞
serverSocketChannel.configureBlocking(false);
// 檢索與此通道關聯的服務器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 進行服務的綁定
serverSocket.bind(new InetSocketAddress(port));
// 通過open()方法找到Selector
// 底層: 開啓epoll,爲當前socket服務創建epoll服務,epoll_create
selector = Selector.open();
// 註冊到selector,等待連接
/**
* 底層:
* 1.將當前的epoll,服務器地址,端口號綁定,如果有連接請求,直接添加到epoll中,epoll的底層是紅黑樹,
* 可以快速的實現連接的查找和狀態更新。如果有新的連接過來,直接存放到epoll中。如果有連接過期,中斷,
* 會從epoll中刪除。
* 2.通過epoll_ctl添加到epoll的同時,會註冊一個回調函數給內核,當網卡有數據來的時候,會通知內核,內核
* 調用回調函數,將當前內核數據的事件狀態添加到list鏈表中
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----8888:");
}
// 監聽
private void listen() throws IOException {
while (true) {
// 選擇一組鍵,並且相應的通道已經打開
/**
* epoll底層維護一個鏈表,rdlist,基於事件驅動模式,當網卡有數據請求過來,會發起硬件中斷,通知內核已經有來了。內核調用
* 回調函數,將當前的事件添加到rdlist中,將當前可用的rdlist列表發送給用戶態,用戶去遍歷rdlist中的事件,進行處理
*/
selector.select();
// 返回此選擇器的已選擇鍵集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 獲得當前epoll的rdlist複製到用戶態,遍歷,同事刪除當前rdlist中的事件
iterator.remove();
handleKey(selectionKey);
}
}
}
// 處理請求
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受請求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count=0;
// 測試此鍵的通道是否已準備好接受新的套接字連接。
if (selectionKey.isAcceptable()) {
// 返回爲之創建此鍵的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的連接。
// 此方法返回的套接字通道(如果有)將處於阻塞模式。
client = server.accept();
// 配置爲非阻塞
client.configureBlocking(false);
// 註冊到selector,等待連接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 返回爲之創建此鍵的通道。
client = (SocketChannel) selectionKey.channel();
//將緩衝區清空以備下次讀取
receivebuffer.clear();
//讀取服務器發送來的數據到緩衝區中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服務器端接受客戶端數據--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//將緩衝區清空以備下次寫入
sendbuffer.clear();
// 返回爲之創建此鍵的通道。
client = (SocketChannel) selectionKey.channel();
sendText="message from server--" + flag++;
//向緩衝區中輸入數據
sendbuffer.put(sendText.getBytes());
//將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
sendbuffer.flip();
//輸出到通道
client.write(sendbuffer);
System.out.println("服務器端向客戶端發送數據--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
int port = 8888;
NIOServer server = new NIOServer(port);
server.listen();
}
}
以上是簡單的NIO 客戶端和服務端進行通信的demo。具體過程都已經註解說明。
5、小結
本章節詳細的描述了多路複用技術的底層原理,以及實現了nio的demo,並且在nio基礎上配合底層epoll進行了詳解。如有問題歡迎諮詢。本文參考了大量的博客,由於時間已久,當時沒有記錄博客的來源,這裏說聲感謝。如果需要備註博客,歡迎博客作者提醒,謝謝!!