1.BIO(blocking I/O)
BIO是一個傳統的IO,是一個阻塞的IO,當然都是這麼說,那麼堵塞在哪裏呢,我們通過代碼示例給大家解說
我先用程序模擬一個客戶端連接服務端程序,建立一個socket連接來監聽客戶端,然後監聽到了以後用getInputStream進行獲取流然後死循環監聽客戶端的請求數據。
/**
* 用BIO方式讓客戶端連接程序,監聽服務端
*
* @Author df
* @Date 2020/4/12 15:24
* @Version 1.0
*/
public class BioServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("BIOServer has started,Listening on port" + serverSocket.getLocalSocketAddress());
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
// BIO阻塞原因:getInputStream阻塞一直佔用當前線程資源,不讓當前線程做其他事情
Scanner input = new Scanner(clientSocket.getInputStream());
//針對每個socket,不斷的進行數據交互
while (true) {
String request = input.nextLine();
if ("quit".equals(request)) {
break;
}
System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
String response = "From BIOServer Hello " + request + ".\n";
clientSocket.getOutputStream().write(response.getBytes());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
咱們來啓動一下main方法,啓動完成,監聽本地的8888端口
接下來就用cmd命令telnet來連接服務,輸入如下命令回車,如下第二張圖就連接上了
輸入字符敲回車,服務端都能收到了
然後打開第二個cmd窗口,同樣輸入telnet localhost 8888然後回車,你發現控制檯沒有再打印出第二個連接信息了,輸入任何字符也都沒有輸出了,那麼爲什麼呢?
當第一個線程請求以後,socket建立連接打印信息,一直循環等待,等第二個線程需要進來的時候就不能進行訪問了,因爲一直被第一個線程getInputStream堵塞着。也就是說阻塞一直佔用當前線程資源,不讓當前線程做其他事情。
那麼這樣阻塞也不是辦法啊,那我難道只能進行一次連接麼,這樣肯定是不行的,那需要解決這個問題啊!
既然BIO通過獲取getInputStream進行阻塞,那麼可以不可以用多線程的方式解決這個問題呢,讓客戶端能夠多個連接呢。也給大家準備了線程池改良版的代碼示例
/**
* 線程池版改良BIO阻塞問題
*
* @Author df
* @Date 2020/4/12 16:09
* @Version 1.0
*/
public class BioServerThreadPool {
Map<Socket, String> map;
public static void main(String[] args) {
// 創建3個線程池
ExecutorService executor = Executors.newFixedThreadPool(3);
RequestHandler requestHeader = new RequestHandler();
try (ServerSocket serverSocket = new ServerSocket(8888)) {
System.out.println("BIO thread Server has started,Listening on port" + serverSocket.getLocalSocketAddress());
while (true) {
Socket clientSocket = serverSocket.accept();
// 線程提交
executor.submit(new ClientHandler(clientSocket, requestHeader));
System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
}
} catch (
IOException e) {
e.printStackTrace();
}
}
/**
* @Author df
* @Date 2020/4/12 20:52
* @Version 1.0
*/
public class ClientHandler implements Runnable {
private static RequestHandler requestHandler;
private static Socket clientSocket;
public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
this.requestHandler = requestHandler;
this.clientSocket = clientSocket;
}
@Override
public void run() {
try (Scanner input = new Scanner(clientSocket.getInputStream())) {
while (true) {
String request = input.nextLine();
if ("quit".equals(request)) {
break;
}
System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
String response = "From BIOServer Hello " + request + ".\n";
clientSocket.getOutputStream().write(response.getBytes());
}
} catch (Exception e) {
}
}
}
public class RequestHandler {
public String handle(String request) {
return "From Server Hello " + request + ".\n";
}
}
咱們來啓動一下,打開cmd同樣輸入telnet localhost 8888然後回車,發現可以連接,並且也正常通信的
那麼再打開一個cmd,輸入telnet localhost 8888然後回車,看是否能連接呢?發現第二個也能連接並且正常通信
那麼打開第三個呢,能否連接呢?發現也是能連接上的,並且正常通信
那麼第四個呢?發現可以連接,但是無法進行任何輸出操作,爲什麼?
這時候就把請求操作都放到等待隊列裏了,所以你能有連接操作,但是已經被其他三個線程阻塞掉了,不能再有線程處理它了,那麼這個時候咱們在其他三個已經連接成功的輸入quit回車將其連接退出,又會發生什麼呢?你會發現第四個線程立馬把之前阻塞時候進行輸入的字符全部輸出,並且可以進行正常的通信了
其實tomcat中就是用這種BIO的方式進行請求連接,防止阻塞用線程池方式,默認線程池設置200個。所以多線程是解決阻塞的一種方式。
但是啊,在高併發場景下我有10000個人請求,如果選用線程池,就算開1000個線程池,9999個放入了等待隊列裏,而且等待隊列也不是源源不斷的存儲的,再說了讓用戶等待這種設計是不行的,而且多線程切換的開銷也大,所以這個是jdk的BIO留下的坑,那麼jdk會解決,所以JDK1.4以後出現了NIO。
那麼再說NIO之前一定要知道爲什麼IO是阻塞的呢?所以就說一下IO的阻塞流程!
1.1 BIO阻塞的過程!
其實IO是否阻塞和java代碼沒有直接關係,IO的本質(針對java而言):應用程序和操作系統內核進行數據交互。
爲什麼這麼說呢?假如我們要讀取硬盤的gupao.txt文件一定是要inputStream讀取,但是我們的java程序一定能直接讀取磁盤麼,答案是否的也是沒有權限的,需要操作系統幫助我們讀取,我們java程序是進程也可以叫做用戶空間,用戶空間是程序員可以操作的,可以直接對硬盤操作的就是內核空間。下圖在進行講解一下
以讀操作爲例說一下過程:
- java程序發出讀操作,內核空間收到請求通過磁盤控制器轉化對磁盤進行操作。
- 操作以後返回數據通過DMA方式將數據放入內核空間的緩衝區裏面。
- Java read是從緩衝區拿數據進行返回的。
所以走到這裏應該明白阻塞不阻塞的與我們程序沒有關係,我們也不會無緣無故的阻塞,阻塞不阻塞和內核空間纔有關係。
但是JDK設計者想好了該如何解決,因爲以前的BIO是我發起了read請求以後一直等待內核空間讀取放入緩衝然後用戶空間纔讀取,內核空間沒有操作完它就會一直等待,但是NIO就是我發起了read請求以後我就不管了我還可以做其他的事情,直到你把數據讀取完返回給我,我再操作,這樣就是非阻塞的了
把以上的敘述講解完大家就應該理解了,接下來說NIO
2.NIO(non-blocking I/O)
那麼這張圖就是同步非阻塞也就是NIO,首先application就是用戶空間,kernel就是內核空間。
1.用戶通過read,在這裏就是recvfrom(java也要調用c語言去讀取的)去讀取,內核空間接收到了進行操作,然後用戶空間不需要等待直接可以去幹別的事情,然後再來詢問內核空間處理好了麼,沒有的話用戶空間還可以幹別的,直到在繼續詢問內核空間發現內核空間告訴它自己處理好了(內核空間把數據拷貝到緩衝區)那麼進行返回,結束
那麼NIO方式的代碼示例我也準備好了,給大家演示一下
public class NIOServer {
public static void main(String[] args) {
try {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 設置不阻塞
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(9999));
System.out.println("NIO NIOServer has started ,listening on port:" + serverChannel.getLocalAddress());
Selector selector = Selector.open();
// 每個客戶端來了之後,就把客戶端註冊到selector選擇器上,默認狀態就是ACCEPT
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer buffer = ByteBuffer.allocate(1024);
RequestHandler requestHandler = new RequestHandler();
// 輪詢,服務端不斷輪詢,等待客戶端的連接
while (true) {
int select = selector.select();
if (select == 0) {
continue;
}
// 如果selector有的話,那麼就取出對應的channel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 判斷SelectionKey中的channel狀態如何
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept();
// 客戶端的channel來源打出來
System.out.println("Connection from " + clientChannel.getRemoteAddress());
// 將客戶端的也設置爲非阻塞
clientChannel.configureBlocking(false);
// 將channel的狀態設置爲read
clientChannel.register(selector, SelectionKey.OP_READ);
}
// 接下來輪詢到的時候發現狀態是readable
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 數據的交互是以buffer爲中間橋樑的
channel.read(buffer);
// 用buffer取數據也用buffer返回數據
String request = new String(buffer.array()).trim();
buffer.clear();
System.out.println(String.format("From %s : %s", channel.getRemoteAddress(), request));
String response = requestHandler.handle(request);
channel.write(ByteBuffer.wrap(response.getBytes()));
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
你們自行用之前的方法測試把,打開5,7八個都不會阻塞
3.AIO(NIO.2) (Asynchronous I/O)
異步非阻塞,服務器實現模式爲一個有效請求一個線程,客戶端I/O請求都是由OS先完成了再通知服務器應用啓動線程進行處理。
1.當應用空間要讀取的時候發送給內核空間,內核空間不管有沒有處理直接返回,然後等到內核空間處理完畢直接發送信號(deliver signal)把數據傳送給用戶空間。
4.多路複用IO
1.每一次的連接讀取也好寫操作也好都先不進行連接,都先存儲也可以說是登記到這邊,不會直接分配線程去調度資源,直到真的要操作input或Out的時候我纔去分配線程做這些操作呢。
1.有操作進行連接都會通過select進行註冊,輪詢監控發現有需要讀取或者寫操作再去分配線程,那麼之後的操作可以使用同步非阻塞或者異步非阻塞或者阻塞IO都是可以的。
以上就是全部內容,以上筆記記錄學習來源咕泡教育-Jack老師的公開課學習整理!