socket 多路復用原理和代碼 select poll epoll


B站有學習視頻 

https://www.bilibili.com/video/BV1n5411b76b?p=1 可以直接從該視頻第一小節6:00 開始看。

老師從BIO 開始講BIO的缺陷,改進方案:多線程BIO ,在一步步進化到NIO,最后進化到調用linux內核的多路復用。

多路復用簡化圖流程如下:

首先需要思考,最原始的socket流有何缺陷,"痛點"在哪里,根據痛點又是如何改造的。比如:

1、流是單向的,通道是雙向的,可讀可寫。
2、流讀寫是阻塞的,通道可以異步讀寫,效率的提升很明顯。

下面是我整理出老師講的進化過程:

單線程原生socket

首先回顧一下socket clinet和socket server是怎么調用的。

//服務端 
ServerSocket serverSocket = new ServerSocket(); serverSocket.setReuseAddress(true);//這個設置要放在綁定端口前 serverSocket.bind(new InetSocketAddress(8090)); while(true){ Socket socket = serverSocket.accept();//阻塞 socket.getInputStream().read();//阻塞 } //客戶端 Socket clientSocket = new Socket("127.0.0.1", 8090); clientSocket.getOutputStream().write("aaa".getBytes()); clientSocket.close();

以上服務端代碼在遇到高並發的客戶端訪問時,會不停的創建對象,有性能問題
為了解決性能問題,需要引入多線程,進化版如下:

 多線程socket

  //服務器
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);//這個設置要放在綁定端口前
serverSocket.bind(new InetSocketAddress(8090));
ExecutorService pool = Executors.newFixedThreadPool(10000);//線程池
while(true){
    Socket socket = serverSocket.accept();//阻塞
    //socket.getInputStream().read();//這段阻塞的代碼放入子線程HandleSocketSer中
    pool.execute(new HandleSocketSer(socket));
}
//客戶端和之前一樣

現在代碼接收連接的是主線程,已經讓子線程來處理每個連接了。

但是還有性能問題,有1萬個連接,1萬個連接中只有200個連接有數據發送過來,但是卻起了1萬個線程,會有資源浪費的情況,需要進一步優化

單線程多路復用

 

 思路就是添加一個列表,寫個循環,一直監控socket連接中有沒有數據過來,這樣接收新的連接不會阻塞,每次有數據發送過來,都會先添加到列表中,在遍歷一次列表獲取數據,然后阻塞到serverSocket.accept()繼續等待。

這里的時候已經使用ServerSocketChannel通道了,N個請求過來,都是復用這一個通道來處理(讀/寫)的。

單線程 (使用linux內核做)多路復用

為了進一步優化代碼性能,將輪訓監控列表的部分,放到linux內核執行(通過jvm調用linux內核),可以提高性能,進化版如下:

如上圖所以,如果現在有三個連接,1和2發送數據,而3沒有數據只做了連接,是不會做讀/寫操作的。

SelectorServerDemo

public class SelectorTest {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc= ServerSocketChannel.open();
        ssc.configureBlocking(false);//配置為非阻塞模式
        ssc.socket().bind(new InetSocketAddress(7707));
 // 通過open()方法找到Selector 
// 底層: 開啟epoll,為當前socket服務創建epoll服務,epoll_create
Selector selector
=Selector.open(); ssc.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer buff=ByteBuffer.allocate(48); while (true){ int n=selector.select(); if(n==0) continue; Iterator<SelectionKey> it=selector.selectedKeys().iterator(); while(it.hasNext()){ SelectionKey sk=it.next(); if(sk.isAcceptable()){ System.out.println("accpet----------");
//這里類型轉為ServerSocketChannel 主要用來處理請求,“實際干活的”是下面SocketChannel
//在netty中,進化為BossGroup和workGroup SocketChannel ssc_a
=((ServerSocketChannel) sk.channel()).accept(); ssc_a.configureBlocking(false); ssc_a.register(selector,SelectionKey.OP_READ); }else if(sk.isConnectable()){ System.out.println("Connect----------"); //DOOTHER }else if(sk.isReadable()){ System.out.println("Read----------"); SocketChannel ssc_r=(SocketChannel) sk.channel(); //清理緩存並接收數據 buff.clear();
try {
int count=ssc_r.read(buff); if (count > 0) { System.out.println(new String(buff.array(),0,count)); ssc_r.register(selector, SelectionKey.OP_WRITE); }
} catch (IOException e) {
sk.cancel();//關閉需要2步
ssc_r.close();
}
}else if(sk.isWritable()){
 System.out.println("Write----------"); buff.clear(); // 返回為之創建此鍵的通道。 SocketChannel ssc_w = (SocketChannel) sk.channel(); String sendText="response message ------"; //向緩沖區中輸入數據  buff.put(sendText.getBytes()); //將緩沖區各標志復位,因為向里面put了數據標志被改變要想從中讀取數據發向服務器,就要復位  buff.flip(); //輸出到通道  ssc_w.write(buff); ssc_w.register(selector, SelectionKey.OP_READ); } it.remove(); } } } }

ClinetDemo

public class SelectorClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sc=SocketChannel.open();
        sc.connect(new InetSocketAddress("127.0.0.1",7707));
        ByteBuffer bf= ByteBuffer.allocate(48);

        bf.putChar('N');
        bf.putChar('B');
        bf.putChar('A');
        bf.flip();   //flip 將寫模式切換為讀取模式(原理是通過改變游標和位置)
        sc.write(bf); //模擬發送
        sc.close();  
        System.out.println("client end====");
    }
}

 后續的一個演化版本就是netty了,一個高性能、異步事件驅動的NIO框架。

參考 

https://www.bilibili.com/video/BV1n5411b76b?p=1   (享學課堂視頻)

https://www.jianshu.com/p/0d497fe5484a   (簡書狼哥博客)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM