編寫高性能Web應用程序的10個入門技巧
? 數據層性能
? 技巧 1 — 返回多個結果集
? 技巧 2 — 分頁的數據訪問
? 技巧 3 — 連接池
? 技巧 4 — ASP.NET 緩存 API
? 技巧 5 — 每請求緩存
? 技巧 6 — 后臺處理
? 技巧 7 — 頁輸出緩存和代理服務器
? 技巧 8 — 運行 IIS 6.0(只要用于內核緩存)
? 技巧 9 — 使用 Gzip 壓縮
? 技巧 10 — 服務器控件視圖狀態
?
使用 ASP.NET 編寫 Web 應用程序的簡單程度令人不敢相信。正因為如此簡單,所以很多
開發人員就不會花時間來設計其應用程序的結構,以獲得更好的性能了。在本文中,我將
講述 10 個用于編寫高性能 Web 應用程序的技巧。但是我并不會將這些建議僅局限于
ASP.NET 應用程序,因為這些應用程序只是 Web 應用程序的一部分。本文不作為對 Web
應用程序進行性能調整的權威性指南 — 一整本書恐怕都無法輕松講清楚這個問題。請將
本文視作一個很好的起點。
成為工作狂之前,我原來喜歡攀巖。在進行任何大型攀巖活動之前,我都會首先仔細查看
指南中的路線,閱讀以前游客提出的建議。但是,無論指南怎么好,您都需要真正的攀巖
體驗,然后才能嘗試一個特別具有挑戰性的攀登。與之相似,當您面臨修復性能問題或者
運行一個高吞吐量站點的問題時,您只能學習如何編寫高性能 Web 應用程序。
我的個人體驗來自在 Microsoft 的 ASP.NET 部門作為基礎架構程序經理的經驗,在此期
間我運行和管理 www.ASP.NET,幫助設計社區服務器的結構,社區服務器是幾個著名
ASP.NET 應用程序(組合到一個平臺的 ASP.NET Forums、.Text 和 nGallery)。我確信
有些曾經幫助過我的技巧對您肯定也會有所幫助。
您應該考慮將應用程序分為幾個邏輯層。您可能聽說過 3 層(或者 n 層)物理體系結構
一詞。這些通常都是規定好的體系結構方式,將功能在進程和/或硬件之間進行了物理分離
。當系統需要擴大時,可以很輕松地添加更多的硬件。但是會出現一個與進程和機器跳躍
相關的性能下降,因此應該避免。所以,如果可能的話,請盡量在同一個應用程序中一起
運行 ASP.NET 頁及其相關組件。
因為代碼分離以及層之間的邊界,所以使用 Web 服務或遠程處理將會使得性能下降 20%
甚至更多。
數據層有點與眾不同,因為通常情況下,最好具有專用于數據庫的硬件。然而進程跳躍到
數據庫的成本依然很高,因此數據層的性能是您在優化代碼時首先要考慮的問題。
在深入應用程序的性能修復問題之前,請首先確保對應用程序進行剖析,以便找出具體的
問題所在。主要性能計數器(如表示執行垃圾回收所需時間百分比的計數器)對于找出應
用程序在哪些位置花費了其主要時間也非常有用。然而花費時間的位置通常非常不直觀。
本文講述了兩種類型的性能改善:大型優化(如使用 ASP.NET 緩存),和進行自身重復的
小型優化。這些小型優化有時特別有意思。您對代碼進行一點小小的更改,就會獲得很多
很多時間。使用大型優化,您可能會看到整體性能的較大飛躍。而使用小型優化時,對于
某個特定請求可能只會節省幾毫秒的時間,但是每天所有請求加起來,則可能會產生巨大
的改善。
數據層性能
談到應用程序的性能調整,有一個試紙性的測試可用來對工作進行優先級劃分:代碼是否
訪問數據庫?如果是,頻率是怎樣的?請注意,這一相同測試也可應用于使用 Web 服務或
遠程處理的代碼,但是本文對這些內容未做講述。
如果某個特定的代碼路徑中必需進行數據庫請求,并且您認為要首先優化其他領域(如字
符串操作),則請停止,然后執行這個試紙性測試。如果您的性能問題不是非常嚴重的話
,最好花一些時間來優化一下與數據庫、返回的數據量、進出數據庫的往返頻率相關的花
費時間。
了解這些常規信息之后,我們來看一下可能會有助于提高應用程序性能的十個技巧。首先
,我要講述可能會引起最大改觀的更改。
技巧 1 — 返回多個結果集
仔細查看您的數據庫代碼,看是否存在多次進入數據庫的請求路徑。每個這樣的往返都會
降低應用程序可以提供的每秒請求數量。通過在一個數據庫請求中返回多個結果集,可以
節省與數據庫進行通信所需的總時間長度。同時因為減少了數據庫服務器管理請求的工作
,還會使得系統伸縮性更強。
雖然可以使用動態 SQL 返回多個結果集,但是我首選使用存儲過程。關于業務邏輯是否應
該駐留于存儲過程的問題還存在一些爭議,但是我認為,如果存儲過程中的邏輯可以約束
返回數據的話(縮小數據集的大小、縮短網絡上所花費時間,不必篩選邏輯層的數據),
則應贊成這樣做。
使用 SqlCommand 實例及其 ExecuteReader 方法填充強類型的業務類時,可以通過調用
NextResult 將結果集指針向前移動。圖 1 顯示了使用類型類填充幾個 ArrayList 的示例
會話。只從數據庫返回您需要的數據將進一步減少服務器上的內存分配。
Figure 1 Extracting Multiple Resultsets from a DataReader
// read the first resultset
reader = command.ExecuteReader();
// read the data from that resultset
while (reader.Read()) {
??? suppliers.Add(PopulateSupplierFromIDataReader( reader ));
}
// read the next resultset
reader.NextResult();
// read the data from that second resultset
while (reader.Read()) {
??? products.Add(PopulateProductFromIDataReader( reader ));
}
技巧 2 — 分頁的數據訪問
ASP.NET DataGrid 具有一個很好的功能:數據分頁支持。在 DataGrid 中啟用分頁時,一
次會顯示固定數量的記錄。另外,在 DataGrid 的底部還會顯示分頁 UI,以便在記錄之間
進行導航。該分頁 UI 使您能夠在所顯示的數據之間向前和向后導航,并且一次顯示固定
數量的記錄。
還有一個小小的波折。使用 DataGrid 的分頁需要所有數據均與網格進行綁定。例如,您
的數據層需要返回所有數據,那么 DataGrid 就會基于當前頁篩選顯示的所有記錄。如果
通過 DataGrid 進行分頁時返回了 100,000 個記錄,那么針對每個請求會放棄 99,975 個
記錄(假設每頁大小為 25 個記錄)。當記錄的數量不斷增加時,應用程序的性能就會受
到影響,因為針對每個請求必須發送越來越多的數據。
要編寫性能更好的分頁代碼,一個極佳的方式是使用存儲過程。圖 2 顯示了針對
Northwind 數據庫中的 Orders 表進行分頁的一個示例存儲過程。簡而言之,您此時要做
的只是傳遞頁索引和頁大小。然后就會計算合適的結果集,并將其返回。
Figure 2 Paging Through the Orders Table
CREATE PROCEDURE northwind_OrdersPaged
(
??? @PageIndex int,
??? @PageSize int
)
AS
BEGIN
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @RowsToReturn int
-- First set the rowcount
SET @RowsToReturn = @PageSize * (@PageIndex + 1)
SET ROWCOUNT @RowsToReturn
-- Set the page bounds
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1
-- Create a temp table to store the select results
CREATE TABLE #PageIndex
(
??? IndexId int IDENTITY (1, 1) NOT NULL,
??? OrderID int
)
-- Insert into the temp table
INSERT INTO #PageIndex (OrderID)
SELECT
??? OrderID
FROM
??? Orders
ORDER BY
??? OrderID DESC
-- Return total count
SELECT COUNT(OrderID) FROM Orders
-- Return paged results
SELECT
??? O.*
FROM
??? Orders O,
??? #PageIndex PageIndex
WHERE
??? O.OrderID = PageIndex.OrderID AND
??? PageIndex.IndexID > @PageLowerBound AND
??? PageIndex.IndexID < @PageUpperBound
ORDER BY
??? PageIndex.IndexID
END
在社區服務器中,我們編寫了一個分頁服務器控件,以完成所有的數據分頁。您將會看到
,我使用的就是技巧 1 中討論的理念,從一個存儲過程返回兩個結果集:記錄的總數和請
求的數據。
返回記錄的總數可能會根據所執行查詢的不同而有所變化。例如,WHERE 子句可用來約束
返回的數據。為了計算在分頁 UI 中顯示的總頁數,必須了解要返回記錄的總數。例如,
如果總共有 1,000,000 條記錄,并且要使用一個 WHERE 子句將其篩選為 1000 條記錄,
那么分頁邏輯就需要了解記錄的總數才能正確呈現分頁 UI。
技巧 3 — 連接池
在 Web 應用程序和 SQL Server? 之間設置 TCP 連接可能是一個非常消耗資源的操作。Mi
crosoft 的開發人員到目前為止能夠使用連接池已經有一段時間了,這使得他們能夠重用
數據庫連接。他們不是針對每個請求都設置一個新的 TCP 連接,而是只在連接池中沒有任
何連接時才設置新連接。當連接關閉時,它會返回連接池,在其中它會保持與數據庫的連
接,而不是完全破壞該 TCP 連接。
當然,您需要小心是否會出現泄漏連接。當您完成使用連接時,請一定要關閉這些連接。
再重復一遍:無論任何人對 Microsoft?.NET Framework 中的垃圾回收有什么評論,請一
定要在完成使用連接時針對該連接顯式調用 Close 或 Dispose。不要相信公共語言運行庫
(CLR) 會在預先確定的時間為您清除和關閉連接。盡管 CLR 最終會破壞該類,并強制連
接關閉,但是當針對對象的垃圾回收真正發生時,并不能保證。
要以最優化的方式使用連接池,需要遵守一些規則。首先打開連接,執行操作,然后關閉
該連接。如果您必須如此的話,可以針對每個請求多次打開和關閉連接(最好應用技巧 1
),但是不要一直將連接保持打開狀態并使用各種不同的方法對其進行進出傳遞。第二,
使用相同的連接字符串(如果使用集成身份驗證的話,還要使用相同的線程標識)。如果
不使用相同的連接字符串,例如根據登錄的用戶自定義連接字符串,那么您將無法得到連
接池提供的同一個優化值。如果您使用集成身份驗證,同時還要模擬大量用戶,連接池的
效率也會大大下降。嘗試跟蹤與連接池相關的任何性能問題時,.NET CLR 數據性能計數器
可能非常有用。
每當應用程序連接資源時,如在另一個進程中運行的數據庫,您都應該重點考慮連接該資
源所花時間、發送或檢索數據所花時間,以及往返的數量,從而進行優化。優化應用程序
中任何種類的進程跳躍都是獲得更佳性能的首要一點。
應用層包含了連接數據層、將數據轉換為有意義類實例和業務流程的邏輯。例如社區服務
器,您要在其中填充Forums 或 Threads集合,應用業務規則(如權限);最重要的是要在
其中執行緩存邏輯。
技巧 4 — ASP.NET 緩存 API
編寫應用程序代碼行之前,一個首要完成的操作是設計應用層的結構,以便最大化利用
ASP.NET 緩存功能。
如果您的組件要在 ASP.NET 應用程序中運行,則只需在該應用程序項目中包括一個
System.Web.dll 引用。當您需要訪問該緩存時,請使用 HttpRuntime.Cache 屬性(通過
Page.Cache 和 HttpContext.Cache 也可訪問這個對象)。
對于緩存數據,有幾個規則。首先,如果數據可能會多次使用時,則這是使用緩存的一個
很好的備選情況。第二,如果數據是通用的,而不特定于某個具體的請求或用戶時,則也
是使用緩存的一個很好的備選情況。如果數據是特定于用戶或請求的,但是壽命較長的話
,仍然可以對其進行緩存,但是這種情況可能并不經常使用。第三,一個經常被忽略的規
則是,有時可能您緩存得太多。通常在一個 x86 計算機上,為了減少內存不足錯誤出現的
機會,您會想使用不高于 800MB 的專用字節運行進程。因此緩存應該有個限度。換句話說
,您可能能夠重用某個計算結果,但是如果該計算采用 10 個參數的話,您可能要嘗試緩
存 10 個排列,這樣有可能給您帶來麻煩。一個要求 ASP.NET 的最常見支持是由于過度緩
存引起的內存不足錯誤,尤其是對于大型數據集。
緩存有幾個極佳的功能,您需要對它們有所了解。首先,緩存會實現最近最少使用的算法
,使得 ASP.NET 能夠在內存運行效率較低的情況下強制緩存清除 - 從緩存自動刪除未使
用過的項目。第二,緩存支持可以強制失效的過期依賴項。這些依賴項包括時間、密鑰和
文件。時間經常會用到,但是對于 ASP.NET 2.0,引入了一個功能更強的新失效類型:數
據庫緩存失效。它指的是當數據庫中的數據發生變化時自動刪除緩存中的項。有關數據庫
緩存失效的詳細信息,請參閱 MSDN?Magazine 2004 年 7 月的 Dino Esposito Cutting
Edge 專欄。要了解緩存的體系結構,請參閱圖 3。
技巧 5 — 每請求緩存
在本文前面部分,我提到了經常遍歷代碼路徑的一些小改善可能會導致較大的整體性能收
益。對于這些小改善,其中有一個絕對是我的最愛,我將其稱之為"每請求緩存"。
緩存 API 的設計目的是為了將數據緩存較長的一段時間,或者緩存至滿足某些條件時,但
每請求緩存則意味著只將數據緩存為該請求的持續時間。對于每個請求,要經常訪問某個
特定的代碼路徑,但是數據卻只需提取、應用、修改或更新一次。這聽起來有些理論化,
那么我們來舉一個具體的示例。
在社區服務器的論壇應用程序中,頁面上使用的每個服務器控件都需要個性化的數據來確
定使用什么外觀、使用什么樣式表,以及其他個性化數據。這些數據中有些可以長期緩存
,但是有些數據卻只針對每個請求提取一次,然后在執行該請求期間對其重用多次,如要
用于控件的外觀。
為了達到每請求緩存,請使用 ASP.NET HttpContext。對于每個請求,都會創建一個
HttpContext 實例,在該請求期間從 HttpContext.Current 屬性的任何位置都可訪問該實
例。該 HttpContext 類具有一個特殊的 Items 集合屬性;添加到此 Items 集合的對象和
數據只在該請求持續期間內進行緩存。正如您可以使用緩存來存儲經常訪問的數據一樣,
您也可以使用 HttpContext.Items 來存儲只基于每個請求使用的數據。它背后的邏輯非常
簡單:數據在它不存在的時候添加到 HttpContext.Items 集合,在后來的查找中,只是返
回 HttpContext.Items 中的數據。
技巧 6 — 后臺處理
通往代碼的路徑應該盡可能快速,是嗎?可能有時您會覺得針對每個請求執行的或者每
n 個請求執行一次的任務所需資源非常多。發送電子郵件或者分析和驗證傳入數據就是這
樣的一些例子。
剖析 ASP.NET Forums 1.0 并重新構建組成社區服務器的內容時,我們發現添加新張貼的
代碼路徑非常慢。每次添加新張貼時,應用程序首先需要確保沒有重復的張貼,然后必須
使用"壞詞"篩選器分析該張貼,分析張貼的字符圖釋,對張貼添加標記并進行索引,請求
時將張貼添加到合適的隊列,驗證附件,最終張貼之后,立即向所有訂閱者發出電子郵件
通知。很清楚,這涉及很多操作。
經研究發現,大多數時間都花在了索引邏輯和發送電子郵件上。對張貼進行索引是一個非
常耗時的操作,人們發現內置的 System.Web.Mail 功能要連接 SMYP 服務器,然后連續發
送電子郵件。當某個特定張貼或主題領域的訂閱者數量增加時,執行 AddPost 功能所需的
時間也越來越長。
并不需要針對每個請求都進行電子郵件索引。理想情況下,我們想要將此操作進行批處理
,一次索引 25 個張貼或者每五分鐘發送一次所有電子郵件。我們決定使用以前用于對數
據緩存失效進行原型設計的代碼,這個失效是用于最終進入 Visual Studio? 2005 的內容
的。
System.Threading 命名空間中的 Timer 類非常有用,但是在 .NET Framework 中不是很
有名,至少對于 Web 開發人員來說是這樣。創建之后,這個 Timer 類將以一個可配置的
間隔針對 ThreadPool 中的某個線程調用指定的回調。這就表示,您可以對代碼進行設置
,使其能夠在沒有對 ASP.NET 應用程序進行傳入請求的情況下得以執行,這是后臺處理的
理想情況。您還可以在此后臺進程中執行如索引或發送電子郵件之類的操作。
但是,這一技術有幾個問題。如果應用程序域卸載,該計時器實例將停止觸發其事件。另
外,因為 CLR 對于每個進程的線程數量具有一個硬性標準,所以可能會出現這樣的情形:
服務器負載很重,其中計時器可能沒有可在其基礎上得以完成的線程,在某種程度上可能
會造成延遲。ASP.NET 通過在進程中保留一定數量的可用線程,并且僅使用總線程的一部
分用于請求處理,試圖將上述情況發生的機會降到最低。但是,如果您具有很多異步操作
時,這可能就是一個問題了。
這里沒有足夠的空間來放置該代碼,但是您可以下載一個可以看懂的示例,網址是
www.rob-howard.net。請了解一下 Blackbelt TechEd 2004 演示中的幻燈片和演示。
技巧 7 — 頁輸出緩存和代理服務器
ASP.NET 是您的表示層(或者說應該是您的表示層);它由頁、用戶控件、服務器控件(H
ttpHandlers 和 HttpModules)以及它們生成的內容組成。如果您具有一個 ASP.NET 頁,
它會生成輸出(HTML、XML、圖像或任何其他數據),并且您針對每個請求運行此代碼時,
它都會生成相同的輸出,那么您就擁有一個可用于頁輸出緩存的絕佳備選內容。
將此行內容添加頁的最上端
<%@ Page OutputCache VaryByParams="none" Duration="60" %>
就可以高效地為此頁生成一次輸出,然后對它進行多次重用,時間最長為 60 秒,此時該
頁將重新執行,輸出也將再一次添加到 ASP.NET 緩存。通過使用一些低級程序化 API 也
可以完成此行為。對于輸出緩存有幾個可配置的設置,如剛剛講到的 VaryByParams 屬性
。VaryByParams 剛好被請求到,但還允許您指定 HTTP GET 或 HTTP POST 參數來更改緩
存項。例如,只需設置 VaryByParam="Report" 即可對 default.aspx?Report=1 或
default.aspx?Report=2 進行輸出緩存。通過指定一個以分號分隔的列表,還可以指定其
他參數。
很多人都不知道何時使用輸出緩存,ASP.NET 頁還會生成一些位于緩存服務器下游的
HTTP 標頭,如 Microsoft Internet Security and Acceleration Server 或 Akamai 使
用的標頭。設置了 HTTP 緩存標頭之后,可以在這些網絡資源上對文檔進行緩存,客戶端
請求也可在不必返回原始服務器的情況下得以滿足。
因此,使用頁輸出緩存不會使得您的應用程序效率更高,但是它可能會減少服務器上的負
載,因為下游緩存技術會緩存文檔。當然,這可能只是匿名內容;一旦它成為下游之后,
您就再也不會看到這些請求,并且再也無法執行身份驗證以阻止對它的訪問了。
技巧 8 — 運行 IIS 6.0(只要用于內核緩存)
如果您未運行 IIS 6.0 (Windows Server? 2003),那么您就錯過了 Microsoft Web 服務
器中的一些很好的性能增強。在技巧 7 中,我討論了輸出緩存。在 IIS 5.0 中,請求是
通過 IIS 然后進入 ASP.NET 的。涉及到緩存時,ASP.NET 中的 HttpModule 會接收該請
求,并返回緩存中的內容。
如果您正在使用 IIS 6.0,就會發現一個很好的小功能,稱為內核緩存,它不需要對
ASP.NET 進行任何代碼更改。當請求由 ASP.NET 進行輸出緩存時,IIS 內核緩存會接收緩
存數據的一個副本。當請求來自網絡驅動程序時,內核級別的驅動程序(無上下文切換到
用戶模式)就會接收該請求,如果經過了緩存,則會將緩存的數據刷新到響應,然后完成
執行。這就表示,當您將內核模式緩存與 IIS 和 ASP.NET 輸出緩存一起使用時,就會看
到令人不敢相信的性能結果。在 ASP.NET 的 Visual Studio 2005 開發過程中,我一度是
負責 ASP.NET 性能的程序經理。開發人員完成具體工作,但是我要看到每天進行的所有報
告。內核模式緩存結果總是最有意思的。最常見的特征是網絡充滿了請求/響應,而 IIS
運行時的 CPU 使用率只有大約 5%。這太令人震驚了!當然使用 IIS 6.0 還有一些其他原
因,但是內核模式緩存是其中最明顯的一個。
技巧 9 — 使用 Gzip 壓縮
雖然使用 gzip 并不一定是服務器性能技巧(因為您可能會看到 CPU 使用率的提高),但
是使用 gzip 壓縮可以減少服務器發送的字節數量。這就使人們覺得頁速度加快了,并且
還減少了帶寬的用量。根據所發送數據、可以壓縮的程度以及客戶端瀏覽器是否支持(IIS
只會向支持 gzip 壓縮的客戶端發送經過 gzip 壓縮的內容,如 Internet Explorer
6.0 和 Firefox),您的服務器每秒可以服務于更多的請求。實際上,幾乎每當您減少所
返回數據的數量時,都會增加每秒請求數。
Gzip 壓縮已經內置到 IIS 6.0 中,并且其性能比 IIS 5.0 中使用的 gzip 壓縮要好的多
,這是好消息。但不幸的是,當嘗試在 IIS 6.0 中打開 gzip 壓縮時,您可能無法在
IIS 的屬性對話中找到該設置。IIS 小組在該服務器中置入了卓越的 gzip 功能,但是忘
了包括一個用于啟用該功能的管理 UI。要啟用 gzip 壓縮,您必須深入到 IIS 6.0 的
XML 配置設置內部(這樣不會引起心臟虛弱)。順便提一句,這歸功于 OrcsWeb 的
Scott Forsyth,他幫助我提出了在 OrcsWeb 上宿主的 www.asp.net 服務器的這個問題。
本文就不講述步驟了,請閱讀 Brad Wilson 的文章,網址是 IIS6 Compression。還有一
篇有關為 ASPX 啟用壓縮的知識庫文章,網址是 Enable ASPX Compression in IIS。但是
您應該注意,由于一些實施細節,IIS 6.0 中不能同時存在動態壓縮和內核緩存。
技巧 10 — 服務器控件視圖狀態
視圖狀態是一個有趣的名稱,用于表示在所生成頁的隱藏輸出字段中存儲一些狀態數據的
ASP.NET。當該頁張貼回服務器時,服務器可以分析、驗證、并將此視圖狀態數據應用回該
頁的控件樹。視圖狀態是一個非常強大的功能,因為它允許狀態與客戶端一起保持,并且
它不需要 cookie 或服務器內存即可保存此狀態。很多 ASP.NET 服務器控件都使用視圖狀
態來保持在與頁元素進行交互期間創建的設置,例如保存對數據進行分頁時顯示的當前頁
。
然而使用視圖狀態也有一些缺點。首先,服務或請求頁時,它都會增加頁的總負載。對張
貼回服務器的視圖狀態數據進行序列化或取消序列化時,也會發生額外的開銷。最后,視
圖狀態會增加服務器上的內存分配。
幾個服務器控件有著過度使用視圖狀態的趨勢,即使在并不需要的情況下也要使用它,其
中最著名的是 DataGrid。ViewState 屬性的默認行為是啟用,但是如果您不需要,則可以
在控件或頁級別關閉。在控件內,只需將 EnableViewState 屬性設置為 false,或者在頁
中使用下列設置即可對其進行全局設置:
<%@ Page EnableViewState="false" %>
如果您不回發頁,或者總是針對每個請求重新生成頁上的控件,則應該在頁級別禁用視圖
狀態。
我為您講述了一些我認為在編寫高性能 ASP.NET 應用程序時有所幫助的技巧。正如我在本
文前面部分提到的那樣,這是一個初步指南,并不是 ASP.NET 性能的最后結果。(有關改
善 ASP.NET 應用程序性能的信息,請參閱 Improving ASP.NET Performance。)只有通過
自己的親身體驗才能找出解決具體性能問題的最好方法。但是,在您的旅程中,這些技巧
應該會為您提供一些好的指南。在軟件開發中,幾乎沒有絕對的東西;每個應用程序都是
唯一的。
from: http://www.chinahtml.com/programming/8/2006/11622676777784_2.shtml