基於ACE Proactor框架下高併發、大容量吞吐程序設計

本文來自:http://my.oschina.net/JJREN/blog/51966

Reactor與 Proactor 
基本概念
在高性能的I/O設計中,有兩個比較著名的模式Reactor和Proactor模式,其中Reactor模式用於同步I/O,而Proactor運用於異步I/O操作。

       在比較這兩個模式之前,我們首先的搞明白幾個概念,

什麼是阻塞和非阻塞 
阻塞和非阻塞是針對於進程在訪問數據的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作函數的實現方式。

阻塞方式下讀取或者寫入函數將一直等待。

非阻塞方式下,讀取或者寫入函數會立即返回一個狀態值。


什麼是同步和異步 
同步和異步是針對應用程序和內核的交互而言的。
同步指的是用戶進程觸發IO操作並等待或者輪詢的去查看IO操作是否就緒。
異步是指用戶進程觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知。

一般來說I/O模型可以分爲:同步阻塞,同步非阻塞,異步阻塞,異步非阻塞。

讓我們來看一下每種不同I/O模型的具體描述

   同步阻塞 IO: 
   在此種方式下,用戶進程在發起一個IO操作以後,必須等待IO操作的完成,只有當真正完成了IO操作以後,用戶進程才能運行。JAVA傳統的IO模型屬於此種方式!
   同步非阻塞IO: 
在此種方式下,用戶進程發起一個IO操作以後邊可返回做其它事情,但是用戶進程需要時不時的詢問IO操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的CPU資源浪費。其中目前JAVA的NIO就屬於同步非阻塞IO。
   異步阻塞IO: 
此種方式下,應用發起一個IO操作以後,不等待內核IO操作的完成,等內核完成IO操作以後會通知應用程序,這其實就是同步和異步最關鍵的區別,同步必須等待或者主動的去詢問IO是否完成,那麼爲什麼說是阻塞的呢?因爲此時是通過select系統調用來完成的,而select函數本身的實現方式是阻塞的,而採用select函數有個好處就是它可以同時監聽多個文件句柄,從而提高系統的併發性!
   異步非阻塞IO: 
此種方式下,用戶進程只需要發起一個IO操作然後立即返回,等IO操作真正的完成以後,應用程序會得到IO操作完成的通知,此時用戶進程只需要對數據進行處理就好了,不需要進行實際的IO讀寫操作,因爲真正的IO讀取或者寫入操作已經由內核完成了。目前Java中還沒有支持此種IO模型。   

阻塞型I/O意味着控制權只有到調用操作結束後纔會回到調用者手裏.結果調用者被阻塞了,這段時間了做不了任何其它事情。 更鬱悶的是,在等待IO結果的時間裏,調用者所在線程此時無法騰出手來去響應其它的請求,這真是太浪費資源了。拿read() 操作來說吧,調用此函數的代碼會一直僵在此處直至它所讀的socket緩存中有數據到來。

相比之下,非阻塞同步是會立即返回控制權給調用者的。調用者不需要等等,它從調用的函數獲取兩種結果:要麼此次調用成功進行了;要麼系統返回錯誤標識告訴調用者當前資源不可用,你再等等或者再試一次看吧。比如read()操作,如果當前socket無數據可讀,則立即返回EWOULBLOCK/EAGAIN,告訴調用read()者"數據還沒準備好,你稍後再試".。

在非阻塞異步調用中,稍有不同。調用函數在立即返回時,還告訴調用者,這次請求已經開始了。系統會使用另外的資源或者線程來完成這次調用操作,並在完成的時候知會調用者(比如通過回調函數)。拿Windows的ReadFile()或者POSIX的aio_read()來說,調用它之後,函數立即返回,操作系統在後臺同時開始讀操作。

在以上三種IO形式中,非阻塞異步是性能最高、伸縮性最好的。搞清楚了以上概念以後,我們再回過頭來看看,Reactor模式和Proactor模式。

此文詳細的闡述了基於TCP高性能的GOLDEN數據服務器模塊的設計以及解決方案 ,我們在文章的後面就不再提及阻塞式的方案了,因爲阻塞式I/O實在是缺少可伸縮性,性能也達不到高性能服務器的要求。

