本篇文章是BIO到NIO、多路復(fù)用器, 從理論到實(shí)踐, 結(jié)合實(shí)際案例對(duì)比各自效率與特點(diǎn)(上)的下一篇, 如果沒(méi)有看的小伙伴, 可以先看下, 不然可能會(huì)不連貫.
多路復(fù)用器簡(jiǎn)介
多路復(fù)用器是對(duì)于傳統(tǒng)NIO的優(yōu)化, 解決了傳統(tǒng)NIO無(wú)法直接獲取所有所有連接的狀態(tài), 需要挨個(gè)遍歷所有連接查看是否準(zhǔn)備就緒的問(wèn)題, 這種方式會(huì)涉及到很多次系統(tǒng)調(diào)用, 用戶(hù)態(tài)和內(nèi)核態(tài)的切換,效率不高.
那多路復(fù)用器是怎樣優(yōu)化的呢?
首先要明白 多路的路是誰(shuí)-------->其實(shí)就是每個(gè)IO連接
每個(gè)路有沒(méi)有數(shù)據(jù)誰(shuí)知道呢-------->內(nèi)核知道, 那既然內(nèi)核自己知道某一時(shí)刻有哪些連接是有連接的, 是不是我們直接調(diào)用對(duì)應(yīng)功能方法即可, 所以這里就有個(gè)多路復(fù)用器, 你調(diào)用這個(gè)多路復(fù)用器, 它就會(huì)給你返回所有的路的IO狀態(tài).
這個(gè)就可以通過(guò)一次系統(tǒng)調(diào)用,獲取所有連接的IO狀態(tài)的結(jié)果集
然后程序自己對(duì)有狀態(tài)的(準(zhǔn)備好的)連接進(jìn)行讀寫(xiě),這樣才是高性能
這里注意,多說(shuō)一句, 只要程序自己讀寫(xiě)數(shù)據(jù), 你的IO模型就是同步的
多路復(fù)用器的兩個(gè)階段
多路復(fù)用器有兩個(gè)階段, 或者說(shuō)是內(nèi)核的兩類(lèi)實(shí)現(xiàn), 這兩類(lèi)實(shí)現(xiàn)的最終目的都是一樣的, 就是幫你返回所有IO連接的IO狀態(tài)(是否可讀), 但是實(shí)現(xiàn)細(xì)節(jié)有些許差別, 可以理解為epoll是select poll的升級(jí)版.
這里還是再提示下, 以下的兩種實(shí)現(xiàn)講的操作系統(tǒng)中的實(shí)現(xiàn), 并不是Java中的方法.
-
select poll
需要把所有IO連接存到一個(gè)集合中, 把這個(gè)集合傳遞拷貝給內(nèi)核, 也就是調(diào)用select或者poll, 內(nèi)核會(huì)把集合中準(zhǔn)備就緒的連接給個(gè)特殊標(biāo)識(shí), 然后返回.
這樣程序就可以直接知道哪些連接是有狀態(tài)的, 從而直接進(jìn)行讀取數(shù)據(jù)
弊端:
假如有1w個(gè)連接, 每次都需要把這個(gè)1w個(gè)連接拷貝給內(nèi)核, 這個(gè)拷貝就是損耗點(diǎn), 每次需要重復(fù)拷貝數(shù)據(jù)給內(nèi)核. -
epoll
正是因?yàn)閟elect, poll 有自身的弊端, 這才催生了epoll.
優(yōu)化
以空間換時(shí)間, 開(kāi)辟了內(nèi)核空間, 緩存了應(yīng)用程序的連接信息. 這樣就不需要重復(fù)的拷貝數(shù)據(jù).無(wú)損耗才是高性能.實(shí)現(xiàn)步驟
1. 在一個(gè)linux機(jī)器上, 有很多的應(yīng)用程序, 所以一個(gè)應(yīng)用程序想要使用epoll的話(huà), 首先需要在內(nèi)核中 開(kāi)辟空間------對(duì)應(yīng)epoll_create系統(tǒng)調(diào)用
2. 然后當(dāng)連接創(chuàng)建后, 把這個(gè)連接加入到該空間------對(duì)應(yīng)epoll_ctl(add)系統(tǒng)調(diào)用
3. 然后才是進(jìn)行詢(xún)問(wèn), 看看有哪些IO連接準(zhǔn)備就緒------對(duì)應(yīng)epoll_wait系統(tǒng)調(diào)用
Java中的多路復(fù)用器封裝
在java.nio的包下,封裝了對(duì)于多路復(fù)用的實(shí)習(xí)和使用,也就是Selector類(lèi)
Java中的Seletor底層用的是哪種實(shí)現(xiàn)? select poll 還是epoll?
Java其實(shí)會(huì)在運(yùn)行的時(shí)候會(huì)動(dòng)態(tài)的決定使用哪種實(shí)現(xiàn), 因?yàn)樗鼤?huì)調(diào)用固定的方法去啟動(dòng)多路復(fù)用器,即Selector.open, 你的程序可能跑在不同的內(nèi)核之上, jdk會(huì)優(yōu)先選擇好的epoll, 但是如果沒(méi)有epoll這個(gè)多路復(fù)用器的話(huà),只有select或者poll, 也是可以正常運(yùn)行的
主要使用方法介紹:
這里有三個(gè)主要的方法, 不管底層使用的是哪種實(shí)現(xiàn), 都會(huì)調(diào)用這三個(gè)方法, 但是根據(jù)不同實(shí)現(xiàn), 具體做的事情又不一樣,區(qū)別如下:
- Selector.open
啟動(dòng)多路復(fù)用器, 優(yōu)先選擇epoll, 沒(méi)有的話(huà)選擇select或者poll.
如果是epoll的話(huà), 需要在內(nèi)核中開(kāi)辟空間, 即調(diào)用epoll_create. - register
select、poll: 會(huì)在jvm里建一個(gè)數(shù)組, 把每個(gè)連接對(duì)應(yīng)的文件描述符(fd4)都放進(jìn)去.
epoll: 則相當(dāng)于調(diào)用內(nèi)核方法epoll_ctl(add), 將該連接加入到內(nèi)核空間, 直接由內(nèi)核管理. - select
select、poll: 則會(huì)將jvm中的數(shù)組傳給內(nèi)核, 即調(diào)用select(fd4)或者poll(fd4)
epoll: 相當(dāng)于直接調(diào)用內(nèi)核方法epol_wait, 直接詢(xún)問(wèn)內(nèi)核
測(cè)試代碼
服務(wù)端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @ClassName:
* @Description:(描述這個(gè)類(lèi)的作用)
* @author:
* @date:
*
*/
public class SelectorTest {
private static ServerSocketChannel server=null;
private static Selector selector;
static int port=9090;
static int count=5000;
static long startTime;
public static void initServer(){
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//這里會(huì)在編譯期間自動(dòng)選擇 多路復(fù)用器的 實(shí)現(xiàn)
//可能為select poll 也可能為epoll
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
initServer();
System.out.println("服務(wù)器啟動(dòng)了......");
startTime = System.currentTimeMillis();
try {
flag:
while (true){
//select相當(dāng)于詢(xún)問(wèn)內(nèi)核有無(wú)數(shù)據(jù)可讀取 或者 有無(wú)連接可建立
//里面?zhèn)魅氲膮?shù)是超時(shí)時(shí)間,傳入0代表阻塞,一直等待有人建立連接或發(fā)送數(shù)據(jù)
//如果傳入的>0, 比如200, 則會(huì)最多等待200毫秒,有沒(méi)有都會(huì)返回一個(gè)結(jié)果
while(selector.select(0)>0){
//從多路復(fù)用器中取出所有有效的key
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//獲取之后要進(jìn)行移除,否則會(huì)重復(fù)獲取
iterator.remove();
//有新連接可建立
if(key.isAcceptable()){
acceptHander(key);
//可以進(jìn)行讀取
}else if(key.isReadable()){
readHander(key);
}
}
if(count <= 0){
System.out.println("處理5000個(gè)連接用時(shí):"+(System.currentTimeMillis()-startTime)/1000+"s");
server.close();
selector.close();
break flag;
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private static void readHander(SelectionKey key) {
//取出當(dāng)前key所關(guān)聯(lián)的客戶(hù)端
SocketChannel client = (SocketChannel) key.channel();
//取出該客戶(hù)端 對(duì)應(yīng)的 buffer
//這個(gè)buffer是我們建立連接時(shí)傳進(jìn)去和 channel一對(duì)一綁定的
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read=0;
try {
for(;;){
//從channel中讀取數(shù)據(jù)寫(xiě)入到buffer中
read = client.read(buffer);
if(read==0){
break;
//這里可能有bug,客戶(hù)端可能關(guān)掉,處理close_wait狀態(tài), 會(huì)一直監(jiān)聽(tīng)到這個(gè)事件
// 這里直接簡(jiǎn)單暴力的關(guān)掉
}else if(read<0){
client.close();
break;
}else{
//對(duì)于buffer,剛剛是寫(xiě),現(xiàn)在進(jìn)行讀操作,調(diào)用flip
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
String str = new String(bytes);
System.out.println(client.socket().getRemoteSocketAddress()+" -->" +str);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private static void acceptHander(SelectionKey key) {
try {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192);
//將這個(gè)新連接交給多路復(fù)用器去管理,后面多路復(fù)用器中才能監(jiān)控這個(gè)連接, 在我們?nèi)カ@取的時(shí)候,給我們返回有狀態(tài)的連接
//同時(shí)這里將channel和buffer 一對(duì)一 進(jìn)行綁定,可以很方便的往里寫(xiě)入, 或者 讀出來(lái)
client.register(selector, SelectionKey.OP_READ,buffer);
System.out.println("add client port:"+client.socket().getPort());
count--;
} catch (IOException e) {
e.printStackTrace();
}
}
}
測(cè)試使用的客戶(hù)端代碼還是和上篇文章中保持一致, 這里不再放了.
壓測(cè)結(jié)果
以上所有說(shuō)的都是理論, 而理論一定是需要實(shí)際結(jié)果來(lái)驗(yàn)證的, 我們這里就還是同樣處理5000個(gè)連接, 并接收同樣消息, 看看多路復(fù)用器的實(shí)際效果如何.
可以看到, 效果是非常非常明顯的, 比BIO,NIO都要快太多了, 而且還代碼還是單線(xiàn)程模型, 將其擴(kuò)展成多線(xiàn)程, 效率將會(huì)更高.
總結(jié)
從BIO -> NIO -> 多路復(fù)用器, 我們分析了各自的缺點(diǎn)及演變過(guò)程, 并是實(shí)際結(jié)果對(duì)比了各自的效率, 相信你會(huì)更加印象深刻.
針對(duì)本文的測(cè)試結(jié)果總結(jié)如下:
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-690022.html
今天的分享就到這里了,有問(wèn)題可以在評(píng)論區(qū)留言,均會(huì)及時(shí)回復(fù)呀.
我是bling,未來(lái)不會(huì)太差,只要我們不要太懶就行, 咱們下期見(jiàn).文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-690022.html
到了這里,關(guān)于BIO到NIO、多路復(fù)用器, 從理論到實(shí)踐, 結(jié)合實(shí)際案例對(duì)比各自效率與特點(diǎn)(下)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!