Tomcat 的過濾訣竅

發表于:2007-06-21來源:作者:點擊數: 標簽:
Servlet 2.3 規范的附加過濾為 J2EE 應用程序提供了強化的 性能 Sing Li (westmakaha@yahoo.com) 作家, Wrox 出版社 2001 年 6 月 新的 Java Servlet 2.3 規范有不少最激動人心的功能,其中之一便是過濾。乍一看,Servlet 2.3 過濾似乎與 Apache、IIS、Nets

   
  Servlet 2.3 規范的附加過濾為 J2EE 應用程序提供了強化的性能

Sing Li (westmakaha@yahoo.com)
作家, Wrox 出版社
2001 年 6 月

新的 Java Servlet 2.3 規范有不少最激動人心的功能,其中之一便是過濾。乍一看,Servlet 2.3 過濾似乎與 Apache、IIS、Netscape Web 服務器及其它服務器中已有的傳統過濾器非常相似。事實上,Servlet 2.3 過濾從結構上來說是一個完全不同的設計 -- 補充支持 Java 平臺面向對象的特性,以提供更高級別的性能。本文向您介紹了 Tomcat 4 中的過濾,并展示了如何在項目中高效地使用過濾器。請點擊文章頂部或底部的 討論,參與討論論壇,與本文作者和其他讀者分享您對本文的想法。

過濾是 Tomcat 4 的新功能。(如想了解 Tomcat 的簡要歷史,請參閱 Tomcat 的故事)。它是 Servlet 2.3 規范的一部分,并且最終將為所有支持此標準的 J2EE 容器的廠商所采用執行。開發人員將能夠用過濾器來實現以前使用不便的或難以實現的功能,這些功能包括:

* 資源訪問(Web 頁、JSP 頁、servlet)的定制身份認證

* 應用程序級的訪問資源的審核和記錄

* 應用程序范圍內對資源的加密訪問,它建立在定制的加密方案基礎上

* 對被訪問資源的及時轉換,包括從 servlet 和 JSP 的動態輸出

這個清單當然并沒有一一羅列,但它讓您初步體驗到了過濾所帶來的額外價值。在本文中,我們將詳細討論 Servlet 2.3 的過濾,來看一看過濾器是如何配合 J2EE 處理模型的。不像其它傳統的過濾方案,Servlet 2.3 過濾是建立在嵌套調用的基礎上的。我們來研究一下這一差別是怎樣在架構上與新的高性能 Tomcat 4 設計取得一致的。最后,我們將獲得一些編寫及測試兩個 Servlet 2.3 過濾器的實際經驗。這些過濾器只完成很簡單的功能,使我們得以將注意力集中于編寫過濾器以及如何將它們集成進 Web 應用程序的機制。

作為 Web 應用程序構建模塊的過濾器
在物理結構上,過濾器是 J2EE Web 應用程序中的應用程序級的 Java 代碼組件。除了 servlet 和 JSP 頁以外,遵循 Servlet 2.3 規范編碼的開發人員能將過濾器作為在 Web 應用程序中加入活動行為的機制。與在特定的 URL 上工作的 servlet 和 JSP 頁不同,過濾器接入 J2EE 容器的處理管道,并能跨越由 Web 應用程序提供的 URL 子集(或所有 URL)進行工作。圖 1 說明了過濾是在哪里配合 J2EE 請求處理的。

圖 1. 過濾器與 J2EE 請求處理

[myimg]upload/fig1.png[/myimg]

兼容 Servlet 2.3 的容器允許過濾器在請求被處理(通過 Servlet 引擎)以前以及請求得到處理以后(過濾器將可以訪問響應)訪問 Web 請求。

在這些情況下,過濾器可以:

* 在請求得到處理以前修改請求的標題
* 提供它自己的請求版本以供處理
* 在請求處理以后和被傳回給用戶以前修改響應
* 先取得由容器進行的所有請求處理,并產生自己的響應

比過濾器的可用性更為重要的是,接入 J2EE 處理管道需要創建不可移植的、容器專用的和系統范圍的擴展機制(如 Tomcat 3 攔截器)。