兩種IO多路複用方案:Reactor和 Proactor
一般情況下,I/O複用機制需要事件分離器(event demultiplexor ). 事件分離器的作用,就是將那些讀寫事件源分發給各讀寫事件的處理者,就像送快遞的在樓下喊:誰的什麼東西送了,快來拿吧。開發人員在開始的時候需要在事件分離器那裏註冊感興趣的事件,並提供相應的事件處理器(event handlers),或者是回調函數;事件分離器在適當的時候會將請求的事件分發給這些handler或者回調函數。

涉及到事件分離器的兩種模式稱爲:Reactor和Proactor。Reactor模式是基於同步I/O的,而Proactor模式是和異步I/O相關的。在Reactor模式中,事件分離者等待某個事件或者是應用或者是某個操作的狀態發生(比如文件描述符可讀寫,或者是socket可讀寫),事件分離者就把這個事件傳給事先註冊的事件處理器或者事件處理函數或者回調函數,由後者來做實際的讀寫操作。 

而在Proactor模式中,事件處理器(或者由事件分離器代爲)直接發起一個異步讀寫操作(相當於請求),而實際的工作是由操作系統來完成的。發起時,需要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分離器得知了這個請求,它默默等待這個請求的完成,然後轉發完成事件給相應的事件處理器或者事件處理函數或者回調。舉例來說,在Windows上事件處理器投遞了一個異步IO操作(稱有overlapped的技術),事件分離器等IOCompletion事件完成 ,這種異步模式的典型實現是基於操作系統底層異步API的,所以我們可稱之爲“系統級別”的或者“真正意義上”的異步,因爲具體的讀寫是由操作系統代勞的。 

舉另外個例子來更好地理解Reactor與Proactor兩種模式的區別。這裏我們只關注read操作,因爲write操作也是差不多的。下面是Reactor的做法: 

某個事件處理器宣稱它對某個socket上的讀事件很感興趣; 

事件分離者等着這個事件的發生; 

當事件發生了,事件分離器被喚醒,這負責通知先前那個事件處理器;

事件處理器收到消息,於是去那個socket上讀數據了. 如果需要,它再次宣稱對這個socket上的讀事件感興趣,一直重複上面的步驟;

下面再來看看真正意義的異步模式Proactor是如何做的: 

事件處理器直接投遞發一個讀操作(當然,操作系統必須支持這個異步操作)。這個時候,事件處理器根本不關心讀事件,它只管發這麼個請求,它魂牽夢縈的是這個讀操作的完成事件。這個事件處理器很拽,發個命令就不管具體的事情了,只等着別人(系統)幫他搞定的時候給他回個話。
事件分離器等着這個讀事件的完成(比較下與Reactor的不同);
當事件分離器默默等待完成事情到來的同時,操作系統已經在一邊開始幹活了,它從目標讀取數據,放入用戶提供的緩存區中,最後通知事件分離器,這個事情我搞完了;
事件分離器通知之前的事件處理器: 你吩咐的事情搞定了;
事件處理器這時會發現想要讀的數據已經乖乖地放在他提供的緩存區中,想怎麼處理都行了。如果有需要,事件處理器還像之前一樣發起另外一個讀操作,和上面的幾個步驟一樣。 


現行做法
開源C++開發框架ACE(Adaptive Communication Enviromen) 提供了大量平臺獨立的底層併發支持類(線程、互斥量等).同時在更高一層它也提供了獨立的幾組C++類,用於實現Reactor及Proactor模式。 儘管它們都是平臺獨立的單元,但他們都提供了不同的接口. 

ACE Proactor在MS-Windows上無論是性能還在健壯性都更勝一籌,這主要是由於Windows提供了一系列高效的底層異步API。

不幸的是,並不是所有操作系統都爲底層異步提供健壯的支持。舉例來說,許多Unix系統就有麻煩. ACE中的Proactor在Unix上是使用Posix標準實現的異步操作,Posix中有一個AIO,Proactor使用AIO實現異步傳輸。但Linux在2.6以前版本中不支持AIO,而在2.6版本以後,部分支持AIO。就因爲這個部分支持,所以,Posix的子類不能正常工作。因此,ACE Reactor可能是Unix系統上更合適的解決方案.正因爲系統底層的支持力度不一,爲了在各系統上有更好的性能,開發者不得不維護獨立的好幾份代碼:爲Windows準備的ACE Proactor以及爲Unix系列提供的ACE Reactor。


