IO通訊模型(一)同步阻塞模式BIO(Blocking IO)
幾個概念
阻塞IO 和非阻塞IO 這兩個概念是程式級別的。主要描述的是程式請求作業系統IO操作後,如果IO資源沒有準備好,那麼程式該如何處理的問題:前者等待;後者繼續執行(但是使用執行緒一直輪詢,直到有IO資源準備好了)。
同步IO 和 非同步IO,這兩個概念是作業系統級別的。主要描述的是作業系統在收到程式請求IO操作後,如果IO資源沒有準備好,該如何響應程式的問題:前者不響應,直到IO資源準備好以後;後者返回一個標記(好讓程式和自己知道以後的資料往哪裡通知),當IO資源準備好以後,再用事件機制返回給程式。
同步阻塞模式(Blocking IO)
同步阻塞IO模型是最簡單的IO模型,使用者執行緒在核心進行IO操作時如果資料沒有準備號會被阻塞。
偽程式碼表示如下:
{
// 阻塞,直到有資料
read(socket, buffer);
process(buffer);
}
BIO通訊方式的特點
:
- 一個執行緒負責連線,多執行緒則為每一個接入開啟一個執行緒。
- 一個請求一個應答。
- 請求之後應答之前客戶端會一直等待(阻塞)。
BIO通訊方式在單執行緒伺服器下一次只能處理一個請求,在處理完畢之前一直阻塞。因此不適用於高併發的情況。不過可以使用多執行緒稍微
改進。
Java同步阻塞模式
Java中的阻塞模式BIO,就是在java.net
包中的Socket套接字的實現,Socket套接字是TCP/UDP等傳輸層協議的實現。
Java同步阻塞模式編碼
多執行緒客戶端
為了測試服務端程式,可以先編寫一個多執行緒客戶端用於請求測試。
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.CountDownLatch; /** * <p> * BIO測試 * 模擬20個客戶端併發請求,服務端則使用單執行緒。 * * @Author niujinpeng * @Date 2018/10/15 10:50 */ public class SocketClient { public static void main(String[] args) throws InterruptedException { Integer clientNumber = 20; CountDownLatch countDownLatch = new CountDownLatch(clientNumber); // 分別啟動20個客戶端 for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) { SocketClientRequestThread client = new SocketClientRequestThread(countDownLatch, index); new Thread(client).start(); } synchronized (SocketClient.class) { SocketClient.class.wait(); } } } /** * <p> * 客戶端,用於模擬請求 * * @Author niujinpeng * @Date 2018/10/15 10:53 */ class SocketClientRequestThread implements Runnable { private CountDownLatch countDownLatch; /** * 執行緒的編號 */ private Integer clientIndex; public SocketClientRequestThread(CountDownLatch countDownLatch, Integer clientIndex) { this.countDownLatch = countDownLatch; this.clientIndex = clientIndex; } @Override public void run() { Socket socket = null; OutputStream clientRequest = null; InputStream clientResponse = null; try { socket = new Socket("localhost", 83); clientRequest = socket.getOutputStream(); clientResponse = socket.getInputStream(); //等待,直到SocketClientDaemon完成所有執行緒的啟動,然後所有執行緒一起傳送請求 this.countDownLatch.await(); // 傳送請求資訊 clientRequest.write(("這是第" + this.clientIndex + "個客戶端的請求").getBytes()); clientRequest.flush(); // 等待伺服器返回訊息 System.out.println("第" + this.clientIndex + "個客戶端請求傳送完成,等待伺服器響應"); int maxLen = 1024; byte[] contentBytes = new byte[maxLen]; int realLen; String message = ""; // 等待服務端返回,in和out不能cloese while ((realLen = clientResponse.read(contentBytes, 0, maxLen)) != -1) { message += new String(contentBytes, 0, realLen); } System.out.println("第" + this.clientIndex + "個客戶端接受到來自伺服器的訊息:" + message); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } finally { try { if (clientRequest != null) { clientRequest.close(); } if (clientRequest != null) { clientResponse.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
單執行緒服務端
因為Java中的Socket就是BIO的模式,因此我們可以很簡單的編寫一個BIO單執行緒服務端。
SocketServer.java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服務端
* <p>
* 單執行緒阻塞的伺服器端
*
* @Author niujinpeng
* @Date 2018/10/15 11:17
*/
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(83);
try {
while (true) {
// 阻塞,直到有資料準備完畢
Socket socket = serverSocket.accept();
// 開始收取資訊
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
Integer sourcePort = socket.getPort();
int maxLen = 1024 * 2;
byte[] contextBytes = new byte[maxLen];
// 阻塞,直到有資料準備完畢
int realLen = input.read(contextBytes, 0, maxLen);
// 讀取資訊
String message = new String(contextBytes, 0, realLen);
// 輸出接收資訊
System.out.println("伺服器收到來自埠【" + sourcePort + "】的資訊:" + message);
// 響應資訊
output.write("Done!".getBytes());
// 關閉
output.close();
input.close();
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
}
多執行緒服務端
單執行緒伺服器,在處理請求時只能同時處理一條,也就是說如果在請求到來時發現有請求尚未處理完畢,只能等待處理,因此使用多執行緒改進
服務端。
SocketServerThread.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服務端
* <p>
* 多執行緒的阻塞的服務端
* <p>
* 當然,接收到客戶端的socket後,業務的處理過程可以交給一個執行緒來做。
* 但還是改變不了socket被一個一個的做accept()的情況。
*
* @Author niujinpeng
* @Date 2018/10/15 11:17
*/
public class SocketServerThread implements Runnable {
/**
* 日誌
*/
private static final Logger logger = LoggerFactory.getLogger(SocketServerThread.class);
private Socket socket;
public SocketServerThread(Socket socket) {
this.socket = socket;
}
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(83);
try {
while (true) {
Socket socket = serverSocket.accept();
//當然業務處理過程可以交給一個執行緒(這裡可以使用執行緒池),並且執行緒的建立是很耗資源的。
//最終改變不了.accept()只能一個一個接受socket的情況,並且被阻塞的情況
SocketServerThread socketServerThread = new SocketServerThread(socket);
new Thread(socketServerThread).start();
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
@Override
public void run() {
InputStream in = null;
OutputStream out = null;
try {
//下面我們收取資訊
in = socket.getInputStream();
out = socket.getOutputStream();
Integer sourcePort = socket.getPort();
int maxLen = 1024;
byte[] contextBytes = new byte[maxLen];
//使用執行緒,同樣無法解決read方法的阻塞問題,
//也就是說read方法處同樣會被阻塞,直到作業系統有資料準備好
int realLen = in.read(contextBytes, 0, maxLen);
//讀取資訊
String message = new String(contextBytes, 0, realLen);
//下面列印資訊
logger.info("伺服器收到來自於埠:" + sourcePort + "的資訊:" + message);
//下面開始傳送資訊
out.write("回發響應資訊!".getBytes());
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
//試圖關閉
try {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
if (this.socket != null) {
this.socket.close();
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
}
}
}
看起來多執行緒增加了服務能力,但是很明顯多執行緒改進之後仍有以下侷限性
:
- 接收和通知處理結果的過程依舊是單執行緒的。
- 系統可以建立的執行緒數量有限。
cat /proc/sys/kernel/threads-max
可以檢視可以建立的執行緒數量。 - 如果執行緒較多,CPU需要更多的時間切換,處理真正業務的時間就會變少。
- 建立執行緒會消耗較多資源,JVM建立一個執行緒都會預設分配128KB空間。
- 多執行緒也無法解決因為
呼叫底層系統
的同步IO
而決定的同步IO機制。
同步阻塞模式總結
BIO模式因為程序的阻塞掛起,不會消耗過多的CPU資源,而且開發難度低,比較適合併發量小的網路應用開發。同時很容易發現因為請求IO會阻塞程序,所以不時候併發量大的應用。如果為每一個請求分配一個執行緒,系統開銷就會過大。
同時在Java中,使用了多執行緒來處理阻塞模式,也無法解決程式在accept()
和read()
時候的阻塞問題。因為accept()
和read()
的IO模式支援是基於作業系統的,如果作業系統發現沒有套接字從指定的埠傳送過來,那麼作業系統就會等待
。這樣accept()
和read()
方法就會一直等待。
歡迎關注公眾號與我聯絡!