概念上的 Tomcat 過濾
不同于在 Apache、IIS 或 Netscape 服務器中能找到的熟悉的過濾機制,Servlet 2.3 過濾器并非建立在掛鉤式函數調用上。事實上, Tomcat 4 級別的引擎架構脫離了傳統的 Tomcat 3.x 版本。新的 Tomcat 4 引擎取代了在請求處理的不同階段調用掛鉤式方法的整體式引擎,它在內部使用了一系列的嵌套調用、包裝請求及響應。不同的過濾器和資源處理器構成了一個鏈。

在傳統架構中:

* 每次接受到請求,掛鉤式方法就被調用,不論它們是否執行(有時甚至是空的)。

* 方法的作用域及并發關系(每個方法可能在不同的線程上被調用)不允許在處理相同的請求時簡單、高效地共享不同掛鉤式方法調用間的變量和信息。

在新架構中:

* 嵌套的方法調用通過一系列過濾器實現,它僅有應用于當前請求的過濾器組成;基于掛鉤式調用的傳統執行方式需要在處理短句中調用掛鉤式例程,即使一個特定短句的處理邏輯不起任何作用。

* 局部變量在實際的過濾方法返回之前都作保留,并且可用(因為上游過濾器的調用總在堆棧上,等待后續調用的返回)。

這一新架構為今后的 Tomcat 性能調整與優化提供了一個新的、更對象友好的基礎。Servlet 2.3 過濾器是這個新的內部架構的自然擴展。該架構為 Web 應用程序設計人員提供了一個可移植的執行過濾行為的方法。

調用鏈
所有過濾器都服從調用的過濾器鏈,并通過定義明確的接口得到執行。一個執行過濾器的 Java 類必須執行這一 javax.servlet.Filter 接口。這一接口含有三個過濾器必須執行的方法:

* doFilter(ServletRequest, ServletResponse, FilterChain):這是一個完成過濾行為的方法。這同樣是上游過濾器調用的方法。引入的 FilterChain 對象提供了后續過濾器所要調用的信息。

* init(FilterConfig):這是一個容器所調用的初始化方法。它保證了在第一次 doFilter() 調用前由容器調用。您能獲取在 web.xml 文件中指定的初始化參數。

* destroy():容器在破壞過濾器實例前,doFilter()中的所有活動都被該實例終止后,調用該方法。

請注意:Filter 接口的方法名及語義在最近的幾個 beta 周期中曾有過不斷的改變。Servlet 2.3 規范仍未處于最后的草案階段。在 Beta 1 中,該接口包括 setFilterConfig() 和 getFilterConfig() 方法,而不是 init() 和 destroy()。

嵌套調用在 doFilter() 方法執行中發生。除非您建立一個過濾器明確阻止所有后續處理(通過其它過濾器及資源處理器),否則過濾器一定會在 doFilter 方法中作以下的調用:

FilterChain.doFilter(request, response);



安裝過濾器:定義與映射
容器通過 Web 應用程序中的配置描述符 web.xml 文件了解過濾器。有兩個新的標記與過濾器相關:<filter> 和 <filter-mapping>。應該指定它們為 web.xml 文件內 <web-app> 標記的子標記。

過濾器定義的元素
<filter> 標記是一個過濾器定義,它必定有一個 <filter- name> 和 <filter-class> 子元素。<filter-name> 子元素給出了一個與過濾器實例相關的、基于文本的名字。<filter-class> 指定了由容器載入的實際類。您能隨意地包含一個 <init-param> 子元素為過濾器實例提供初始化參數。例如,下面的過濾器定義指定了一個叫做 IE Filter 的過濾器:

清單 1. 過濾器定義標記

<web-app>

<filter>
<filter-name>IE Filter</filter-name>
<filter-class>com.ibm.devworks.filters.IEFilter</filter-class>
</filter>

</web-app>

容器處理 web.xml 文件時,它通常為找到的每個過濾器定義創建一個過濾器實例。這一實例用來服務所有的可用 URL 請求;因此,以線程安全的方式編寫過濾器是最為重要的。