就像我們提到過的,真正的異步模式需要操作系統級別的支持。由於事件處理器及操作系統交互的差異,爲Reactor和Proactor設計一種通用統一的外部接口是非常困難的。這也是設計通行開發框架的難點所在。 

ACE Proactor 框架
怎樣發送和接收數據
ACEProactor框架包含了一組高度相關的類,其數量相對較多,我在進行以下描述的時候不可能按照順序討論它們,而又不進行提前引用。到最後我會描述完所有這些類。下面這些類給出了ACE Proactor框架的各個類以及它們之間的關係。可以把這個圖1-1當作描述ACE Proactor框架實際應用的範本。注意:類名中以ACE_開始的類名稱是ACE Procator框架中包含的類,而以golden_開始的類名稱是實際應用範本提供的類。

下面的代碼聲明瞭一個類,它所完成的基本工作是處理接收和發送數據。




圖1.1 ACE Proactor框架中的類


#include "ace/Asynch_IO.h"
class golden_aio_handler : public ACE_Service_Handler
{
public :
golden_aio_handler (golden_aio_acceptor *acc = 0) ;


virtual void open ( ACE_HANDLE new_handle,


ACE_Message_Block &message_block ) ;


virtual void handle_read_stream(
const ACE_Asynch_Read_Stream::Result &result); 


virtual void handle_write_stream(
const ACE_Asynch_Write_Stream::Result &result);


private:

ACE_Asynch_Read_Stream reader_; 

ACE_Asynch_Write_Stream writer_;

} ; 


這段代碼首先包含了一些必需的頭文件,以引入這個例子使用的ACE Proactor框架類:

ACE_Service_Handler 在Proactor框架中創建事件處理器所用的目標類 。

ACE_Handler ACE_Service_Handler的父類,定義了通過ACE_Proactor框架處理異步I/O完成事件所需要的接口。

ACE_Asynch_Read_Stream 用於在已經連接的TCP/IP socket上發起讀操作的I/0工廠類。

ACE_Asynch_Write_Stream 用於在已經連接的TCP/IP socket上發起寫操作的I/0工廠類。

Result 每個I/O工廠類都把Result定義爲嵌在自己內部的類,用以保存該工廠發起的每個操作的結果。所有的Result類都從ACE_Asynch_Result派生,並且增加了專用於它們所針對的I/O類型的數據和方法。因爲每個異步I/O操作的發起和完成都是分離的、不同的事情,需要有一種機制來“記住”操作的參數,並且連同結果一起吧這些參數轉交給完成處理器。

設置事件處理器併發起I/O
當TCP連接打開時,我們應該把新socket的句柄傳給事件處理器對象,在這個例子中是golden_aio_handler。把句柄放在事件處理器裏是有益的,原因如下:
它是socket的生命期一個方便的控制點,因爲它是連接工廠的目標。

I/O操作最有可能從這個類發起。
在使用ACE_Proactor框架的異步連接建立類時golden_aio_handler::open()掛鉤方法會在新連接建立時被調用。下面是我們程序中的open()掛鉤:

void
golden_aio_handler::open(ACE_HANDLE new_handle, ACE_Message_Block &)
{
this->handle(new_handle);


//打開異步讀寫
reader_.open (*this, new_handle, 0, proactor ());
writer_.open (*this, new_handle, 0, proactor ());

//準備讀的緩衝區
ACE_NEW_NORETURN(mblk_, ACE_Message_Block (SIZEOF_HEADER_WITH_CRC));

if (reader_.read (*mblk_,SIZEOF_HEADER_WITH_CRC) <0)
{


delete this ;

}
}

在一開始,我們使用繼承而得到的ACE_Handler::handle()方法保存新socket的句柄。該方法把句柄存儲在一個方便的地方,以便在析構函數~golden_aio_handler()訪問或者用於其他用途。這是在這個類中實現的socket句柄生命期管理的一部分。
要發起I/O,必須初始化所需的I/O工廠對象。在存儲了socket句柄之後,open()方法會初始化reader_和writer_ I/O工廠對象,爲發起I/O操作做準備。兩個類的open()方法都是一樣的:

