一、問題
BIO 和 NIO 作為 Server 端,當建立了 10 個連線時,分別產生多少個執行緒?
答案: 因為傳統的 IO 也就是 BIO 是同步執行緒堵塞的,所以每個連線都要分配一個專用執行緒來處理請求,這樣 10 個連線就會建立 10 個執行緒去處理。而 NIO 是一種同步非阻塞的 I/O 模型,它的核心技術是多路複用,可以使用一個連結上的不同通道來處理不同的請求,所以即使有 10 個連線,對於 NIO 來說,開啟 1 個執行緒就夠了。
二、BIO 程式碼實現
public
class
DemoServer
extends
Thread
{
private
ServerSocket serverSocket;
public
int getPort()
{
return serverSocket.getLocalPort();
}
public
void run()
{
try
{
serverSocket =
new
ServerSocket(0);
while
(true)
{
Socket socket = serverSocket.accept();
RequestHandler requestHandler =
new
RequestHandler(socket);
requestHandler.start();
}
}
catch
(IOException e)
{
e.printStackTrace();
}
finally
{
if
(serverSocket !=
null)
{
try
{
serverSocket.close();
}
catch
(IOException e)
{
e.printStackTrace();
}
}
}
}
public
static
void main(String[] args)
throws
IOException
{
DemoServer server =
new
DemoServer();
server.start();
try
(Socket client =
new
Socket(InetAddress.getLocalHost(), server.getPort()))
{
BufferedReader bufferedReader =
new
BufferedReader(new
InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s ->
System.out.println(s));
}
}
}
// 簡化實現,不做讀取,直接傳送字串
class
RequestHandler
extends
Thread
{
private
Socket socket;
RequestHandler(Socket socket)
{
this.socket = socket;
}
@Override
public
void run()
{
try
(PrintWriter out =
new
PrintWriter(socket.getOutputStream());)
{
out.println("Hello world!");
out.flush();
}
catch
(Exception e)
{
e.printStackTrace();
}
}
}
- 伺服器端啟動 ServerSocket,埠 0 表示自動繫結一個空閒埠。
- 呼叫 accept 方法,阻塞等待客戶端連線。
- 利用 Socket 模擬了一個簡單的客戶端,只進行連線、讀取、列印。
- 當連線建立後,啟動一個單獨執行緒負責回覆客戶端請求。
這樣,一個簡單的 Socket 伺服器就被實現出來了。
(圖片來源於楊曉峰)
三、NIO 程式碼實現
public
class
NIOServer
extends
Thread
{
public
void run()
{
try
(Selector selector =
Selector.open();
ServerSocketChannel serverSocket =
ServerSocketChannel.open();)
{// 建立 Selector 和 Channel
serverSocket.bind(new
InetSocketAddress(InetAddress.getLocalHost(),
8888));
serverSocket.configureBlocking(false);
// 註冊到 Selector,並說明關注點
serverSocket.register(selector,
SelectionKey.OP_ACCEPT);
while
(true)
{
selector.select();// 阻塞等待就緒的 Channel,這是關鍵點之一
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while
(iter.hasNext())
{
SelectionKey key = iter.next();
// 生產系統中一般會額外進行就緒狀態檢查
sayHelloWorld((ServerSocketChannel) key.channel());
iter.remove();
}
}
}
catch
(IOException e)
{
e.printStackTrace();
}
}
private
void sayHelloWorld(ServerSocketChannel server)
throws
IOException
{
try
(SocketChannel client = server.accept();)
{ client.write(Charset.defaultCharset().encode("Hello world!"));
}
}
// 省略了與前面類似的 main
}
- 首先,通過 Selector.open() 建立一個 Selector,作為類似排程員的角色。
- 然後,建立一個 ServerSocketChannel,並且向 Selector 註冊,通過指定 SelectionKey.OP_ACCEPT,告訴排程員,它關注的是新的連線請求。注意:為什麼我們要明確配置非阻塞模式呢?這是因為阻塞模式下,註冊操作是不允許的,會丟擲 IllegalBlockingModeException 異常。
- Selector 阻塞在 select 操作,當有 Channel 發生接入請求,就會被喚醒。
- 在 sayHelloWorld 方法中,通過 SocketChannel 和 Buffer 進行資料操作,在本例中是傳送了一段字串。
可以看到,在前面兩個樣例中,IO 都是同步阻塞模式,所以需要多執行緒以實現多工處理。而 NIO 則是利用了單執行緒輪詢事件的機制,通過高效地定位就緒的 Channel,來決定做什麼,僅僅 select 階段是阻塞的,可以有效避免大量客戶端連線時,頻繁執行緒切換帶來的問題,應用的擴充套件能力有了非常大的提高。下面這張圖對這種實現思路進行了形象地說明。
作者: 王磊的部落格
免費Java資料領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo/Kafka、Hadoop、Hbase、Flink等高併發分散式、大資料、機器學習等技術。
傳送門:mp.weixin.qq.com/s/JzddfH-7y…