Reactor模型是典型的事件驅動模型。在網絡編程中,所謂的事件當然就是read、write、bind、connect、close等這些動作了。Reactor模型的實現有很多種,下面介紹最基本的三種:
- 單線程版
- 多線程版
- 主從多線程版
Key Word:Java NIO,Reactor模型,Java并發編程,Event-Driven
單線程版本
結構圖(引用自Doug Lea的Scalable IO in Java)如下:
上圖中Reactor是一個典型的事件驅動中心,客戶端發起請求并建立連接時,會觸發注冊在多路復用器Selector上的SelectionKey.OP_ACCEPT事件,綁定在該事件上的Acceptor對象的職責就是接受請求,為接下來的讀寫操作做準備。
Reactor設計如下:
/**
* Reactor
*
* @author wqx
*
*/
public class Reactor implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(Reactor.class);
private Selector selector;
private ServerSocketChannel ssc;
private Handler DEFAULT_HANDLER = new Handler(){
@Override
public void processRequest(Processor processor, ByteBuffer msg) {
//NOOP
}
};
private Handler handler = DEFAULT_HANDLER;
/**
* 啟動階段
* @param port
* @throws IOException
*/
public Reactor(int port, int maxClients, Handler serverHandler) throws IOException{
selector = Selector.open();
ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(port));
this.handler = serverHandler;
SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT);
sk.attach(new Acceptor());
}
/**
* 輪詢階段
*/
@Override
public void run() {
while(!ssc.socket().isClosed()){
try {
selector.select(1000);
Set<SelectionKey> keys;
synchronized(this){
keys = selector.selectedKeys();
}
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
dispatch(key);
it.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
close();
}
public void dispatch(SelectionKey key){
Runnable r = (Runnable)key.attachment();
if(r != null)
r.run();
}
/**
* 用于接受TCP連接的Acceptor
*
*/
class Acceptor implements Runnable{
@Override
public void run() {
SocketChannel sc;
try {
sc = ssc.accept();
if(sc != null){
new Processor(Reactor.this,selector,sc);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void close(){
try {
selector.close();
if(LOG.isDebugEnabled()){
LOG.debug("Close selector");
}
} catch (IOException e) {
LOG.warn("Ignoring exception during close selector, e=" + e);
}
}
public void processRequest(Processor processor, ByteBuffer msg){
if(handler != DEFAULT_HANDLER){
handler.processRequest(processor, msg);
}
}
}
上面是典型的單線程版本的Reactor實現,實例化Reactor對象的過程中,在當前多路復用器Selector上注冊了OP_ACCEPT事件,當OP_ACCEPT事件發生后,Reactor通過dispatch方法執行Acceptor的run方法,Acceptor類的主要功能就是接受請求,建立連接,并將代表連接建立的SocketChannel以參數的形式構造Processor對象。
Processor的任務就是進行I/O操作。
下面是Processor的源碼:
/**
* Server Processor
*
* @author wqx
*/
public class Processor implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(Processor.class);
Reactor reactor;
private SocketChannel sc;
private final SelectionKey sk;
private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);
private ByteBuffer inputBuffer = lenBuffer;
private ByteBuffer outputDirectBuffer = ByteBuffer.allocateDirect(1024 * 64);
private LinkedBlockingQueue<ByteBuffer> outputQueue = new LinkedBlockingQueue<ByteBuffer>();
public Processor(Reactor reactor, Selector sel,SocketChannel channel) throws IOException{
this.reactor = reactor;
sc = channel;
sc.configureBlocking(false);
sk = sc.register(sel, SelectionKey.OP_READ);
sk.attach(this);
sel.wakeup();
}
@Override
public void run() {
if(sc.isOpen() && sk.isValid()){
if(sk.isReadable()){
doRead();
}else if(sk.isWritable()){
doSend();
}
}else{
LOG.error("try to do read/write operation on null socket");
try {
if(sc != null)
sc.close();
} catch (IOException e) {}
}
}
private void doRead(){
try {
int byteSize = sc.read(inputBuffer);
if(byteSize < 0){
LOG.error("Unable to read additional data");
}
if(!inputBuffer.hasRemaining()){
if(inputBuffer == lenBuffer){
//read length
inputBuffer.flip();
int len = inputBuffer.getInt();
if(len < 0){
throw new IllegalArgumentException("Illegal data length");
}
//prepare for receiving data
inputBuffer = ByteBuffer.allocate(len);
}else{
//read data
if(inputBuffer.hasRemaining()){
sc.read(inputBuffer);
}
if(!inputBuffer.hasRemaining()){
inputBuffer.flip();
processRequest();
//clear lenBuffer and waiting for next reading operation
lenBuffer.clear();
inputBuffer = lenBuffer;
}
}
}
} catch (IOException e) {
LOG.error("Unexcepted Exception during read. e=" + e);
try {
if(sc != null)
sc.close();
} catch (IOException e1) {
LOG.warn("Ignoring exception when close socketChannel");
}
}
}
/**
* process request and get response
*
* @param request
* @return
*/
private void processRequest(){
reactor.processRequest(this,inputBuffer);
}
private void doSend(){
try{
/**
* write data to channel:
* step 1: write the length of data(occupy 4 byte)
* step 2: data content
*/
if(outputQueue.size() > 0){
ByteBuffer directBuffer = outputDirectBuffer;
directBuffer.clear();
for(ByteBuffer buf : outputQueue){
buf.flip();
if(buf.remaining() > directBuffer.remaining()){
//prevent BufferOverflowException
buf = (ByteBuffer) buf.slice().limit(directBuffer.remaining());
}
//transfers the bytes remaining in buf into directBuffer
int p = buf.position();
directBuffer.put(buf);
//reset position
buf.position(p);
if(!directBuffer.hasRemaining()){
break;
}
}
directBuffer.flip();
int sendSize = sc.write(directBuffer);
while(!outputQueue.isEmpty()){
ByteBuffer buf = outputQueue.peek();
int left = buf.remaining() - sendSize;
if(left > 0){
buf.position(buf.position() + sendSize);
break;
}
sendSize -= buf.remaining();
outputQueue.remove();
}
}
synchronized(reactor){
if(outputQueue.size() == 0){
//disable write
disableWrite();
}else{
//enable write
enableWrite();
}
}
} catch (CancelledKeyException e) {
LOG.warn("CancelledKeyException occur e=" + e);
} catch (IOException e) {
LOG.warn("Exception causing close, due to " + e);
}
}
public void sendBuffer(ByteBuffer bb){
try{
synchronized(this.reactor){
if(LOG.isDebugEnabled()){
LOG.debug("add sendable bytebuffer into outputQueue");
}
//wrap ByteBuffer with length header
ByteBuffer wrapped = wrap(bb);
outputQueue.add(wrapped);
enableWrite();
}
}catch(Exception e){
LOG.error("Unexcepted Exception: ", e);
}
}
private ByteBuffer wrap(ByteBuffer bb){
bb.flip();
lenBuffer.clear();
int len = bb.remaining();
lenBuffer.putInt(len);
ByteBuffer resp = ByteBuffer.allocate(len+4);
lenBuffer.flip();
resp.put(lenBuffer);
resp.put(bb);
return resp;
}
private void enableWrite(){
int i = sk.interestOps();
if((i & SelectionKey.OP_WRITE) == 0){
sk.interestOps(i | SelectionKey.OP_WRITE);
}
}
private void disableWrite(){
int i = sk.interestOps();
if((i & SelectionKey.OP_WRITE) == 1){
sk.interestOps(i & (~SelectionKey.OP_WRITE));
}
}
}
其實Processor要做的事情很簡單,就是向selector注冊感興趣的讀寫時間,OP_READ或OP_WRITE,然后等待事件觸發,做相應的操作。
@Override
public void run() {
if(sc.isOpen() && sk.isValid()){
if(sk.isReadable()){
doRead();
}else if(sk.isWritable()){
doSend();
}
}else{
LOG.error("try to do read/write operation on null socket");
try {
if(sc != null)
sc.close();
} catch (IOException e) {}
}
}
而doRead()和doSend()方法稍微復雜了一點,這里其實處理了用TCP協議進行通信時必須要解決的問題:TCP粘包拆包問題
TCP粘包拆包問題
我們都知道TCP協議是面向字節流的,而字節流是連續的,無法有效識別應用層數據的邊界。如下圖:
上圖顯示的應用層有三個數據包,D1,D2,D3.當應用層數據傳到傳輸層后,可能會出現粘包拆包現象。
TCP協議的基本傳輸單位是報文段,而每個報文段最大有效載荷是有限制的,一般以太網MTU為1500,去除IP頭20B,TCP頭20B,那么剩下的1460B就是傳輸層最大報文段的有效載荷。如果應用層數據大于該值(如上圖中的數據塊D2),那么傳輸層就會進行拆分重組。
解決方案
- 消息定長(通信雙方發送的消息固定長度,缺點很明顯:浪費可恥?。。。?/li>
- 每個消息之間加分割符(缺點:消息編解碼耗時,并且如果消息體中本省就包含分隔字符,需要進行轉義,效率低)
- 每個數據包加個Header?。。。╤eader中指定后面數據的長度,這不就是Tcp、Ip協議通用的做法么。。。哈哈)
采用方案三
示意圖如下:
header區占用4B,內容為數據的長度。too simple。。。-_-
理論有了,下面具體分析下Read、Write的實現過程:
doRead
inputBuffer負責接受數據,lenBuffer負責接受數據長度,初始化的時候,將lenBuffer賦給inputBuffer,定義如下:
private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);
private ByteBuffer inputBuffer = lenBuffer;
- 如果inputBuffer == lenBuffer,那么從inputBuffer中讀取出一個整型值len,這個值就是接下來要接受的數據的長度。同時分配一個大小為len的內存空間,并復制給inputBuffer,準備接受數據!??!
private void doRead(){
try {
int byteSize = sc.read(inputBuffer);
if(byteSize < 0){
LOG.error("Unable to read additional data");
}
if(!inputBuffer.hasRemaining()){
if(inputBuffer == lenBuffer){
//read length
inputBuffer.flip();
int len = inputBuffer.getInt();
if(len < 0){
throw new IllegalArgumentException("Illegal data length");
}
//prepare for receiving data
inputBuffer = ByteBuffer.allocate(len);
else{...}
- 如果inputBuffer != lenBuffer,那么開始接受數據吧!
if(inputBuffer == lenBuffer){
//。。。
}else{
//read data
if(inputBuffer.hasRemaining()){
sc.read(inputBuffer);
}
if(!inputBuffer.hasRemaining()){
inputBuffer.flip();
processRequest();
//clear lenBuffer and waiting for next reading operation
lenBuffer.clear();
inputBuffer = lenBuffer;
}
}
注意:
- 必須保證緩沖區是滿的,即inputBuffer.hasRemaining()=false
- processRequest后,將inputBuffer重新賦值為lenBuffer,為下一次讀操作做準備。
doWrite
用戶調用sendBuffer方法發送數據,其實就是將數據加入outputQueue,這個outputQueue就是一個發送緩沖隊列。
public void sendBuffer(ByteBuffer bb){
try{
synchronized(this.reactor){
if(LOG.isDebugEnabled()){
LOG.debug("add sendable bytebuffer into outputQueue");
}
//wrap ByteBuffer with length header
ByteBuffer wrapped = wrap(bb);
outputQueue.add(wrapped);
enableWrite();
}
}catch(Exception e){
LOG.error("Unexcepted Exception: ", e);
}
}
doSend方法就很好理解了,無非就是不斷從outputQueue中取數據,然后寫入channel中即可。過程如下:
將發送隊列outputQueue中的數據寫入緩沖區outputDirectBuffer:
- 清空outputDirectBuffer,為發送數據做準備
- 將outputQueue數據寫入outputDirectBuffer
- 調用socketChannel.write(outputDirectBuffer);將outputDirectBuffer寫入socket緩沖區
執行步驟2的時候,我們可能會遇到這么幾種情況:
1.某個數據包大小超過了outputDirectBuffer剩余空間大小
2.outputDirectBuffer已被填滿,但是outputQueue仍有待發送的數據
執行步驟3的時候,也可能出現下面兩種情況:
1.outputDirectBuffer被全部寫入socket緩沖區
2.outputDirectBuffer只有部分數據或者壓根就沒有數據被寫入socket緩沖區
實現過程可以結合源碼,這里重點分析下面幾個點:
為什么需要重置buf的position
int p = buf.position();
directBuffer.put(buf);
//reset position
buf.position(p);
寫入directBuffer的數據是即將被寫入SocketChannel的數據,問題就在于:當我們調用
int sendSize = sc.write(directBuffer);
的時候,directBuffer中的數據都被寫入Channel了嗎?明顯是不確定的(具體可以看java.nio.channels.SocketChannel.write(ByteBuffer src)的doc文檔)
上面的問題如何解決
思路很簡單,根據write方法返回值sendSize,遍歷outputQueue中的ByteBuffer,根據buf.remaining()和sendSize的大小,才可以確定buf是否真的被發送了。如下所示:
while(!outputQueue.isEmpty()){
ByteBuffer buf = outputQueue.peek();
int left = buf.remaining() - sendSize;
if(left > 0){
buf.position(buf.position() + sendSize);
break;
}
sendSize -= buf.remaining();
outputQueue.remove();
}
網絡通信基本解決,上面的處理思路是參照Zookeeper網絡模塊的實現,有興趣可以看Zookeeper相應源碼。
測試
Server端:
public class ServerTest {
private static int PORT = 8888;
public static void main(String[] args) throws IOException, InterruptedException {
Thread t = new Thread(new Reactor(PORT,1024,new MyHandler()));
t.start();
System.out.println("server start");
t.join();
}
}
用戶自定義Handler:
public class MyHandler implements Handler {
@Override
public void processRequest(Processor processor, ByteBuffer msg) {
byte[] con = new byte[msg.remaining()];
msg.get(con);
String str = new String(con,0,con.length);
String resp = "";
switch(str){
case "request1":resp = "response1";break;
case "request2":resp = "response2";break;
case "request3":resp = "response3";break;
default :resp = "";
}
ByteBuffer buf = ByteBuffer.allocate(resp.getBytes().length);
buf.put(resp.getBytes());
processor.sendBuffer(buf);
}
}
client端
public class ClientTest {
private static String HOST = "localhost";
private static int PORT = 8888;
public static void main(String[] args) throws IOException {
Client client = new Client();
client.socket().setTcpNoDelay(true);
client.connect(
new InetSocketAddress(HOST,PORT));
ByteBuffer msg;
for(int i = 1; i <= 3; i++){
msg = ByteBuffer.wrap(("request" + i).getBytes());
System.out.println("send-" + "request" + i);
ByteBuffer resp = client.send(msg);
byte[] retVal = new byte[resp.remaining()];
resp.get(retVal);
System.out.println("receive-" + new String(retVal,0,retVal.length));
}
}
}
輸出:
send-request1
receive-response1
send-request2
receive-response2
send-request3
receive-response3
Client是一個客戶端工具類,簡單封裝了發送ByteBuffer前,添加header的邏輯。詳見源碼。Client.java
總結
在這種實現方式中,dispatch方法是同步阻塞的?。?!所有的IO操作和業務邏輯處理都在NIO線程(即Reactor線程)中完成。如果業務處理很快,那么這種實現方式沒什么問題,不用切換到用戶線程。但是,想象一下如果業務處理很耗時(涉及很多數據庫操作、磁盤操作等),那么這種情況下Reactor將被阻塞,這肯定是我們不希望看到的。解決方法很簡單,業務邏輯進行異步處理,即交給用戶線程處理。
下面分析下單線程版的Reactor模型的缺點:
- 自始自終都只有一個Reactor線程,缺點很明顯:Reactor意外掛了,整個系統也就無法正常工作,可靠性太差。
- 單線程的另外一個問題是在大負載的情況下,Reactor的處理速度必然會成為系統性能的瓶頸。
如何解決上述問題呢?下文詳解Reactor多線程版本