過濾器映射及子元素
<filter-mapping> 標記代表了一個過濾器的映射,指定了過濾器會對其產生作用的 URL 的子集。它必須有一個 <filter-name> 子元素與能找到您希望映射的過濾器的過濾器定義相對應。接下來,您可以使用 <servlet-name> 或 <url-pattern> 子元素來指定映射。<servlet-name> 指定了一個過濾器應用的 servlet (在 web.xml 文件中的其它地方已定義)。您能使用 <url-pattern> 來指定一個該過濾器應用的 URL 的子集。例如, /* 的樣式用來代表該過濾器映射應用于該應用程序用到的每個 URL,而 /dept/humanresources/* 的樣式則表明該過濾器映射只應用于人力資源部專有的 URL。

容器使用這些過濾器映射來確定一個特定的過濾器是否應參與某個特定的請求。清單 1 是為應用程序的所有 URL 定義的應用于 IE Filter 的一個過濾器映射:

清單 2. 過濾器映射標記

<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>




創建一個簡單的過濾器
現在該來定義我們的第一個過濾器了。這是一個不重要的過濾器,檢查請求標題以確定是不是使用 Internet Explorer 瀏覽器來查看 URL 的。如果是 Internet Explorer 瀏覽器,過濾器就顯示“拒絕訪問”的信息。盡管操作并不重要,但這個示例演示了:

* 一個過濾器的一般剖析
* 一個在請求到達資源處理器前檢查其標題信息的過濾器
* 如何編寫一個過濾器來阻止基于運行時間檢測到的條件(驗證參數、源 IP、時間…等等)的后續處理

此過濾器的源代碼作為 IEFilter.java,com.ibm.devworks.filters 包的一部分位于源代碼發布區中(請參閱參考資料)?,F在就讓我們來仔細研究一下該過濾器的代碼。

清單 3. 使用 Filter 接口

public final class IEFilter implements Filter {
private FilterConfig filterConfig = null;



所有的過濾器都須執行 Filter 接口。我們創建了一個局部變量以容納由容器在初始化過濾器時傳遞進來的 filterConfig。這有時發生在第一次調用 doFilter() 前。

清單 4. doFilter 方法

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
String browserDet =
((HttpServletRequest) request).getHeader("User-Agent").toLowerCase();

if ( browserDet.indexOf("msie") != -1) {
PrintWriter out = response.getWriter();
out.println("<html><head></head><body>");
out.println("<h1>Sorry, page cannot be displayed!</h1>");
out.println("</body></html>");
out.flush();
return;
}



doFilter() 完成了大部分工作。我們來檢查一下叫做“用戶代理”標題的請求標題。所有的瀏覽器都提供這個標題。我們將其轉換成小寫字母,然后查找說明問題的標識字符串 "msie"。如果檢測到了 Internet Explorer,我們就從響應對象中獲取一個 PrintWriter 來寫出自己的響應。在寫出了定制的響應后,方法無需連到其它過濾器就能返回。這就是過濾器阻止后續處理的方法。

如果瀏覽器并非 Internet Explorer,我們就能進行正常的鏈式操作,讓后續過濾器和處理器能在得到請求時獲得執行的機會:

清單 5. 進行正常鏈式操作

chain.doFilter(request, response);

}



隨后,我們粗略地執行該過濾器中的 init() 和 destroy() 方法:

清單 6. init() 和 destroy() 方法

public void destroy() {
}

public void init(FilterConfig filterConfig) {
this.filterConfig = filterConfig;

}
}



測試 IEFilter
假設您安裝了 Tomcat 4 beta 3 (或更新版本)并能使用,請按下列步驟啟動 IEFilter 并運行:

1.

在 $TOMCAT_HOME/conf 目錄下的 server.xml 文件里創建一個新的應用程序上下文,如下所示:


<!-- Tomcat Examples Context -->
<Context path="/examples" docBase="examples" debug="0"
reloadable="true">
...
</Context>

<Context path="/devworks" docBase="devworks" debug="0"
reloadable="true">
<Logger className="org.apache.catalina.logger.FileLogger"
prefix="localhost_devworks_log." suffix=".txt"
timestamp="true"/>

</Context>



2.

編輯代碼區的 devworks/WEB-INF 下的 web.xml 文件,以包括下列的過濾器定義及映射:

<web-app>

<filter>
<filter-name>IE Filter</filter-name>
<filter-class>com.ibm.devworks.filters.IEFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>



3.

在 $TOMCAT_HOME/webapps 目錄下創建一個叫做 devworks 的新目錄,并將所有 devworks 目錄下的東西(包括所有子目錄)從源代碼區復制到該位置?,F在就準備好啟動 Tomcat 4 了。
4.

使用下面的 URL 來訪問一個簡單的 index.html 頁面:
http://<hostname>/devworks/index.html

如果您使用的是 Internet Explorer,就能看見如圖 2 所示的定制的“拒絕訪問”信息。

圖 2. IEFilter 在遇到 Internet Explorer 的運行效果

[myimg]upload/fig2.png[/myimg]

如果您使用的是 Netscape,那就能看見如圖 3 所示的確切的 HTML 頁面。

圖 3. IEFilter 用 Netscape 瀏覽器的瀏覽效果

[myimg]upload/fig3.png[/myimg]

編寫轉換資源的過濾器
現在該來試一下更復雜的過濾器了。該過濾器:

* 從過濾器定義的實例初始化參數中讀取一組 "search" 及 "replace" 文本

* 過濾被訪問的 URL,將出現的第一個 "search" 文本替代為 "replace" 文本

在我們深入研究這個過濾器的過程中,您將對內容轉換/替代過濾器的架構加深了解。相同的架構能用于任何加密、壓縮及轉換(如由 XSLT 轉換來的 SML)過濾器。

核心機密是在鏈式處理的過程中傳遞一個定制的響應對象的包裝版本。該定制的包裝響應對象須隱藏原響應對象(從而對其實現 包裝),并提供一個定制的流以供后續處理器寫入。如果工作(文本替換、轉換、壓縮、加密…等)能迅速完成,定制流的執行就能中止后續記錄并完成需要的工作。然后定制的流就會將經轉換的數據寫入包裝的響應對象(也就是說,簡單的字符替換加密)。如果工作無法迅速完成,定制的流就需等待,直到后續處理器完成對流的寫入(也就是說,當其關閉或刷新流時)。然后它才完成轉換工作,并將經轉換的輸出結果寫入“真正的”響應中。

在我們的過濾器(ReplaceTextFilter)中,定制的包裝響應對象叫作 ReplaceTextWrapper。定制流的執行叫做 ReplaceTextStream。您能在 com.ibm.devworks.filters 包中的 ReplaceTextFilter.java 文件里找到源代碼。(請參閱參考資料)?,F在就讓我們來研究一下源代碼吧。

清單 7. ReplaceTextStream 類

class ReplaceTextStream extends ServletOutputStream {
private OutputStream intStream;
private ByteArrayOutputStream baStream;
private boolean closed = false;

private String origText;
private String newText;

public ReplaceTextStream(OutputStream outStream,
String searchText,
String replaceText) {
intStream = outStream;
baStream = new ByteArrayOutputStream();
origText = searchText;
newText = replaceText;
}



這是定制的輸出流代碼。intStream 變量包含了對來自響應對象的實際流的引用。baStream 是我們輸出流的緩沖版本,后續處理器就寫入這里。closed 標記標明了 close() 是否在此實例流中被調用。構造器將來自響應對象的流引用存儲起來并創建了緩沖流。它還將文本字符串存儲起來供以后的替代操作使用。

清單 8. write() 方法

public void write(int i) throws java.io.IOException {
baStream.write(i);
}



我們須提供自己的源于 ServletOutputStream 的 write() 方法。在此,我們當然是寫入緩沖流。所有來自后續處理器的更高級輸出方法都將以最低級別使用該方法,以保證所有的寫入都指向緩沖流。

清單 9. close() 及 flush() 方法

public void close() throws java.io.IOException {
if (!closed) {

processStream();
intStream.close();
closed = true;
}
}

public void flush() throws java.io.IOException {
if (baStream.size() != 0) {
if (! closed) {
processStream(); // need to synchronize the flush!
baStream = new ByteArrayOutputStream();
}
}
}



close() 及 flush() 方法是我們完成轉換的語句。根據后續處理器不同,其中的一個或兩個程序都有可能被調用。我們使用布爾型的 closed 標識來避免異常情況。請注意,我們將實際的替代工作委托給了 processStream() 方法。

清單 10. processStream() 方法

public void processStream() throws java.io.IOException {
intStream.write(replaceContent(baStream.toByteArray()));
intStream.flush();
}



processStream() 方法將經轉換的輸出結果從 baStream 寫入其已經配有的 intStream 中去。轉換工作獨立于 replaceContent() 方法。

清單 11. replaceContent() 方法

public byte [] replaceContent(byte [] inBytes) {
String retVal ="";
String firstPart="";

String tpString = new String(inBytes);
String srchString = (new String(inBytes)).toLowerCase();

int endBody = srchString.indexOf(origText);

if (endBody != -1) {
firstPart = tpString.substring(0, endBody);
retVal = firstPart + newText +
tpString.substring(endBody + origText.length());

} else {
retVal=tpString;
}

return retVal.getBytes();
}

}



replaceContent() 是發生搜索與替換的語句。它將一個字節數組作為輸入并返回一個字節數組,創建一個原始的概念接口。事實上,我們能通過替換該方法中的邏輯部分來完成任何形式的轉換。這里,我們進行非常簡單的文本替換。

清單 12. ReplaceTextWrapper 類

class ReplaceTextWrapper extends HttpServletResponseWrapper {
private PrintWriter tpWriter;
private ReplaceTextStream tpStream;

public ReplaceTextWrapper(ServletResponse inResp, String searchText,
String replaceText)
throws java.io.IOException {
super((HttpServletResponse) inResp);
tpStream = new ReplaceTextStream(inResp.getOutputStream(),
searchText,
replaceText);
tpWriter = new PrintWriter(tpStream);
}

public ServletOutputStream getOutputStream() throws java.io.IOException {

return tpStream;
}
public PrintWriter getWriter() throws java.io.IOException {

return tpWriter;
}
}



我們定制的包裝響應能方便地從幫助類 HttpServletResponseWrapper 中導出。這一類粗略地執行許多方法,允許我們簡單地覆蓋 getOutputStream() 方法以及 getWriter() 方法,提供了定制輸出流的實例。

清單 13. ReplaceTextWrapper() 方法

public final class ReplaceTextFilter implements Filter {
private FilterConfig filterConfig = null;
private String searchText = ".";
private String replaceText = ".";
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {


ReplaceTextWrapper myWrappedResp = new ReplaceTextWrapper( response,
searchText, replaceText);
chain.doFilter(request, myWrappedResp);
myWrappedResp.getOutputStream().close();

}

public void destroy() {
}



最后,還有過濾器本身。它所做的不過是使用 FilterChain 為遞交響應后續創建一個定制的包裝響應實例,如下所示:

清單 14. 創建一個定制的包裝響應實例

public void init(FilterConfig filterConfig) {

String tpString;
if (( tpString = filterConfig.getInitParameter("search") ) != null)
searchText = tpString;
if (( tpString = filterConfig.getInitParameter("replace") ) != null)
replaceText = tpString;

this.filterConfig = filterConfig;
}

}



在 init 方法中,我們取回了過濾器定義中指定的初始參數。filterConfig 對象中的 getInitParameter() 方法便于用來實現這個目的。

測試 ReplaceTextFilter
假如您使用先前提及的步驟測試了 IEFilter,并將所有文件復制到了 $TOMCAT/webapps/devworks 下,您就能用以下的步驟來測試 ReplaceTextFilter:

1.

編輯 $TOMCAT/wepapps/devworks/WEB-INF 目錄下的 web.xml 文件,以包含下列過濾器的定義及映射:

<web-app>

<filter>
<filter-name>Replace Text Filter</filter-name>
<filter-class>com.ibm.devworks.filters.ReplaceTextFilter</filter-class>
<init-param>
<param-name>search</param-name>
<param-value>cannot</param-value>
</init-param>
<init-param>
<param-name>replace</param-name>
<param-value>must not</param-value>
</init-param>

</filter>


<filter-mapping>
<filter-name>Replace Text Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>



2.

重新啟動 Tomcat。
3.

現在,請用下面的 URL 來訪問 index.html 頁面:
http://<host name>:8080/devworks/index.html

請注意, ReplaceTextFilter 是如何迅速地將 cannot 變為 must not 的。想確信過濾使用了所有資源,您可以嘗試編寫輸出結果含有字符串 cannot 的 JSP 頁或 servlet。

過濾器鏈排列順序的重要性
過濾器鏈式排列的順序取決于 web.xml 描述信息內 <filter-mapping> 語句的順序。在大多數情況下,過濾器鏈式排列的順序是非常重要的。也就是說,在應用 A 過濾器前使用 B 過濾器與在使用 B 過濾器前使用 A 過濾器所得到的結果是完全不同的。如果一個應用程序中使用了一個以上的過濾器,那么在寫入 <filter-mapping> 語句的時候要小心。

我們能輕易地通過排列 web.xml 文件中 <filter-mapping> 的順序看到這一效果:

清單 15. 過濾的順序 -- IE Filter 為先

<web-app>

<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>Replace Text Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>



現在,用 Internet Explorer 載入 index.html 頁。您能看到由于 IE Filter 處于過濾器鏈中的第一位,所以 Replace Text Filter 沒有機會執行。因此,輸出的信息是 "Sorry, page cannot be displayed!"

現在,將 <filter-mapping> 標記的順序顛倒過來,變為:

清單 16. 過濾的順序 -- Replace Text Filter 為先

<web-app>

<filter-mapping>
<filter-name>Replace Text Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>IE Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>



再次用 Internet Explorer 載入 index.html 頁面。這次,Replace Text Filter 先執行,將包裝的響應對象提供給 IE Filter。在 IE Filter 寫入了其定制的響應后,專用的響應對象在輸出結果到達最終用戶處以前完成轉換。故而,我們看到了這條信息:Sorry, page must not be displayed!

在應用程序中使用過濾器
寫這篇文章的時候, Tomcat 4 正處于 beta 周期的后期,正式發行的日子已為期不遠。主要的 J2EE 容器廠商都準備好了將 Servlet 2.3 規范整合到其產品中去。對于 Servlet 2.3 過濾器如何工作有一個基本的了解有助于您在設計及編寫基于 J2EE 的應用程序時往自己的工具庫中再加入一件多功能的工具。

參考資料

* 下載本文中用到的源代碼。

* Tomcat 主頁提供了關于 Tomcat 所有可獲得的發行版本信息。請定期查閱該頁上的文件鏈接以了解有關過濾的文檔更新信息。

* 請閱讀正式規范, Java Servlet 2.3,以了解關于過濾的詳細信息。

* 請加入官方的 Servlet 興趣郵件列表來討論 Servlet 2.3 過濾的復雜問題。

* 更深入地了解 WebSphere 3.5 是如何處理 servlet 過濾的。

* 該 IBM 紅皮書提供了關于 利用 servlet、JSP 頁和 EJB 組件的 Web 應用程序設計。

* "Developing JSP Tag Libraries with Apache Tomcat 4.0 and VisualAge for Java" 向您展示了如何將面向 Java 的 VisualAge 附加到已安裝的 Apache Tomcat 4.0 服務器上,以及如何在標記代碼中設置斷點。

* 在 專業 JSP 第 2 版(Wrox 出版社,2000 年 5 月)中,有許多過濾器編寫示例,包括一個壓縮過濾器和一個 XSLT 轉換過濾器。

* 對 servlet 編程感興趣嗎?來試試該免費的 dW 專用教程,它描述了 利用 servlet 和 JSP 技術建立因特網應用程序的技巧(developerWorks,2000 年 12 月)。

* 了解一下 JSP 技術為滿足開發人員的需要而在不斷改變(developerWorks,2001 年 6 月)。

* 在 developerWorks Java 技術專區中尋找更多的 Java 參考資料。



關于作者
Sing Li 是 Professional Jini 以及許多其它由 Wrox 出版社出版的書籍的作者。他是技術雜志的定期投稿者,也是 P2P 革命的積極傳播者。Sing 是個顧問,還是個自由作家,能通過 westmakaha@yahoo.com 聯系他。

原文轉自:http://www.anti-gravitydesign.com

国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97