int open (ACE_Handler &handler,
ACE_HANDLE handle = ACE_INVALID_HANDLE,

const void *completion_key = 0,

ACE_Proactor *proactor = 0);

第一個參數表示工廠對象所發起的操作的完成事件處理器裏。當通過工廠對象發起的I/O操作完成時,ACE_Proactor框架會回調這個對象。這也是爲什麼該處理器對象叫做完成事件處理器的原因。在我們的程序中,golden_aio_handler對象是ACE_Handler的後代,即是讀操作也是寫操作的完成事件處理器,所以*this被用作處理器參數。handle是新傳入的socket句柄,completion_key參數只適用於windows默認傳入0即可,proactor參數會傳入一個在進程範圍的ACE_Procator單體對象。

程序中的open()掛鉤方法所做的最後一件事情,是調用ACE_Asynch_Read_Stream::read()方法,從而在新的socket上發起一個讀操作。ACE_Asynch_Read_Stream::read()函數如下:

int read (ACE_Message_Block &message_block,

size_t num_bytes_to_read,

const void *act = 0,

int priority = 0,

int signal_number = ACE_SIGRTMIN);

爲傳輸指定一個ACE_Message_Block ,使得緩衝區管理變得更爲容易,因爲可以利用ACE_Message_Block的各種能力,以及它與ACE的其他部分的集成。在發起讀操作時,數據會被讀入開始於數據塊的寫指針處在的塊中,因爲要被讀取的數據將被寫入塊中。

完成I/O操作
ACE_Proactor框架是基於事件的框架。I/O工廠登記“每個操作”與“該操作完成時應回調的完成事件處理器”之間建立關聯。當讀取完成時,ACE_Proactor框架會調用ACE_Handler::handle_read_stream()掛鉤方法:

void golden_aio_handler::handle_read_stream(

const ACE_Asynch_Read_Stream::Result &result) 

{ ACE_Asynch_Read_Stream::Result &result

if (!result.success () || result.bytes_transferred () == 0)

delete this;

else if (result.bytes_transferred () < result.bytes_to_read ())
{

if (reader_.read (*mblk_, result.bytes_to_read () - result.bytes_transferred ()) < 0)

delete this ;

}

else if (mblk_->length () == SIZEOF_HEADER_WITH_CRC) 

handle_msg_header();

else 




if (handle_msg_pack()<0) 


delete this ;


}


}


傳入的ACE_Asynch_Read_Stream::Result指向的是用於保存讀取操作結果的對象。每個I/O工廠類都會定義自己的Result類,即用於保存每個操作發起時所用的參數,又用於保存操作的結果。


如果讀操作讀取了任何數據,處理接收read到的報文數據包用handle_msg_pack函數,然後發起一個寫操作,把數據處理結果返回給對端。當寫操作完成時,ACE_Proactor框架調用下面的handle_write_stream方法:


void golden_aio_handler::handle_write_stream(const ACE_Asynch_Write_Stream::Result &result) 





if(reader_.read (*mblk_,SIZEOF_HEADER_WITH_CRC) < 0)


delete this;


}


不管寫操作是否成功完成,在該操作中使用的消息塊都會釋放。如果socket出了問題,先前發起的讀操作也會完成並出錯,而handle_read_stream()會清理對象和socket句柄。






圖1-2給出了本程序事件序列。


建立連接
ACE提供裏兩個工廠類,用於通過ACE_Proactor框架前攝式地建立TCP/IP連接:


ACE_Asynch_Acceptor , 用於發起被動的連接建立


ACE_Asynch_Connector , 用於發起主動的連接建立




圖1.2 ACE Proactor異步回調序列圖


當使用其中一個類建立TCP/IP連接時,ACE_Proactor框架會創建一個從ACE_Service_Handler派生的事件服務處理器,比如golden_aio_handler,用以處理新連接。ACE_Service_Handler類是


ACE_Proactor框架中所有用異步方式連接的服務的基類,從ACE_Handler派生,所以服務類也可以處理在服務中發起的I/O操作的完成.


ACE_Asynch_Acceptor是一個相當容易使用的類,它的一個掛鉤方法是一個protected虛方法:make_handler()。Proactor框架調用這個方法獲取一個ACE_Service_Handler對象,用以爲新連接提供服務。下面的代碼說明了這種情況:


