摘要:本文設計并實現了一個面向教學的且支持程序員級別動態網頁二次開發的Web服務器。
關鍵詞:Web服務器;面向教學;JSP/Servlet
中圖分類號:G64 文獻標識碼:A
文章編號:1672-5913 (2007) 17-0067-04
1引言
Web服務器是進行互聯網應用開發的基礎。在它剛剛出現的時侯,Web服務器提供的只是靜態HTML網頁的服務,在Web服務器收到一個來自瀏覽器的請求時,它會把服務器上與請求資源URI(統一資源標志符)相對應的靜態文件通過HTTP協議傳給Web瀏覽器。隨著技術的發展,如何提供動態網頁的服務成為關注的焦點。由此人們實現了IIS、Apache Tomcat和Weblogic等諸多技術成熟且功能強大的服務器,引入了ASP和JSP/Servlet等諸多技術和規范。以Tomcat為例,它是JSP/Servlet規范的參考實現[1]。但在教學與實驗中,學生在學習JSP/Servlet時很容易陷于龐大的API(應用程序接口)中,浮于各個方法(函數)是如何使用等表面問題上,而忽視了對技術真正實現機制的理解。為使學生掌握技術的實現機理,盡管采用了多媒體等諸多教學輔助手段,但是學生對Web服務器的幕后機制還是一知半解。為解決這樣的問題,在教學與實驗中結合一個具體的Web服務器的源碼來分析與學習就很有必要。由此可揭示相關技術的真正實現機制,使學生能透過現象看本質,不是浮于對各種動態網頁技術的API的死記硬背上。目前市面上已有不少開源的Web服務器,Tomcat就是一個例子,但是它們相對教學而言還是太過復雜。基于此,本文設計并用Java語言實現了一個接口簡單但支持二次開發的Web服務器(為之命名為MyServer),內置了用于會話跟蹤的機制,提供了簡明的用于二次開發的接口MyServlet,并充分考慮到了教學實驗的需求,把MyServer服務器源代碼的規模控制在一千行左右,并對服務器的各個功能模塊作了較好的劃分,以便于學生的閱讀與理解。
2MyServer服務器的設計
從總體上而言,MyServer服務器能提供的服務可分為兩類:一是靜態網頁的服務,包括HTML頁面、圖片、音頻和視頻文件;二是動態網頁的服務,為此MyServer服務器需要內置方便程序員二次開發的相關機制,包括會話(MySeesion)的管理、用來產生動態頁面的MyServlet組件(使用MyServer服務器的程序員可通過實現MyServlet接口來產生動態網頁)的自動加載與管理、請求消息的解析和響應消息的產生。MyServer服務器上的靜態網頁服務和動態網頁服務既有相同之處又有不同之處。兩者的共同之處是對于來自瀏覽器的每個請求,MyServer服務器都會啟動一個新的線程ServerThread,由此線程從原始的HTTP請求消息中提取出請求URI,然后在服務器上查找相應的資源來給瀏覽器作響應。它們的不同之處在于:對于靜態網頁的服務而言,MyServer服務器只要把服務器上與請求URI相對應的靜態文件通過HTTP協議傳給Web瀏覽器即可;而對于動態網頁的服務而言,服務器需要進一步解析請求消息,從中提取來自瀏覽器的請求參數和頭域信息,同時還要找到與URI相匹配的MyServlet組件,由此組件來產生動態響應,如果相關的MyServlet組件還未被加載,則MyServer服務器還應動態載入相關的MyServlet組件類,生成該類的實例化對象,并把由MyServer服務器自動生成的代表解析后的當前請求消息的MyRequest對象和代表當前響應消息的MyResponse對象傳給MyServlet對象,MyServlet對象就可以根據當前請求的方法是GET還是POST來調用相應的doGet或doPost方法來作處理。MyServer服務器的運行界面如下所示。

