在開篇之前,我們對JavaNIO 的使用方式不做過多介紹,這種API的介紹方式網上太多了,沒必要詳細介紹,我們假設NIO的使用方式,你能夠熟練運用。這是NIO系列第三篇:
通過之前的Unix的IO模型介紹,想必也瞭解到了5種IO模型。java的NIO是屬於同步非阻塞IO,關於IO多路複用,java沒有相應的IO模型,但有相應的編程模式,Reactor 就是基於NIO中實現多路複用的一種模式。本文將從以下幾點闡述Reactor模式:
reactor 是什麼
爲何要用,能解決什麼問題
如何用,更好的方式
其他事件處理模式
一、Reactor 是什麼
關於reactor 是什麼,我們先從wiki上看下:
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.
從上述文字中我們可以看出以下關鍵點 :
事件驅動(event handling)
可以處理一個或多個輸入源(one or more inputs)
通過Service Handler同步的將輸入事件(Event)採用多路複用分發給相應的Request Handler(多個)處理
自POSA2 中的關於Reactor Pattern 介紹中,我們瞭解了Reactor 的處理方式:
同步的等待多個事件源到達(採用select()實現)
將事件多路分解以及分配相應的事件服務進行處理,這個分派採用server集中處理(dispatch)
分解的事件以及對應的事件服務應用從分派服務中分離出去(handler)
關於Reactor Pattern 的OMT 類圖設計:
二、爲何要用Reactor
常見的網絡服務中,如果每一個客戶端都維持一個與登陸服務器的連接。那麼服務器將維護多個和客戶端的連接以出來和客戶端的contnect 、read、write ,特別是對於長鏈接的服務,有多少個c端,就需要在s端維護同等的IO連接。這對服務器來說是一個很大的開銷。
1、BIO
比如我們採用BIO的方式來維護和客戶端的連接:
// 主線程維護連接
public void run() {
try {
while (true) {
Socket socket = serverSocket.accept();
//提交線程池處理
executorService.submit(new Handler(socket));
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 處理讀寫服務
class Handler implements Runnable {
public void run() {
try {
//獲取Socket的輸入流,接收數據
BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String readData = buf.readLine();
while (readData != null) {
readData = buf.readLine();
System.out.println(readData);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
很明顯,爲了避免資源耗盡,我們採用線程池的方式來處理讀寫服務。但是這麼做依然有很明顯的弊端:
同步阻塞IO,讀寫阻塞,線程等待時間過長
在制定線程策略的時候,只能根據CPU的數目來限定可用線程資源,不能根據連接併發數目來制定,也就是連接有限制。否則很難保證對客戶端請求的高效和公平。
多線程之間的上下文切換,造成線程使用效率並不高,並且不易擴展
狀態數據以及其他需要保持一致的數據,需要採用併發同步控制
2、NIO
那麼可以有其他方式來更好的處理麼,我們可以採用NIO來處理,NIO中支持的基本機制:
非阻塞的IO讀寫
基於IO事件進行分發任務,同時支持對多個fd的監聽
我們看下NIO 中實現相關方式:
public NIOServer(int port) throws Exception {
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
}
@Override
public void run() {
while (!Thread.interrupted()) {
try {
//阻塞等待事件
selector.select();
// 事件列表
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()) {
it.remove();
//分發事件
dispatch((SelectionKey) (it.next()));
}
} catch (Exception e) {
}
}
}
private void dispatch(SelectionKey key) throws Exception {
if (key.isAcceptable()) {
register(key);//新鏈接建立,註冊
} else if (key.isReadable()) {
read(key);//讀事件處理
} else if (key.isWritable()) {
wirete(key);//寫事件處理
}
}
private void register(SelectionKey key) throws Exception {
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
// 獲得和客戶端連接的通道
SocketChannel channel = server.accept();
channel.configureBlocking(false);
//客戶端通道註冊到selector 上
channel.register(this.selector, SelectionKey.OP_READ);
}
我們可以看到上述的NIO例子已經差不多擁有reactor的影子了
基於事件驅動-> selector(支持對多個socketChannel的監聽)
統一的事件分派中心-> dispatch
事件處理服務-> read & write
事實上NIO已經解決了上述BIO暴露的1&2問題了,服務器的併發客戶端有了量的提升,不再受限於一個客戶端一個線程來處理,而是一個線程可以維護多個客戶端(selector 支持對多個socketChannel 監聽)。
但這依然不是一個完善的Reactor Pattern ,首先Reactor 是一種設計模式,好的模式應該是支持更好的擴展性,顯然以上的並不支持,另外好的Reactor Pattern 必須有以下特點:
更少的資源利用,通常不需要一個客戶端一個線程
更少的開銷,更少的上下文切換以及locking
能夠跟蹤服務器狀態
能夠管理handler 對event的綁定
那麼好的Reactor Pattern應該是怎樣的?
三、Reactor
在應用Java NIO構建Reactor Pattern中,大神 Doug Lea(讓人無限景仰的java 大神)在“Scalable IO in Java”中給了很好的闡述。我們採用大神介紹的3種Reactor 來分別介紹。
首先我們基於Reactor Pattern 處理模式中,定義以下三種角色:
Reactor 將I/O事件分派給對應的Handler
Acceptor 處理客戶端新連接,並分派請求到處理器鏈中
Handlers 執行非阻塞讀/寫 任務
1、單Reactor單線程模型
我們看代碼的實現方式:
/**
* 等待事件到來,分發事件處理
*/
class Reactor implements Runnable {
private Reactor() throws Exception {
SelectionKey sk =
serverSocket.register(selector,
SelectionKey.OP_ACCEPT);
// attach Acceptor 處理新連接
sk.attach(new Acceptor());
}
public void run() {
try {
while (!Thread.interrupted()) {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()) {
it.remove();
//分發事件處理
dispatch((SelectionKey) (it.next()));
}
}
} catch (IOException ex) {
//do something
}
}
void dispatch(SelectionKey k) {
// 若是連接事件獲取是acceptor
// 若是IO讀寫事件獲取是handler
Runnable runnable = (Runnable) (k.attachment());
if (runnable != null) {
runnable.run();
}
}
}
/**
* 連接事件就緒,處理連接事件
*/
class Acceptor implements Runnable {
@Override
public void run() {
try {
SocketChannel c = serverSocket.accept();
if (c != null) {// 註冊讀寫
new Handler(c, selector);
}
} catch (Exception e) {
}
}
}
這是最基本的單Reactor單線程模型。其中Reactor線程,負責多路分離套接字,有新連接到來觸發connect 事件之後,交由Acceptor進行處理,有IO讀寫事件之後交給hanlder 處理。
Acceptor主要任務就是構建handler ,在獲取到和client相關的SocketChannel之後 ,綁定到相應的hanlder上,對應的SocketChannel有讀寫事件之後,基於racotor 分發,hanlder就可以處理了(所有的IO事件都綁定到selector上,有Reactor分發)。
該模型 適用於處理器鏈中業務處理組件能快速完成的場景。不過,這種單線程模型不能充分利用多核資源,所以實際使用的不多。
2、單Reactor多線程模型
相對於第一種單線程的模式來說,在處理業務邏輯,也就是獲取到IO的讀寫事件之後,交由線程池來處理,這樣可以減小主reactor的性能開銷,從而更專注的做事件分發工作了,從而提升整個應用的吞吐。
我們看下實現方式:
/**
* 多線程處理讀寫業務邏輯
*/
class MultiThreadHandler implements Runnable {
public static final int READING = 0, WRITING = 1;
int state;
final SocketChannel socket;
final SelectionKey sk;
//多線程處理業務邏輯
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public MultiThreadHandler(SocketChannel socket, Selector sl) throws Exception {
this.state = READING;
this.socket = socket;
sk = socket.register(selector, SelectionKey.OP_READ);
sk.attach(this);
socket.configureBlocking(false);
}
@Override
public void run() {
if (state == READING) {
read();
} else if (state == WRITING) {
write();
}
}
private void read() {
//任務異步處理
executorService.submit(() -> process());
//下一步處理寫事件
sk.interestOps(SelectionKey.OP_WRITE);
this.state = WRITING;
}
private void write() {
//任務異步處理
executorService.submit(() -> process());
//下一步處理讀事件
sk.interestOps(SelectionKey.OP_READ);
this.state = READING;
}
/**
* task 業務處理
*/
public void process() {
//do IO ,task,queue something
}
}
3、多Reactor多線程模型
第三種模型比起第二種模型,是將Reactor分成兩部分,
mainReactor負責監聽server socket,用來處理新連接的建立,將建立的socketChannel指定註冊給subReactor。
subReactor維護自己的selector, 基於mainReactor 註冊的socketChannel多路分離IO讀寫事件,讀寫網 絡數據,對業務處理的功能,另其扔給worker線程池來完成。
我們看下實現方式:
/**
* 多work 連接事件Acceptor,處理連接事件
*/
class MultiWorkThreadAcceptor implements Runnable {
// cpu線程數相同多work線程
int workCount =Runtime.getRuntime().availableProcessors();
SubReactor[] workThreadHandlers = new SubReactor[workCount];
volatile int nextHandler = 0;
public MultiWorkThreadAcceptor() {
this.init();
}
public void init() {
nextHandler = 0;
for (int i = 0; i < workThreadHandlers.length; i++) {
try {
workThreadHandlers[i] = new SubReactor();
} catch (Exception e) {
}
}
}
@Override
public void run() {
try {
SocketChannel c = serverSocket.accept();
if (c != null) {// 註冊讀寫
synchronized (c) {
// 順序獲取SubReactor,然後註冊channel
SubReactor work = workThreadHandlers[nextHandler];
work.registerChannel(c);
nextHandler++;
if (nextHandler >= workThreadHandlers.length) {
nextHandler = 0;
}
}
}
} catch (Exception e) {
}
}
}
/**
* 多work線程處理讀寫業務邏輯
*/
class SubReactor implements Runnable {
final Selector mySelector;
//多線程處理業務邏輯
int workCount =Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(workCount);
public SubReactor() throws Exception {
// 每個SubReactor 一個selector
this.mySelector = SelectorProvider.provider().openSelector();
}
/**
* 註冊chanel
*
* @param sc
* @throws Exception
*/
public void registerChannel(SocketChannel sc) throws Exception {
sc.register(mySelector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT);
}
@Override
public void run() {
while (true) {
try {
//每個SubReactor 自己做事件分派處理讀寫事件
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isReadable()) {
read();
} else if (key.isWritable()) {
write();
}
}
} catch (Exception e) {
}
}
}
private void read() {
//任務異步處理
executorService.submit(() -> process());
}
private void write() {
//任務異步處理
executorService.submit(() -> process());
}
/**
* task 業務處理
*/
public void process() {
//do IO ,task,queue something
}
}
第三種模型中,我們可以看到,mainReactor 主要是用來處理網絡IO 連接建立操作,通常一個線程就可以處理,而subReactor主要做和建立起來的socket做數據交互和事件業務處理操作,它的個數上一般是和CPU個數等同,每個subReactor一個縣城來處理。
此種模型中,每個模塊的工作更加專一,耦合度更低,性能和穩定性也大量的提升,支持的可併發客戶端數量可達到上百萬級別。
關於此種模型的應用,目前有很多優秀的礦建已經在應用了,比如mina 和netty 等。上述中去掉線程池的第三種形式的變種,也 是Netty NIO的默認模式。下一節我們將着重講解netty的架構模式。
四、事件處理模式
在 Douglas Schmidt 的大作《POSA2》中有關於事件處理模式的介紹,其中有四種事件處理模式:
Reactor
Proactor
Asynchronous Completion Token
Acceptor-Connector
1. Proactor
本文介紹的Reactor就是其中一種,而Proactor的整體結構和reacotor的處理方式大同小異,不同的是Proactor採用的是異步非阻塞IO的方式實現,對數據的讀寫由異步處理,無需用戶線程來處理,服務程序更專注於業務事件的處理,而非IO阻塞。
2. Asynchronous Completion Token
簡單來說,ACT就是應對應用程序異步調用服務操作,並處理相應的服務完成事件。從token這個字面意思,我們大概就能瞭解到,它是一種狀態的保持和傳遞。
比如,通常應用程序會有調用第三方服務的需求,一般是業務線程請求都到,需要第三方資源的時候,去同步的發起第三方請求,而爲了提升應用性能,需要異步的方式發起請求,但異步請求的話,等數據到達之後,此時的我方應用程序的語境以及上下文信息已經發生了變化,你沒辦法去處理。
ACT 解決的就是這個問題,採用了一個token的方式記錄異步發送前的信息,發送給接受方,接受方回覆的時候再帶上這個token,此時就能恢復業務的調用場景。
上圖中我們可以看到在client processing 這個階段,客戶端是可以繼續處理其他業務邏輯的,不是阻塞狀態。service 返回期間會帶上token信息。
3. Acceptor-Connector
Acceptor-Connector是於Reactor的結合,也可以看成是一種變種,它看起來很像上面介紹的Reactor第三種實現方式,但又有本質的不同。
Acceptor-Connector模式是將網絡中對等服務的連接和初始化分開處理,使系統中的連接建立及服務一旦服務初始化後就分開解除耦合。連接器主動地建立到遠地接受器組件的連接,並初始化服務處理器來處理在連接上交換的數據。同樣地,接受器被動地等待來自遠地連接器的連接請求,在這樣的請求到達時建立連接,並初始化服務處理器來處理在連接上交換的數據。隨後已初始化的服務處理器執行應用特有的處理,並通過連接器和接受器組件建立的連接來進行通信。
這麼處理的好處是:
一般而言,用於連接建立和服務初始化的策略變動的頻度要遠小於應用服務實現和通信協議。
容易增加新類型的服務、新的服務實現和新的通信協議,而又不影響現有的連接建立和服務初始化軟件。比如採用IPX/SPX通信協議或者TCP協議。
連接角色和通信角色的去耦合,連接角色只管發起連接 vs. 接受連接。通信角色只管數據交互。
將程序員與低級網絡編程API(像socket或TLI)類型安全性的缺乏屏蔽開來。業務開發關係底層通信
引用:
http://www.kuqin.com/ace-2002-12/Part-One/Chapter-9.htm
http://www.dre.vanderbilt.edu/%7Eschmidt/PDF/reactor-siemens.pdf
更多架構知識,歡迎關注我的公衆號,大碼候(cool_wier)