golden_aio_handler * golden_aio_acceptor::make_handler (void)


{


///來一個連接,就新增一個句柄。在線程池中處理


golden_aio_handler *ih;


ACE_NEW_RETURN (ih, golden_aio_handler (this), 0);


if (clients_.insert (ih) == -1)


{


delete ih ; 


return NULL ;


}


return ih;


}


return 0 ;


}


ACE_Proactor完成多路分離器
ACE_Proactor類負責驅動ACE_Proactor框架的完成處理,這個類等待完成事件的發生、把這些事件多路分離給相關聯的完成事件處理器,並分派每個完成處理器上適當的掛鉤方法。因此,要讓異步I/O完成事件處理器得以發生----無論是I/O還是連接建立----在Golden Server中都必須運行前攝器的時間循環。這通常很簡單,只需要把下面的代碼插入到應用中就可以了:


int golden_aio::svc()


{


ACE_Proactor::instance()->proactor_run_event_loop ();


return 1 ;


}


可以通過兩種方式來使用ACE_Proactor,如上所示的程序代碼instance(),作爲單體來使用。也可以通過實例化一個或多個實例來使用。這個能力被用於在一個進程中支持多個前攝器。如下代碼所示,這是應用於鏡像發送和鏡像接收的前攝器


int golden_mirror_sender::svc()


{


proactor_sender_->proactor_run_event_loop ();


return 1 ;


}


int golden_mirror_receiver::svc()


