c++|伺服器
C++是標準化的電腦語言,不屬於任何人,而屬於一個標準委員會。STL是支援資料結構和演算法的C++擴充。ATL是微軟擁有和維護的模板庫,使得COM編程更容易。綜合這些技術形成了建立COM組件的一種有效方法,這些COM組件用於ASP頁面。
下面用所有這些技術建立一個COM對象,你將看到VC++ 6.0的嚮導如何提供大量代碼,因此,可以把注意力集中在解決問題上,而不是擔心具體的編程細節。
17.3.1 問題
表現資料的最普通方法是表,列代表欄位的類型,每一行是一條記錄,擁有欄位的值。在文字檔中,表通常由用逗號分開的值(comma-separated values,CSV)組成。
我們將要建立的COM組件以CSV資料作為輸入,高效地儲存它,並提供訪問函數去檢索它。這些資料在COM組件中以STL資料結構表示。在以後部分中,我們會看到怎樣用STL演算法去處理這些資料。另外,在下一章,將介紹怎樣在資料庫中儲存存這些資料。
為了便於說明,假設資料在一個稀疏表中。第一行的欄位是欄位標題,接下來的是一條條資料記錄,記錄的每個欄位對齊於欄位標題。逗號隔離欄位,分行符號(/n)隔離行,空的欄位用兩個逗號表示,即“,,”。
表17-1是一個展開的表的例子。匯出時,逗號會隔離每一個欄位。
17.3.2 設計
這個組件的設計目的是使資料的儲存空間和訪問時間最小。由於資料有可能是稀疏的,即許多欄位是空的,這就有可能使資料的儲存空間最小化。可以通過數值(基於零的索引)訪問資料的行,可以通過欄位名訪問資料的列。例如,要得到表17-1中Keith Moon的Instrument,可以調用GetField (1,"Instrument")。
完成以上工作的工具是STL的vector和map資料結構,這些資料結構是容器,就是說,它們是包含其他對象的一個集合的對象。為了訪問集合中的對象,使用STL遍曆器。
17.3.3 實現
現在你對這個組件的功能已經有了概念,我們將按下面的步驟實現它:
? 建立包含組件的DLL。
? 建立組件。
? 增加屬性。
? 增加方法。
1. 建立DLL和一個組件
選擇ATLCOMAppWizard,建立一個新的VC++項目,然後命名為ASPCOMponents。使用預設的伺服器類型(DLL),不選中複選框,如圖17-1所示。
當完成後,這個嚮導會自動產生一個組件的外殼。這時沒有組件存在,只有DLL根據COM規範所要求的函數存在,可以在ASPCOMponents.cpp檔案中看到這些函數,但不用關心檔案的細節,也不用改變它。當向DLL中添加組件時,嚮導會修改該檔案。
現在將組件放入DLL。在工作區視窗選擇ClassView選項卡,右擊ASPComponents Classes,選擇New ATL Object。然後再選Simple Object,並且在Short Name框中鍵入Tablestorage,如圖17 - 2所示。
除非你打算更深入地研究ATL,否則在Attributes選項卡保持預設狀態,不必改變任何選項。對所有這些選項的意義的詳細描述已超出本書的範圍。Attributes選項卡如圖17-3所示。
點擊OK後,就有了一個用來工作的對象。
到目前為止,VC++嚮導已經完成了所有的工作,如果查看TableStorage.h,你會看到嚮導產生的ATL代碼。在大多數情況下,不必改變這些代碼。實際上,可以只依靠這些代碼,不用瞭解ATL,繼續進行編程。然而,需要對預設的設定做一點改變,使得產生的程式碼能夠編譯。
為了減小工具的大小,當AppWizard建立一個組件時,自動關閉C++的異常處理。因為如果啟用這一功能的話,則需要C運行期庫。STL使用異常處理,所以需要啟用它。在ProjectSettings對話方塊的C/C++選項卡中,下拉Category框選擇C++ Language,確保Settings for框設定為All Configurations,選擇Enable exception handing複選框,如圖1 7 - 4所示。
如果用Release模式編譯可能會得到一個連結錯誤。當編譯一個發行版本的ATL項目時,如果異常處理沒有關閉,會出現這樣的問題。使用異常處理時,需要C運行期啟動代碼;但是在預設下,Release模式的ATL項目定義了_ ATL _MIN_CRT符號,它拒絕啟動代碼。為瞭解決這個問題,在C/C++前置處理器定義中刪除_ ATL_MIN_CRT。詳細資料請看微軟基礎知識庫中的文章Q165259。
我們增加的第一段代碼用於建立STL資料結構,把下列代碼加到TableStorage.h的開頭:
這裡,聲明了兩個映射和一個向量。這個向量實際是兩個映射之一的向量。使用C++的typedef命令定義映射和向量主要是為了使得代碼更易讀,對資料類型描述得更好。
COLUMN_INDEX_MAP將一個字串(列的名稱)映射到一個索引中。INDEX_FIELD_MAP表示資料表中的每一行的資料。由於他可能是一個稀疏行,用一個映射實現是高效的,空欄位不佔用任何空間。INDEX_FIELD_MAP映射COLUMN_INDEX_MAP提供的索引到欄位值。最後,ROW_VECTOR包含代表行的每一個映射。
現在已說明了內部資料結構,可將它們用於使用屬性的外部世界。
2. 增加屬性
我們將增加兩個屬性:行數屬性和列數屬性。
在項目工作區選擇ClassView選項卡,右擊ITableStorage介面。在菜單中選擇Add Property,按圖1 7 - 5填寫對話方塊。選擇Get Function複選框,而不選擇Put Function複選框,使得它為唯讀屬性。
產生返回這一屬性的get_numRows方法。這就是說可有產生屬性值的邏輯。在這種情況下,可通過調用行向量的size()方法設定屬性:
現在有了獲得所儲存資料的行數和列數的方法,還需讓一些資料輸入到組件中,這是下一步要做的工作。
3. 增加方法
到目前為止,還無法把任何資料輸入到內部資料結構中,也無法讀取它們。下面增加四個方法完成下列任務:
? 在資料結構中插入資料。
? 從資料結構中擷取一個欄位。
? 擷取列名稱。
? 對列進行排序。
(1) 分析資料
第一個方法將獲得一個以逗號隔離的字串,進行分析,再將資料輸入資料結構中。
在項目工作區中選擇ClassView選項卡,右擊ITableStorage介面。在菜單中選擇AddMethod,按圖17 - 6填寫對話方塊。
然後用下列代碼填寫ParseCSV方法的主體部分:
CSV資料作為一個字串參數傳遞,並清除兩個STL成員變數,刪除以前調用這個方法時輸入的資料。
要特別注意第一行, 因為它包含列的名稱,每一列的名稱作為一個鍵儲存在m_columnIndexMap中,用映射的當前大小作為索引。當一行處理完後,可以利用列名稱映射的索引也就是列的數值索引這一事實。如果被分析的欄位有資料,就將其儲存在INDEX_FIELD_MAP中。
一旦所有行都處理完後, COM組件中的CSV資料的儲存空間已經最小化,並且由於使用映射可以加快訪問速度。因為只儲存了有實際值的欄位,所以儲存空間最小。由於映射在內部組織資料,可快速檢索,訪問速度很快。
(2) 資料訪問
現在資料已能高效儲存,下一步是能夠訪問它。
向ITableStorage介面添加一個稱為GetField的新方法,按圖17 - 7對對話方塊進行填寫。
給定行數和列名稱,這個方法將返回欄位值(如果它存在)。改變這個方法的主體,如下所示:
用於獲得與映射的鍵相對應的值的映射方法是find。對於COLUMN_INDEX_MAP映射,從find中返回pair<wstring, unsigned short>類型的遍曆器。如果沒有找到鍵,遍曆器具有m_columnIndexMap.end()的值。如果找到鍵,返回的遍曆器的second成員包含對應值:在這種情況下,它是列名稱的索引。這個索引作為INDEX_FIELD_MAP映射的鍵,給定行即可得到欄位值。如果找到這個值,通過[out, retval]參數返回,也就是fieldValue。
下面建立一個得到列名稱的方法。在ITableStorage中添加一個稱為GetColumnName的新方法,按圖17 - 8所示填寫對話方塊。
給定一列的索引,這個方法將返回列的名稱,用下列代碼改變這個方法的主體:
在這個方法中,必須通過映射進行線性尋找,因為程式實際是使用鍵的值而不是鍵進行檢索,遍曆m_columnIndexMap映射直到找到索引或者搜尋到映射的末端(即索引未找到)。注意遍曆器的second成員用於比較,這是因為尋找的是鍵的值而不是鍵。如果找到列索引,列名稱(實際上是鍵)通過[out, retval]參數返回,也就是columnName。
(3) 資料排序
下面添加一個對資料有實際影響的方法。使用STL sort演算法對行進行排序。
然後給I Ta b l e S t o r a g e增加一個新的方法S o r t,按圖1 7 - 9對對話方塊進行填寫:
因為CTableStorage資料結構是一種STL資料結構的組合(一種映射向量),需要提供一種定製的比較函數進行排序。例如,CTableStorage::Sort允許行根據任何列排序,這種排序可以僅使用一個比較函數,或使用一個函數對象。用於排序的列被傳遞給函數對象doCOMpare的構造器。
在TableStorage.cpp檔案某處增加如下函數對象,例如放在includes和TableStorage類的實現部分之間。
這是doCOMpare函數對象的聲明和實現部分,在排序中用於比較。確實,這個函數看起來可能複雜一點,詳細的文法解釋可以查C++手冊,不過它的目的卻非常簡單。比較各行以便按m_direction參數指定的次序排序。以列號作為構造器中的第一個參數,並儲存在一個成員變數(m_column)中。這樣,當做比較時,函數對象就知道比較的是哪一列。
注意doCOMpare是由STL binary_function模板衍生的。STL提供binary_function使得建立比較函數非常方便。
17.3.4 測試
這個組件可用於許多地方:比如VB程式中、ASP檔案中甚至於C++程式中。下面分析一下在ASP中的使用方式。
首先建立一個字串CSVString,它代表資料。用Server.CreateObject建立一個TableStorage對象,用ParseCSV方法對字串進行分析。這時資料在記憶體中。然後根據名字按升序排序。
注意, COMponentTest.ASP頁面的全部代碼可以從Worx網站上下載,本章及下一章的Visual C++項目ASPCOMponents是本書原始碼的一部分。
另外,為了得到一個欄位的值,必須指定欄位名。一個更靈活的介面應允許使用索引來得到欄位的值。運行這個頁面,瀏覽器中應該得到如圖1 7 - 1 0所示的顯示。
17.3.5 錯誤處理
錯誤處理的兩個主要方面是:
? 確保能捕捉所有的錯誤。
? 提供錯誤情況的精確描述。
在程式開發過程中盡量早地使用合適的錯誤處理工具,能明顯減少開發與測試時間,這是因為能更快地發現和糾正錯誤。可以使用C++的異常處理來捕捉錯誤,使用Error對象向用戶端反饋資訊。
1. 異常處理
錯誤處理能力受程式語言限制。一個典型的錯誤處理過程包含如下幾個方面:
? 調用一個函數。
? 檢查傳回值。
? 如果成功,程式採用一個代碼路徑,如果失敗採用另一個。
這個過程的問題是導致代碼嵌套,很難跟蹤。另外,沒有辦法迫使程式員檢測傳回值。他們可能懶得檢查傳回值,或是忘了。
我們更希望編程環境能替我們捕捉錯誤並引導代碼按預先確定的錯誤處理路線運行。在Visual Basic中可以用On Error Goto <錯誤處理函數>做到這點。當錯誤產生時,程式將跳到指定的錯誤處理函數處。也可以在Visual Basic中調用Err.Raise來標記一個錯誤。
C++和Java擁有相同的錯誤處理概念,叫作異常處理。異常處理允許把代碼的一部分放入try塊來保護它。如果錯誤產生於try塊,那麼程式的執行跳到catch塊,在那兒有錯誤處理代碼。當錯誤產生於catch程式塊時,代碼將自動指向try程式塊。使用throw關鍵字發出錯誤資訊。
下面是異常處理的簡單例子:
在第1 8章中將使用這種異常處理風格。
一旦捕獲了錯誤,下一步是將錯誤資訊返回給使用者。如果所使用的COM對象的宿主環境是ASP,最好的方法是通過Error對象報告錯誤。
2. Error對象
如果未用On Error Resume Next,當對沒有值的欄位調用GetField方法時,將遇到如下錯誤:
這個提示用處不大,在錯誤產生時需要組件能提供更多的資訊,可以通過Error對象來做到這一點。
如果建立一個新的ATL對象,並使用Error對象,可以選擇ATL Object Wizard Properties對話方塊中的Support ISupportErrorInfo複選框,指示嚮導產生支援Error對象的代碼。
通過這種修改,我們的類將支援ISupportErrorInfo COM 介面。然而,需要在源檔案TableStorage.cpp插入下面的代碼,才可以實現增加的方法。
我們的類從C COM CoClass派生,有一個Error方法。也就是說C TableStorage類能夠使用C COM Co Class定義的所有公用方法和屬性。現在C TableStorage類支援錯誤介面,因而可以使用這個方法。C TableStorage類也提供Error對象的所有參數,包括錯誤碼、描述和協助資訊。
現在不僅得到了增加的錯誤資訊,而且得到了組件的ProgID。當組件支援ISupportErrorInfo時,ProgID資訊將自動地插入。
另外,我極力推薦程式員在組件開發初期使用錯誤支援。這將有助於程式員調試組件,並能夠協助組件使用者的開發工作。當然,如果不能從錯誤資訊中判斷出是什麼出了錯,下一步就是偵錯工具了。
17.3.6 調試
在Visual C++中可以直接調試一個正常的可執行程式。可以設定斷點,然後在偵錯模式下運行程式。如果有DLL,則需要做更多的工作: C++調試器必須附加上支援調試的進程。
一個DLL不在自己的進程運行,它運行在其他進程中。因此,必須給調試器捆綁上能容納DLL的應用程式進程空間。如果在Visual Basic中測試,就需要捆綁上Visual Basic;如果是在ASP上測試,則需要捆綁上Web伺服器處理序。另外,在Visual Basic中,可以運行VB6.EXE,開啟一個使用DLL的項目,像正常情況一樣設定斷點。
還有一個直接設定組件進行調試的方法,只要在組件中需要調試的地方加上下面一行即可:
DebugBreak();
當容納DLL程式的進程運行到這一行時,將停下來彈出一個對話方塊並告訴你已經到了斷點,並詢問是否調試應用程式。按Cancel調用調試器(注意,按OK不進入調試器)。如果組件調試版本而不是發行版本,當按Cancel時,Visual C++將開始運行並停在插入DebugBreak()的地方。在這可以設定另外的斷點、觀察變數和做其他的調試工作。
這種方式的主要問題是其侵入性。為了調試代碼必須修改代碼,還得十分小心避免遺留DebugBreak()。如果組件產品中有DebugBreak(),運行時將彈出對話方塊並鎖住每一個使用者使其脫離Web伺服器,等待使用者按下OK或Cancel。這是很糟的。
如果組件在ASP中使用,修改然後重新編譯就會出現如下錯誤:
LINK:fatal error LINK1168:connot Debug/aspComponent.dll for writing
Error Executing link.exe
伺服器組件在Web伺服器的進程空間中運行,如果得到上述錯誤,意味著伺服器仍有此組件的引用。更確切地說,伺服器正引用組件中的DLL,所以需要解除對DLL的引用。但是,做到這些需要關閉並重新啟動WWW服務。然而,如果對象是在MTS中運行,這個過程就簡單多了。
COM+調試
首先,確定設定的啟用屬性(activation property)是專用的伺服器處理序而不是庫進程。這才能確保調試器綁定到所要調試的組件的進程。在Project Settings選項卡上,將Executable fordebug session項設定為dllhost.exe,將Program arguments項設定為ProcessID: <進程的ID >,如圖1 7 - 11所示。
進程的I D可以通過在Component Services Explorer中按右鍵應用程式圖示,並選擇Properties來獲得,如圖1 7 - 1 2所示。
關閉組件所在的應用程式的伺服器處理序,確保組件當前不會駐留在記憶體中。否則,可能會使用沒有綁定調試器的組件執行個體。確被建立組件的調試版本,在調試版本中設定所需斷點並從Build菜單中執行程式。
運行帶有程式參數的dllhost.exe,以指示它載入包含被調試組件的應用程式。因為COM+進程綁定了調試器,無論什麼時候訪問組件,調試器都能在斷點處中斷調用。
如果對象失敗,有關C++中的組件的其他情況都自動地放在事件記錄中。
本章介紹了C++的起源以及設定與開發ASP組件相關的環境。讀者已經學習了一部分C++,如STL和ATL,這些都是建立ASP組件的有用工具。這些工具都能滿足伺服器組件的設計需要,其目的就是減少儲存空間和訪問時間。為了介紹它們的使用方法,本章建立了一個能用於任何應用程式的萬用群組件。
組件的可利用能力的一個重要方面是其錯誤處理。C++異常處理提供了在組件中捕獲錯誤的有效方法。要想在應用程式調用之外報告錯誤,組件需要支援ISupportErrorInfo。