基于以上的分析,同時考慮教學與實驗的要求,把MyServer服務器分為以下幾大功能模塊:
(1) 主模塊:負責監聽服務器端口,如果有來自瀏覽器的HTTP請求,則生成一個新的線程來處理當前請求,同時主模塊還應負責管理所有的會話信息、維護所有的MyServlet組件和相關的配置文件。在此模塊中,學生可通過源碼的分析和學習,領悟服務器與瀏覽器之間如何建立連接,服務器如何實現多線程的響應機制,如何提供同步機制以實現對相關數據的并發訪問。
(2) 線程模塊:在此模塊中處理自瀏覽器的當前請求,在此模塊中應生成代表當前請求消息的MyRequest對象和代表當前響應的MyReponse對象,如果請求的是動態資源,還應動態加載相關的MyServlet組件。在此模塊中,學生可體會如何在線程中處理一個HTTP請求。
(3) 請求模塊:在此模塊中完成對HTTP請求消息的解析,包括對請求行、請求首部和請求體(對POST請求而言)的解析,在此模塊中可抽象出代表解析后的當前請求消息的類MyRequest。在此模塊中,學生可學習HTTP請求消息的組織格式,并學習如何從原始的HTTP請求消息中提取出所需的數據。
(4) 響應模塊:在此模塊中實現服務器對瀏覽器響應消息的處理,從中可抽象出代表當前響應消息的類MyResponse。學生可在此模塊中學習HTTP響應消息的組織格式,并學習如何去設置響應消息的狀態碼和相關頭域,如何向瀏覽器輸出各種類型的響應消息。
(5) 動態模塊:在此模塊中定義了MyServer服務器上程序員二次開發所需的MyServlet接口和管理當前會話的MySession類。學生通過對此模塊的分析,可真正明白會話的概念是如何抽象出來的。同時學習Web服務器是如何支持程序員級別的二次開發。
3MyServer服務器的實現
在用Java語言實現MyServer服務器時,考慮了這樣的兩個原則:一是實現MyServer服務器的宗旨是服務教學,通過服務器源碼的學習讓學生把握事物的本質,為此不作過多的安全方面的考慮,以避免學生陷入其他無關的主題中;二是MyServer服務器提供的用于二次開發的API力求成為或類似于現有的技術標準Servlet規范的某個子集,這樣學生便可順利地從MyServer服務器源碼的分析轉入對JSP/Servlet等工業化技術標準和規范的學習或者也可很方便地從JSP/Servlet規范的學習轉入對MyServer服務器源碼的分析上。
3.1主模塊的實現
在主模塊中,首先通過生成一個java.net.Server Socket對象[2],來監聽服務器上的相應端口是否存在來自瀏覽器的連接,如果沒有連接則當前線程阻塞,直到有連接為止。當監聽到來自瀏覽器的連接時,則把代表當前連接的socket對象作為參數傳給ServerThread線程的構造方法,進而啟動一個新線程來處理當前請求。
ServerSocket server = new ServerSocket(80);
//啟動服務器
Socket socket = server.accept();
//監聽
socket.setSoTimeout(100);
//設置超時時間
new ServerThread(this,socket).start();
//生成線程處理響應
代表當前連接的socket應設置一個超時時間(此處設為100毫秒),此超時時間的主要作用是避免從socket輸入流讀取數據時陷入無限等待中。若從Socket讀數據時的等待時間超過了規定的超時時間,則會拋出一個超時異常,此時可認為來自瀏覽器的輸入流中的數據已經讀完。
而對于各個會話對象MySession的管理,則采用哈希表的數據結構,其中存放<會話編號SessionId,MySession對象>的有序對。因為處理HTTP請求的每個ServerThread線程中可能都要訪問會話信息,所以對于此哈希表的訪問應通過同步方法,而產生SessionId的方法也應設應為同步,以避免出現同編號的會話。同時主模塊中還應維護請求URI與MyServlet組件的映射表,這部分信息存于服務器的配置文件中。在服務器啟動時,應從配置文件中讀入映射信息。配置文件采用XML的數據格式,下例表示的是請求URI為/index.html的請求應由名為HelloServelt的MyServlet組件來處理。
cn.edu.fjnu.examples.HelloServlet
3.2線程模塊的實現
在此模塊中,抽象出線程類ServerThread,它的構造方法中有一參數為Socket類型,用來存放與當前請求對應的Socket連接。在此線程的構造方法中分別生成MyRequest和MyResponse對象(分別由由請求模塊和響應模塊來實現),由MyRequest對象獲得請求URI,先查找主模塊中維護的請求URI與MyServlet組件的映射表,如果找不到與當前URI對應的MyServlet組件,則在服務器的本地文件系統中查找相應的靜態文件,如果仍然找不到,則表示不存在瀏覽器所請求的資源,可設置響應狀態碼為404。而如果存在與請求URI相對應的MyServlet組件,則要動態的加載相應的MyServlet組件類,生成該類的一個實例對象,再調用此對象的doPost或doGet方法來處理請求,產生動態響應。因為存在不同的用戶請求相同頁面的情況,所以新生成的MyServlet實例對象應存放到主模塊中的URI與MyServlet組件映射表中以便復用。MySevlet組件類的動態加載與實例化的過程如下:
Class c = Class.forName(“類名”);
//動態加載MyServlet類
Object servlet = c.newInstance();
//實例化
3.3請求模塊的實現
在請求模塊中抽象出了MyRequest類,用來表示來自瀏覽器的當前請求消息。該類的構造方法中有一代表當前連接的參數socket(為Socket類型),因為我們在主模塊中已經設了此socket對象的超時時間[3],所以通過此輸入流讀取數據時若等待的時間過長則會產生異常,服務器認為來自瀏覽器的數據已經讀完。而在Java的I/O中,一般是用返回-1來表示輸入流的數據已經讀完。為此,我們可新建一個繼承java.io.BufferedInputStream的類ServletInputStream,改寫其read()方法,在方法體中捕獲到超時異常時則返回-1。在作了這樣的處理之后,就可用以下方式來獲取來自瀏覽器的輸入流:
BufferedInputStream in =
new ServletInputStream (socket. getInput Stream());
并可進一步抽象出請求分析器RequestAnalyzer,由它專門負責解析ServletInputStream流中的HTTP請求消息。
RequestAnalyzer analyzer = new RequestAnalyzer(in);
while((input = in.read()) != -1){
//不斷讀入當前字節進行分析
}
請求分析器應從輸入流中提取請求行中的請求方法、請求URI和協議信息,提取請求首部的頭域信息(特別的要提取出響應模塊中加入的Cookie信息,其中包含了會話編號SessionId,以進行會話跟蹤),而請求參數的提取則要根據請求行中提取的請求方法是GET還是POST來作處理。如果是GET請求,則要從請求URI中附加的查詢字符串中提取,而如果是POST請求,則要從消息體中提取。提取的請求參數要用java.net.Decoder對請求參數進行解碼。
3.4響應模塊的實現
響應模塊主要負責處理輸出給給瀏覽器的響應消息,由此抽象出類MyResponse用來表示當前響應。它的構造方法中一個有表示當前連接的參數socket(為Socket類型),由socket.getOutputStream可獲得輸出流。為了方便程序員級別的二次開發,MyServer服務器應對響應消息作預處理,即HTTP響應消息的狀態行和消息首部的輸出應由MyServer服務器負責。程序員只負責設置必要的頭域和響應消息主體的輸出。同時更重要的是響應模塊中應加入用于會話跟蹤的SessionId,這部分工作也應由MyServer服務器來預處理。為此,我們就要新建一個繼承java.io.BufferedOutputStream的輸出流ServletOutputstream,它的構造方法中有一參數是由socket.getOutputStream()獲得的輸出流,改寫此類的write()方法。這樣在改寫后的write方法中就可作判斷,如果是第一次調用write方法時,則要往輸出流中添加響應狀態行和響應頭域,同時把當前請求所對應的會話編號SessionId寫入響應頭域的Set-Cookie中,以便進行會話跟蹤。
3.5動態模塊的實現
動態模塊主要是提供支持動態網頁的服務,支持程序員級別的二次開發。為此,定義了一個簡潔的MyServlet接口:
public interface MyServlet{
void doGet(MyRequest req,MyResponse res) throws IOException;
void doPost(MyRequest req,MyResponse res) throws IOException;
}
同時,引入了類MySession用來存放與某次會話相關的數據,其內部也是采用哈希表的形式來存放相關的
4基于MyServer服務器的二次開發
在自定義的MyServer服務器上,通過實驗,它能很好地支持包含音頻、視頻和圖片的靜態網頁的服務。同時為了進一步檢測其性能,在MyServer服務器通過MyServlet組件進行了上傳文件的二次開發。因為在設計與實現MyServer時力求使其API成為Servlet規范的子集,所以服務器端處理瀏覽器文件上傳的測試程序從MyServer服務器移植到Tomcat服務器上基本不用作改動,這也為在我們教學上從MyServer源代碼的分析過渡到JSP/Servlet的可行性提供了有力佐證。下表是用測試程序分別在MyServer服務器和Tomcat服務器上接收瀏覽器上傳文件所需時間的統計數據(為本機上五次實驗的平均值):
文件大小 MyServerTomcat
50MB25.3秒24.9秒
5結束語
本文設計并用Java實現了一個面向教學的Web服務器MyServer,實現了多線程的響應機制,支持會話跟蹤,提供了用于程序員級別二次開發的組件MyServlet,給出了用此組件在MyServer服務器上進行二次開發的一個例子(文件上傳)。根據教學與實驗的特殊要求,對各個功能模塊進行了較好劃分,并把它用到了互聯網應用開發的教學與實驗中。MyServer服務器的目標并不是希望與現有的主流Web服務器搶奪商用市場,而是希望由此服務器的設計與實現來改變傳統的互聯網應用開發的教學與實驗模式,希望借由Web服務器源碼的分析來讓學生理解各個抽象概念背后的實現機理,進而提高教學與實驗的效果。同時鼓勵學生組成團隊,通過合作來實現對現有的MyServer服務器的改進,達到在實驗中潛移默化地培養其團隊合作精神的目的。傳統的教學與實驗方式容易使學生迷失于JSP/Servlet相對龐大的API中,忽視了對幕后機理的思考;而在教學與實驗中結合MyServer服務器源碼的分析,就能起到舉一反三,達到授人以漁的目的,真正改變傳統的填鴨式的喂魚方式。
Design and Implemention of a teaching-oriented web server
Zou Changwei
(College of Software Engineering,Fujian Normal University,Fuzhou,350007,China)
Abstract:A teaching-oriented Web Server which supportsprogrammer-level secondary dynamic page developing is designed and implemented.
Keywords:Web Server ; teaching-oriented;JSP/Servlet
參考文獻:
[1] Hans Bergsten著. 林琪,朱濤江 譯. JSP設計[M]. 北京:中國電力出版社,2004.
[2] Bruce Eckel 著.侯捷 譯. Java編程思想[M]. 北京:機械工業出版社,2002.
[3] Elliotte Rusty Harold著. 朱濤江,林劍 譯. Java網絡編程[M]. 北京:中國電力出版社,2005.
收稿日期:2007-07-22
作者簡介:鄒昌偉(1981 - ),男,福建龍巖人,碩士,助教,研究方向為Web。