{


proactor_recviver_->proactor_run_event_loop ();


return 1 ;





各種操作系統上的異步I/O設施會有很大的不同,爲了在所有這些系統上維持統一的接口和編程方法,ACE_Proactor類使用了Bridge模式來維持靈活性和可擴展性,同時還使得ACE_Proactor框架能夠使用不同的異步I/O實現。


ACE_WIN32_Proactor類是Windows上的ACE_Proactor實現。使用了I/O完成端口進行完成事件檢測。在初始化異步操作工廠時,I/O句柄與前攝器的I/O完成端口被關聯在一起。在這種實現中,windows下的GetQueuedCompletionStatus()函數負責執行事件循環,如下程序代碼


int golden_server::create_proactor() 





ACE_Proactor::instance()->close_singleton();


impl_ = new ACE_WIN32_Proactor(0,1);


ACE_Proactor::instance(new ACE_Proactor(impl_,1),1) ;


return 0; 





int golden_server::create_proactor_mirror_recviver()


{


ACE_NEW_RETURN(impl_mirror_recviver_, ACE_WIN32_Proactor(0,1),-1);


ACE_NEW_RETURN(mirror_recviver_proactor_, ACE_Proactor(impl_mirror_recviver_,1),-1);


return 0; 


}


int golden_server::create_proactor_mirror_sender() 


{


ACE_NEW_RETURN(impl_mirror_sender_, ACE_WIN32_Proactor(0,1),-1);


ACE_NEW_RETURN(mirror_sender_proactor_, ACE_Proactor(impl_mirror_sender_,1),-1);


return 0; 


}


線程池
大多數網絡服務器都被設計成能同時處理多個客戶請求。使用反應式事件處理、多個進程和多個線程。在構建多線程服務器時,我們擁有多種選擇,包括:爲每個請求派生一個新線程、爲每個連接/會話派生一個新線程、預先派生一池受管線程,也就是創建一個線程池。在Golden Server設計中我們採用了了線程池的方法。


線程池模型有兩種變種,每種都有不同的性能特徵:


半同步/半異步模型。在這種模型中,一個偵聽會異步的接收請求,並在某個隊列中緩衝它們。另外一組工作者線程負責同步地處理這些請求。


領導者/跟隨着模型。在這種模型總,有一個線程是領導者,其餘線程是在線程池中的跟隨者。當請求到達時,領導者會拾取它,並從跟隨者中選取一個新的領導者,然後繼續處理該請求。因此,在這種模型中,接收請求的線程池就是處理它的線程。


領導者/跟隨者模型中,只用了一組線程等待新請求,並處理請求。一個線程被選作領導者,阻塞在“到來的請求源”上,當請求到達時,領導者線程首先獲取請求,把某個跟隨者提升爲領導者,然後繼續處理所收到的請求。新領導者在請求源上等待新的請求,與此同時舊領導者會處理剛剛收到的請求,一旦就領導者完成處理,它就會作爲跟隨者線程回到線程池的末尾。


領導者/跟隨者模型的一個優點是性能得到了提高,因爲不用進行線程間的上下文切換。但同時這種模型也是複雜的。在程序中單個ACE_Task封裝了線程池中的所有線程。


class golden_aio :public ACE_Task<ACE_SYNCH>


{


public:


golden_aio(int number_of_connection);


/// ACE_TASK的虛擬方法。用來啓動svc。


virtual int open (void * = 0);


///初始化


virtual int init(u_short ,ACE_Proactor* );


///結束並關閉


virtual int fini();


/// Run by a daemon thread to handle deferred processing


virtual int svc (void);


};


int golden_aio::open (void * )


{


return activate (THR_NEW_LWP | THR_JOINABLE |THR_INHERIT_SCHED ,number_of_thread_);


}


int golden_aio::svc()


{


ACE_Proactor::instance()->proactor_run_event_loop ();


return 1 ;


}


每個線程啓動時,首先會通過調用activate(),將任務轉化爲運行在一個或多個線程中的主動對象。主動對象執行任務的svc()掛鉤方法。每個線程在執行proactor_run_event_loop ()調用GetQueuedCompletionStatus ()。 如果沒有消息到達線程會阻塞在GetQueuedCompletionStatus ()函數上,直到有消息到來那麼有一個線程便成了領導者線程。如果領導者線程能很快的處理完所有事情,領導者線程會再次進入等待狀態。如果領導者線程不能馬上處理完,則從跟隨者線程中指定一個新的領導者線程,自己去處理事件,不再當領導者。


單體模型
在Golden Server中golden_user、golden_authorize、golden_startup、golden_prot、golden_system_info、golden_dir_visitor、golden_mirror_sender、golden_mirror_receiver採用單體實例模式。相關單體模式的概念,請自行參考相關手冊和書籍。


如何添加一個接口應用
Goldensdk模塊
打開goldensdk.h頭文件,根據接口類型找到合適的位置,添加接口的聲明、接口的描述註釋。


打開goldensdk.cpp文件,添加接口的空實現。


打開goldensdk.def文件,添加接口名稱。


按照步驟完成Goldenserver模塊


在goldensdk.cpp文實現已經添加的空接口。


Goldenserver模塊
在golden_message_protocol.h中添加自定義的報文數據結構定義和報文消息ID , 命名規則如下GOLDEN_PACK_XXX、GOLDEN_PACK_XXX_RESULT、MESSAGE_XXX、MESSAGE_XXX_RESULT。


在golden_message_protocol.cpp中添加用來計算報文數據結構大小的函數聲明和整編\解編數據流的函數聲明。命名規則如下:sizeof_GOLDEN_PACK_XXX() 、sizeof_GOLDEN_PACK_XXX_RESULT()、int operator<<(ACE_OutputCDR &cdr , const GOLDEN_PACK_XXX &spack) ,int operator<<(ACE_OutputCDR &cdr , const GOLDEN_PACK_XXX_RESUL &spack);


在golden_message_protocol.cpp中添加用來計算報文數據結構大小的函數實現和整編\解編數據流的函數實現。


在golden_message_protocol.cpp中添加全部變量_gstring中的內容,用來描述報文內容。


在goldeserver.h中的golden_aio_handler類中添加處理報文的成員函數聲明。命名規則如下:int msg_xxx() 、int msg_xxx_result() .


在goldeserver.cpp中添加處理報文的成員函數的空實現golden_aio_handler:: msg_xxx(), golden_aio_handler:: msg_xxx_result() .


在goldeserver.cpp的 int golden_aio_handler::handle_msg_pack()函數中添加報文處理項。


實現f步驟中定義的成員函數空實現。


參考文獻
C++網絡編程卷一--- 運用ACE和模式消除複雜性


C++網絡編程卷二--- 基於ACE和框架的系統化複用


ACE程序員指南--- 網絡與系統編程的實用設計模式


設計模式:可複用面向對象軟件的基礎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章