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 (簡書狼哥博客)