Java BIO
- Java BIO 就是傳統的 java io編程,其相關的類和接口在 java.io包下
- BIO(blocking I/O) : 同步阻塞,服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善
- BIO 方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,程序簡單易理解。
BIO 代碼示例
package nio;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws Exception {
//線程池機制
//思路
//1. 創建一個線程池
//2. 如果有客戶端連接,就創建一個線程,與之通訊(單獨寫一個方法)
//ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newCachedThreadPool = Executors.newSingleThreadExecutor();
//創建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服務器啓動了");
while (true) {
System.out.println("線程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"等待連接....");
//監聽,等待客戶端連接,阻塞
final Socket socket = serverSocket.accept();
System.out.println("連接到一個客戶端");
//就創建一個線程,與之通訊(單獨寫一個方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { //我們重寫
//可以和客戶端通訊
handler(socket);
}
});
}
}
//編寫一個handler方法,和客戶端通訊
public static void handler(Socket socket) {
try {
byte[] bytes = new byte[1024];
//通過socket 獲取輸入流
InputStream inputStream = socket.getInputStream();
//循環的讀取客戶端發送的數據
while (true) {
//read也是阻塞的
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println("線程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName()+"讀到:"+
new String(bytes, 0, read)); //輸出客戶端發送的數據
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("關閉和client的連接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
java BIO的缺點
- 每個請求都需要創建獨立的線程,與對應的客戶端進行數據 Read,業務處理,數據 Write 。當併發數較大時,需要創建大量線程來處理連接,系統資源佔用較大。
- 連接建立後,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。其他請求不能使用當前被掛起的線程。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IIFgERKi-1587207564828)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586499972233.png)]
我們可以通過telnet來測試
我們能看到,每一個請求都需要一個一個線程來處理
此時我們如果採用單例線程池來做多個請求,我們發現程序無法同時處理多個請求,只有關閉正在阻塞的請求才能去處理其他的請求。顯然不合理。
此種方式符合 同步阻塞IO模型
NIO
- Java NIO 全稱 java non-blocking IO,是指 JDK提供的新 API。從JDK1.4 開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱爲NIO(即New IO),是同步非阻塞的
- NIO 相關類都被放在 java.nio 包及子包下,並且對原 java.io 包中的很多類進行改寫。
- NIO有三大核心部分:Channel(通道),Buffer(緩衝區), Selector(選擇器)
- NIO 是面向緩衝區編程的。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網絡
- Java NIO 的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。
- 通俗理解:NIO 是可以做到用一個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以分配50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,非得分配 10000 個。
- HTTP2.0 使用了多路複用的技術,做到同一個連接併發處理多個請求,而且併發請求的數量比 HTTP1.1 大了好幾個數量級
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Ef2wNLD3-1587207564837)(http://www.txjava.cn/wordpress/wp-content/uploads/2020/04/image-1586502329862.png)]
簡單NIO示例
服務端代碼:
package nio.demo1;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NIOServer {
/**
* 創建一個緩存區
* @param args
* @throws Exception
*/
static ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws Exception {
//獲得服務器斷點的服務管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//綁定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//設置成異步
serverSocketChannel.configureBlocking(false);
while (true){
//獲得客戶端連接過來的管道
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
socketChannel.configureBlocking(false);
int length = socketChannel.read(buffer);
if(length != -1){
System.out.println("接收到的數據是:"+new String(buffer.array(), 0, length));
}
}
}
}
}
客戶端代碼
public class NIOClient {
public static void main(String[] args) throws Exception {
InetSocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 6666);
Socket socket = new Socket();
socket.connect(socketAddress);
socket.getOutputStream().write("hello".getBytes());
}
}
在服務端由於是非阻塞IO,那麼我們無法監控到客戶端的連接,所以我們採用不斷輪詢的方式來監控(顯然這是一種性能的巨大消耗),此種方法是同步非阻塞IO模型的一種方式
NIO多路複用解決方案
我們可以看到,我們剛剛手動的方式來做了一個很不合理的方案。那麼到底什麼樣的方案是完美的方案呢?
選擇器
- Java 的 NIO,用非阻塞的 IO 方式。可以用一個線程,處理多個的客戶端連接,就會使用到 Selector(選擇器)
- Selector 能夠檢測多個註冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以註冊到同一個Selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。
- 只有在連接/通道 真正有讀寫事件發生時,纔會進行讀寫,就大大地減少了系統開銷,並且不必爲每個連接都創建一個線程,不用去維護多個線程, 避免了多線程之間的上下文切換導致的開銷
NIO多路複用實現代碼
服務端:
package nio.demo2;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NIOServer1 {
public static void main(String[] args) throws Exception {
//獲得服務器斷點的服務管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//綁定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//設置成異步
serverSocketChannel.configureBlocking(false);
//創建Selector
Selector selector = Selector.open();
//註冊
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
while(selector.select(2000) == 0){
System.out.println("等待連接");
continue;
}
//獲得準備就緒的key
Set<SelectionKey> keys = selector.selectedKeys();
//遍歷keys
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
//獲得key
SelectionKey key = iterator.next();
if(key.isAcceptable()){
//獲得通道
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
System.out.println(Thread.currentThread().getName()+" 接收到一個連接:"+socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
}
if(key.isReadable()){
//獲得通道
SocketChannel channel = (SocketChannel) key.channel();
//獲得緩衝區
ByteBuffer buffer = (ByteBuffer) key.attachment();
int length = channel.read(buffer);
if(length != -1){
System.out.println(Thread.currentThread().getName()+" 發送的數據是:"+new String(buffer.array(), 0, length)+" 客戶端的地址:"+channel.getRemoteAddress());
}
buffer.clear();
}
iterator.remove();
}
}
}
}
程序流程:
- 服務端啓動時候獲得Selector,註冊給ServerSocketChannel
- Selector 調用select方法, 監聽請求和讀寫事件,返回有事件發生的通道的個數.
- 通過selector獲得就緒狀態的鍵集合,然後遍歷這個集合判斷這個鍵的類型
- 當客戶端連接時key類型是OP_ACCEPT,會通過 ServerSocketChannel 得到 SocketChannel, 把該通道連同buffer註冊到selector選擇器上。一個 selector 上可以註冊多個 SocketChannel
- 註冊後返回一個 SelectionKey, 會和該 Selector 關聯(集合)
- 進一步得到各個 SelectionKey (有事件發生)
- 如果key的類型是OP_READ或者OP_WRITE(讀寫),通過 SelectionKey反向獲取 SocketChannel , 方法 channel()
- 可以通過得到的channel, 完成業務處理
此刻我會發現我們的NIO使用的就是IO多路複用的模型。
更詳細的Spring源碼解析請關注:java架構師免費課程
每晚20:00直播分享高級java架構技術
掃描加入QQ交流羣264572737