類CNCClient客戶機用來實現“發布—訂閱”和“組播”功能,主要接口(公有函數)如下:
class CNCClient
{ public:
BOOL Connect(…); // 連接服務器
BOOL Disconnect(); // 與服務器斷開連接
BOOL PublishData(…); // 向服務器發布數據
BOOL QueryData(…); // 向服務器查詢數據
BOOL SubscribeData(…); // 向服務器訂閱數據
GROUPIP QueryGroupIP(…); // 向服務器查詢組播地址
DWORD MulticastMessage(…); // 發送組播消息
virtual void MessageResponse(…);// 響應組播消息
…
};
一、客戶程序的“發布”協議
客戶機向服務器發布的每個數據報均含有數據類型、工作組名稱、數據名稱、生命期和數據長度的信息。報文格式如圖5.16所示,數據結構見 DataPublish :
struct DataPublish
{
BYTE iDataType; // 2 個字節數據類型,宏定義為DATA_PUBLISH
char strGroupName[16]; // 16個字節的工作組名字
char strDataName[16]; // 16個字節的數據名字
DWORD dwLifeTime; // 4 個字節的數據生命期,以秒為單位
DWORD dwLength; // 4 個字節的數據內容的長度
char *pchContent; // 數據內容
};
data type group name data name life time length content
圖5.16 用于發布的報文格式
二、客戶程序的“訂閱”協議
客戶機向服務器訂閱數據分兩步實現:
(1)先調用函數QueryData向服務器發送一個 DataQuery格式的報文,用于查詢要訂閱的數據是否存在。
struct DataQuery
{
BYTE iDataType; // 2 個字節數據類型,宏定義為DATA_QUERY
char strGroupName[16]; // 16個字節的工作組名字
char strDataName[16]; // 16個字節的數據名字
};
(2)服務器接收到查詢時,按照 DataQuery結構中的strGroupName和strDataName進行搜索。如果該數據不存在,Server向Client發送一個FALSE標志。如果該數據存在,服務器先向客戶機發送一個TRUE標志,之后立即再向客戶機發送該數據(DataPublish格式)。
如果客戶機得到TRUE標志的查詢結果,就調用函數SubscribeData來接收服務器發送過來的數據。
三、客戶程序的“組播”協議
客戶機先調用函數QueryGroupIP向服務器發送一個GroupAddress格式的報文,用于查詢組播地址。服務器返回相應的十進制點分式的IP地址。
struct GroupAddress
{
BYTE iDataType; // 2 個字節數據類型,宏定義為GROUP_ADDRESS
char strGroupName[16]; // 16個字節的工作組名字
};
客戶機調用函數MulticastMessage向指定的組(根據組播地址)播放消息。組播的數據報結構 DataMulticast定義如下:
struct DataMulticast
{
DWORD dwContentType; // 組播的數據報類型,由用戶定義
char *pchContent; // 組播的數據報內容,由用戶定義
};
如果客戶機接收到組播的消息,將自動調用函數MessageResponse來響應該消息。MessageResponse是虛函數,它將根據dwContentType信息決定如何處理到來的組播消息,具體功能由用戶定義。
5.5.3.2 CNC 服務器的設計
一、數據結構
CNC 服務器的數據結構主要由三部分組成:
(1)一張用于管理組播地址的鏈表。組播地址由服務器動態生成,客戶機可以向服務器查詢任意組的組播地址。
(2)一張用于管理線程指針的鏈表。服務器采用多線程并發處理技術,使客戶機獲得最快的響應。
(3)每個組都有一張用于管理“發布—訂閱”的數據的bbbb表。由于同一時刻,系統可能存在多個生產者與消費者,數據的存入、取出速度成為服務器性能的重要指標。bbbb表可以提供比鏈表更快的數據檢索速度。bbbb表中的數據項結構見DataElement :
struct DataElement
{
char strGroupName[16]; // 工作組的名稱
char strDataName[16]; // 數據的名稱
BYTE iStorageType; // 存儲類型: STORAGE_FILE 或 STORAGE_MEMORY
ColeDateTime TimeToDie; // 作廢時刻
BOOL bLock; // 鎖定標志: TRUE 或 FALSE
DWORD dwLength; // 數據的長度
char *pchContent; // 數據內容
};
存儲類型(iStorageType)的用途:把數據全部保存在內存中將非常消耗服務器的內存資源,在很多情況下是沒有必要的。為了提高內存的使用效率,服務器僅把生命期較短或者長度較短的數據保存在內存中(即為STORAGE_MEMORY類型),而把生命期較長或者長度較長的數據保存在文件中(即為STORAGE_FILE類型)。
作廢時刻(TimeToDie)的用途:客戶機發布的數據均指定了生命期,服務器在接收到數據時即可計算出作廢時刻。服務器將定期掃描bbbb表,若發現有數據超出作廢時刻(并且沒有被鎖定),即可刪除此數據。
鎖定標志(bLock)的用途:很多客戶機可能同時訂閱某個數據,而該數據可能已超出作廢時刻即將被刪除。為避免沖突,規定只要有客戶機訂閱數據,就用iLock標志來鎖定此數據,直到訂閱完成后才消除鎖定。
二、多線程并發技術
服務器有一個主線程和多個子線程。主線程負責客戶機的入連接請求,然后創建一個子線程來處理這個TCP連接。每個子線程按照CNC API的協議與客戶機通訊。由于有多個子線程共享服務器中的數據,多線程對共享資源的同步訪問成為實現的難點。CNC 主要采用了關鍵區、互斥對象等同步手段解決這個問題。
三、Winsock的使用
CNC 1.0運行于bbbbbbs 9x/NT系統下,底層的網絡通訊程序用Winsock編寫。Winsock有兩種工作方式:阻塞方式和非阻塞方式。阻塞方式的優點是編程簡單,可靠性好。缺點是容易使應用程序阻塞住,不能處理其它事務。非阻塞方式是利用bbbbbbs 消息機制實現的。優點是在數據到來的時候,系統向應用程序窗口發送消息,使得應用程序不必總在等待數據,提高了工作效率。缺點是在發送和接收數據時,應用程序并不將事情做完(不阻塞),以至于應用程序要維護復雜的狀態機。
鑒于阻塞方式和非阻塞方式各有優缺點,CNC 服務器采用了混合方式。主線程采用非阻塞的消息驅動方式,可以快速響應客戶機的入連接。在子線程中,仍采用非阻塞的消息驅動方式接受客戶機的請求,只有在響應請求時,采用阻塞的方式一次性地完成數據的發送或接收。
5.5.4 應用示例
圖5.17、圖5.18是參加協同工作的兩個客戶程序示例,這兩個程序均用Intra3D 2.0 和CNC 1.0開發。圖5.17的客戶程序向CNC 服務器訂閱 .3ds和 .obj格式的多邊形模型數據并執行交互式繪制。圖5.18的客戶程序向CNC 服務器訂閱商業統計圖形數據并執行交互式繪制。另有一個客戶機(數據源)向CNC 服務器發布各種數據,并用組播來通知各個客戶機當前發布了什么數據(短消息)。
5.6 小 結
讓我們用著名3D游戲軟件Quake的設計師Michael Abrash 的話總結本章:“所有真正杰出的設計一旦被設計好,看起來都是那么的簡單和顯而易見。但是在獲得杰出設計的過程中,需要付出令人難以置信的努力。”[Abrash 1998]
圖5.17 繪制.3ds和.obj模型的客戶程序
圖5.18 繪制商業統計圖形的客戶程序










