本文是觀看了B站的馬士兵的視頻后的總結(jié):
清華大牛權(quán)威講解nio,epoll,多路復(fù)用,更好的理解redis-netty-Kafka等熱門技術(shù)
和知乎的一篇文章:
看不懂來砍我乔外,epoll原理
理解Socket基礎(chǔ)1—計算機基礎(chǔ)
我們知道內(nèi)存是被分為內(nèi)核和用戶兩個部分的床三,內(nèi)核用于運行操作系統(tǒng)和硬件相關(guān)的底層驅(qū)動,由于系統(tǒng)的保護機制杨幼,用戶態(tài)的進程是無法直接訪問硬件的撇簿,比如網(wǎng)絡(luò)通信的硬件網(wǎng)卡;
硬件設(shè)備接收事件(網(wǎng)卡接收數(shù)據(jù)幀差购,鍵盤接收輸入等)四瘫,當(dāng)有了事件后,硬件層會產(chǎn)生一個中斷歹撒,CPU會立刻停止當(dāng)前的工作(比如當(dāng)前正在執(zhí)行用戶進程)處理這個中斷莲组,處理的工作就是內(nèi)核去實現(xiàn),比如調(diào)用內(nèi)核中對應(yīng)硬件驅(qū)動的回調(diào)暖夭;
用戶態(tài)的進行想要訪問硬件資源(和硬件交互)必須通過內(nèi)核锹杈,內(nèi)核會提供
系統(tǒng)調(diào)用
讓用戶態(tài)安全的訪問計算機;
拿Socket舉例迈着,網(wǎng)絡(luò)數(shù)據(jù)通過物理網(wǎng)線傳給網(wǎng)卡竭望,此時網(wǎng)卡會產(chǎn)生一個中斷,告訴CPU有網(wǎng)絡(luò)數(shù)據(jù)進入電腦了裕菠,這時會將數(shù)據(jù)交給內(nèi)核咬清,具體放在哪我也沒研究過,反正就是放在內(nèi)核里面奴潘,用戶態(tài)(Java)必須通過系統(tǒng)調(diào)用
去拿到這個網(wǎng)絡(luò)數(shù)據(jù)
BIO
傳統(tǒng)的IO使用(偽代碼)
// 客戶端
Socket socket = new Socket("127.0.0.1",8090);
socket.getOutputStream();
socket.getInputStream();
// 服務(wù)端
ServerSocket serverSocket = new ServerSocket(8089);
Socket socket = serverSocket.accept();
socket.getOutputStream();
socket.getInputStream();
客戶端:
- 創(chuàng)建Socket對象旧烧,傳入服務(wù)端對應(yīng)的ip和端口,會自動連接
- 獲取IO流通信
服務(wù)端:
- 服務(wù)端創(chuàng)建
ServerSocket
對象画髓,綁定ip和port -
ServerSocket
調(diào)用accept()
監(jiān)聽客戶端連接掘剪,練連接完成會返回客戶端對應(yīng)的Socket
對象(這是一個阻塞方法,一般會在循環(huán)中開啟線程
去執(zhí)行奈虾,即一個線程一個Socket連接) - 完事兒以后通過Socket獲取IO流進行數(shù)據(jù)的讀寫
這些是我們在java層做的事情夺谁,那么網(wǎng)絡(luò)通信是如何發(fā)生的呢?
首先java層是用戶態(tài)的一個進程肉微,他是無法直接讀取網(wǎng)卡的數(shù)據(jù)的匾鸥,必須通過系統(tǒng)調(diào)用到內(nèi)核中去獲取碉纳;系統(tǒng)調(diào)用是通過native層去實現(xiàn)的勿负;
BIO存在的問題:
- accept()和IO的讀寫是阻塞方法,必須開啟多線程劳曹,每一個Socket連接建立一個線程
- 很多Socket連接建立了并沒有通信笆环,會浪費大量的系統(tǒng)資源攒至;
NIO
為了解決線程浪費問題出現(xiàn)了NIO,將阻塞方法改為非阻塞方法躁劣,如果有連接迫吐,有數(shù)據(jù),就去處理账忘,沒有的話繼續(xù)執(zhí)行下面志膀,等待下次循環(huán);
NIO存在的問題:
NIO雖然解決了線程浪費
的問題鳖擒,可是如果在大量網(wǎng)絡(luò)請求的情況下溉浙,當(dāng)前方案下的執(zhí)行效率會變得非常的低,因為Java層的循環(huán)變得非常的長蒋荚,并且每次循環(huán)都需要調(diào)用系統(tǒng)調(diào)用
去詢問內(nèi)核這個請求有沒有用戳稽,這個連接有沒有數(shù)據(jù),大量的無效的系統(tǒng)調(diào)用也會影響性能期升;
Select:
為了解決NIO在java層大量無效循環(huán)調(diào)用
System call
的情況惊奇,出現(xiàn)了一個select
系統(tǒng)調(diào)用,Select的作用是將10000此循環(huán)全部通過一次SC交給內(nèi)核播赁,由內(nèi)核去循環(huán)颂郎,判斷哪些是有效的循環(huán),比如100次有效循環(huán)容为,那么我的java就可以有目的性的去調(diào)用100次有效的SC去進行數(shù)據(jù)讀寫乓序,Socket連接建立;
select缺點:
- 需要將連接一次性傳遞給內(nèi)核
- 雖然省去了大量的SC坎背,但是內(nèi)核需要去遍歷循環(huán)替劈,內(nèi)核的內(nèi)存壓力會增大
Epoll:
等待隊列紅黑樹
:
Epoll
將所有的Socket連接都在內(nèi)核中保存了下來,就省去了Select一次性將所有的Socket連接發(fā)過來的這一步驟得滤;
就緒列表雙向鏈表
:
Select
效率低的原因是因為需要遍歷所有的連接才能知道哪個連接有數(shù)據(jù)陨献,而epoll
通過維護一個集合,存放所有的就緒連接耿戚,這樣就避免了遍歷的步驟湿故;當(dāng)有數(shù)據(jù)到達時阿趁,中斷程序
會產(chǎn)生一個中斷將有數(shù)據(jù)的Socket添加到就緒列表膜蛔;
epoll將多路復(fù)用的實現(xiàn)拆分為三個步驟:
-
epoll_create:
內(nèi)核會產(chǎn)生一個epoll 實例數(shù)據(jù)結(jié)構(gòu)并返回一個文件描述符,這個特殊的描述符就是epoll實例的句柄脖阵,后面的兩個接口都以它為中心 -
epoll_ctl:
維護等待隊列將被監(jiān)聽的描述符添加到紅黑樹或從紅黑樹中刪除皂股,或者對監(jiān)聽事件進行修改 -
epoll_wait:
阻塞進程,等待數(shù)據(jù)命黔,程序執(zhí)行到這一步時呜呐,如果就緒列表
有數(shù)據(jù)就斤,就直接返回,如果沒有數(shù)據(jù)就會阻塞蘑辑;
NIO
NonBlocking IO特點:
- 非阻塞IO洋机,沒有數(shù)據(jù)時不會阻塞,而是返回0
- 單線程處理多任務(wù)
核心類:
- channel
- selector
- buffer
channel:
channel通道類似流洋魂,既可以從流讀取數(shù)據(jù)绷旗,也可以寫入數(shù)據(jù)到流,流是單向的副砍,通道是雙向的衔肢;
channel的實現(xiàn):
- FileChannel:從文件中讀寫數(shù)據(jù),無法設(shè)置為非阻塞式
- DataGramChannel:從UDP讀寫網(wǎng)絡(luò)數(shù)據(jù)
- SocketChannel:從TCP讀寫網(wǎng)絡(luò)數(shù)據(jù)
- ServerSocketChannel:監(jiān)聽新進來的TCP連接,每一個新的TCP連接都會創(chuàng)建一個新的SocketChannel
buffer
NIO buffer 提供了一組方法豁翎,用來訪問緩沖區(qū)角骤,對于緩沖區(qū),本質(zhì)上是一塊可以寫入數(shù)據(jù)心剥,可以讀取數(shù)據(jù)的內(nèi)存邦尊;
buffer的使用:
1.channel寫入數(shù)據(jù)到buffer
2.調(diào)用buffer的flip()make buffer ready to read
3. 從buffer中讀取數(shù)據(jù)
4.調(diào)用buffer的clear()`make buffer ready to write`
buffer的工作原理:
buffer的重要屬性:capacity position limit
capacity:作為一個內(nèi)存塊,buffer有一個固定大小刘陶,capacity就是記錄buffer的大小
position:當(dāng)buffer寫入的時候position從0開始胳赌,放入一個數(shù)據(jù),position就后移一位匙隔;當(dāng)buffer讀取的時候疑苫,position從0開始,每讀一個數(shù)據(jù)纷责,后移一位捍掺;
limit:在寫入的時候,limit同capacity,表示可以寫入的大性偕拧挺勿;在讀取時,表示當(dāng)前可讀取的數(shù)量喂柒;
buffer的類型:
- ByteBuffer:
- CharBuffer:
- DoubleBuffer:
- FloatBuffer:
- IntBuffer:
- LongBuffer:
- ShortBuffer:
buffer的創(chuàng)建(分配):
// 分配了48字節(jié)大小的字符Buffer
CharBuffer charBuffer = CharBuffer.allocate(48);
向buffer寫入數(shù)據(jù)
// 1 直接用 put() 寫入
charBuffer.put('1');
// 2 channel寫入到buffer
channel.read(buffer);
flip():
將buffer從寫模式轉(zhuǎn)換成讀模式
從buffer讀取數(shù)據(jù)
// 1 直接使用 get() 讀取
char c = charBuffer.get();
// 2 讀取到channel中
channel.write(buffer);
rewind():
將position重新設(shè)置為0不瓶,可以再次讀取buffer(limit保持不變)clear():
將buffer從讀模式轉(zhuǎn)為寫模式,clear不會保存原來的數(shù)據(jù)灾杰,compact():
compact會將未讀的數(shù)據(jù)拷貝到buffer的起始處蚊丐,并且將position移到最后一個數(shù)后面mark() & reset() :
通過mark 記錄position的值,再通過reset恢復(fù)到之前記錄的positionequals() :
比較buffer內(nèi)的剩余元素艳吠,如果它們類型相等麦备,數(shù)量相等,元素值相等,那么兩個buffer 就相等compareTo() :
比較元素的數(shù)量和元素值的大辛莞荨黍匾;
分散和聚集(Scatter/Gather):
-
分散:
將channel的數(shù)據(jù)分散讀取到多個buffer中
scatter read
// 分散 , 一個channel的數(shù)據(jù)讀取到多個buffer
ByteBuffer head = ByteBuffer.allocate(20);
ByteBuffer body = ByteBuffer.allocate(480);
ByteBuffer[] buffers = {head,body};
try {
// 從channel讀取數(shù)據(jù)
channel.read(buffers);
} catch (IOException e) {
e.printStackTrace();
}
-
聚集:
將多個buffer數(shù)據(jù)聚集寫入到一個channel中
gather write
// 聚集 呛梆, 多個buffer數(shù)據(jù)寫入channel
ByteBuffer head = ByteBuffer.allocate(20);
ByteBuffer body = ByteBuffer.allocate(480);
ByteBuffer[] buffers = {head,body};
try {
// 寫入數(shù)據(jù)到channel
channel.write(buffers);
} catch (IOException e) {
e.printStackTrace();
}
Selector
選擇器锐涯,用于實現(xiàn)單線程管理多個channel
,即管理多個網(wǎng)絡(luò)連接
1. selector的創(chuàng)建:
try {
Selector selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
2. 向selector中注冊channel
// 將channel設(shè)置為非阻塞式
socketChannel.configureBlocking(false);
// 注冊到selector上
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
注意, 如果一個 Channel 要注冊到 Selector 中, 那么這個 Channel 必須是非阻塞的, 即channel.configureBlocking(false); 因為 Channel 必須要是非阻塞的, 因此 FileChannel 是不能夠使用選擇器的, 因為 FileChannel 都是阻塞的
register()第二個參數(shù)用于指定selector對channel的什么事件感興趣,常見的事件有:
- SelectionKey.OP_ACCEPT:確認(rèn)事件
- SelectionKey.OP_CONNECT:連接事件填物,TCP連接
- SelectionKey.OP_READ:讀出事件
- SelectionKey.OP_WRITE:寫入事件
SelectionKey:
每次向Selector中注冊一個channel都會拿到一個SelectionKey對象全庸;通過selectionKey對綁定事件進行控制,SelectionKey重要的成員變量:
- interest Set:感興趣事件的集合
- ready Set:已準(zhǔn)備就緒的操作的集合
- Channel:
- Selector:
- 附加對象:
// 獲取 channel
key.channel();
// 獲取 selector
key.selector();
// 獲取 感興趣的事件
key.interestOps();
// 附加對象
key.attach(new Object());
Selector.select():
調(diào)用該方法后會阻塞融痛,知道被注冊的channel有事件出現(xiàn)壶笼,或者出現(xiàn)新的channel注冊事件
Set keySet = selector.selectedKeys();
Iterator iterator = keySet.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = (SelectionKey) iterator.next();
// TODO: 通過 selectionKey 獲取channel 處理事件
iterator.remove(); // 刪除當(dāng)前元素(key)
}