距離開篇已經過了很久,期間完善了一下之前的版本,目前已經能夠完好運行,基本上該有的功能都有了,此外將原來的測試程序改為示例項目,新項目只需按照示例項目結構實現controller和view即可,詳情見: easy-httpserver、 demo-httpsrever。
這次我們將首先來實現一個簡單版本,該版本只包括一些基本的功能,后續版本也將在此基礎上一步步改進。
一、准備工作
俗話說的好,工欲善其事,必先利其器。我們在開始開發之前應做好如下准備(真的很簡單):
- java開發環境,IDE依據個人愛好,JDK1.6+(1.6之后才自帶httpserver)
- maven環境,項目使用maven構建
- git,如果你想clone我的代碼做參考的話,當然github也支持直接下載zip包
二、功能和結構設計
我們動手寫代碼之前應該先確定好項目的功能,並設計好項目的結構,雖然我們目前需要實現的很簡單,但是還是應該簡單的進行一番設計。項目計划的功能如下:
- 基本的http請求接收和響應
- 可以分別處理動態和靜態資源請求,對於靜態請求直接返回對應資源,對於動態請求處理后返回
- 簡單的模板處理,通過替代符替換的方法實現模板數據渲染
- 簡單的log支持,不是必須,但是卻很有用
看起來並沒有多少功能,但是其實僅僅這幾個功能我們就能完成一個小型的動態網站了。在這里需要提一點,如果你還一點都不了解web服務器的工作流程,可以先看 這篇博客了解一下。這里我們先看一些本次實現的服務器的工作流程圖(用在線的 gliffy畫的,挺不錯):
現在功能方面已經清晰了,那么我們據此來分析一下項目結構的設計,對於http的請求和響應處理我們目前直接使用jdk自帶的httpserver處理(httpserver使用);我們需要實現一個比較核心的模塊實現各部分之間的銜接,根據httpserver我們可以實現一個EHHttpHandler來處理,其實現了HttpHandler接口,主要功能是接收http請求,判斷類型,解析參數,調用業務處理conroller,調用視圖處理Viewhandler,最后響應http請求。此外,為了處理業務和視圖渲染我們需實現controller和view相關的類。具體結構代碼見后邊。
三、實現代碼
1、新建並配置項目
首先我們需要新建一個maven項目,首先建立如下結構:
其中主要幾個文件夾和類的功能如下:
- Constants.java存放系統常量,目前主要存放一些路徑,如靜態文件夾路徑等;
- EHServer是入口類,在這里我們初始化配置,並啟動server。
- EHHttpHandler功能前邊已經說過,是項目最核心的類;
- ResultInfo是一個實體類,主要用來傳輸Controller處理后的結果;
- Controller是一個空接口,主要考慮后期拓展;IndexController是業務處理類,其可調用server進行業務處理,並返回結果;
- ViewHandler是視圖處理,根據controller返回的路徑和參數集合,找到對應模板頁,並替換參數
- src/main/view文件夾主要存放靜態資源(static下)和模板(page下,后綴為.page)
好了,文件結構建好了,我們接下來配置maven依賴,由於主要使用的是jdk自帶的包,因此依賴只需要junit和common-log模塊,pom.xml如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>learn-1</groupId> <artifactId>learn-1</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>learn-1</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies> <build> <build> <finalName>easy-httpserver</finalName> <resources> <resource> <directory>${basedir}/src/main/view</directory> </resource> </resources> </build> </build> </project>
2、實現主服務類EHServer
EHServer用來加載配置,初始化基本信息和啟動Server,其代碼如下:
/** * 主服務類 * @author guojing * @date 2014-3-3 */ public class EHServer { private final Log log = LogFactory.getLog(EHServer.class); /** * 初始化信息,並啟動server */ public void startServer() throws IOException { log.info("Starting EHServer......"); //設置路徑 Constants.CLASS_PATH = this.getClass().getResource("/").getPath(); Constants.VIEW_BASE_PATH = "page"; Constants.STATIC_RESOURCE_PATH = "static"; //設置端口號 int port = 8899; // 啟動服務器 HttpServerProvider provider = HttpServerProvider.provider(); HttpServer httpserver = provider.createHttpServer(new InetSocketAddress(port), 100); httpserver.createContext("/", new EHHttpHandler()); httpserver.setExecutor(null); httpserver.start(); log.info("EHServer is started, listening at 8899."); } /** * 項目main */ public static void main(String[] args) throws IOException { new EHServer().startServer(); } }
可以看到上邊代碼使用了httpserver,那么接收的請求又是如何處理的呢?我們可以看到httpserver綁定了EHHttpserver類,而具體的處理就是在該類內完成的,下邊我們就來看看該類的實現。
3、實現EHHttpserver、controller和viewHandler
EHHttpserver實現了HttpHandler,而HttpHandler是httpserver包提供的,實現其內的handle()方法后,在接收到請求后,httpserver將調用該方法進行處理。我們將在該方法內判斷請求類型並進行相應處理,handle實現代碼如下:
public void handle(HttpExchange httpExchange) throws IOException { try { String path = httpExchange.getRequestURI().getPath(); log.info("Receive a request,Request path:" + path); // 根據后綴判斷是否是靜態資源 String suffix = path .substring(path.lastIndexOf("."), path.length()); if (Constants.STATIC_SUFFIXS.contains(suffix)) { byte[] bytes = IOUtil.readFileByBytes(Constants.CLASS_PATH + "static" + path); responseStaticToClient(httpExchange, 200, bytes); return; } // 調用對應處理程序controller ResultInfo resultInfo = invokController(httpExchange); // 返回404 if (resultInfo == null || StringUtil.isEmpty(resultInfo.getView())) { responseToClient(httpExchange, 200, "<h1>頁面不存在<h1>"); return; } // 解析對應view並返回 String content = invokViewHandler(resultInfo); if (content == null) { content = ""; } responseToClient(httpExchange, 200, content); return; } catch (Exception e) { httpExchange.close(); log.error("響應請求失敗:", e); } }
可以看到首先根據url后綴判斷請求資源是否屬於靜態資源,如果是的話,則讀取對應資源並調用responseStaticToClient返回,如果不是則調用invokController進行業務處理,而invokController內部十分簡單,僅實例化一個IndexController(本次示例controller,直接寫死,以后將使用反射動態映射),調用其process方法。IndexController代碼如下:
/**
* 主頁對應的contoller
* @author guojing
*/
public class IndexController implements Controller{
public ResultInfo process(Map<String, Object> map){
ResultInfo result =new ResultInfo();
result.setView("index");
result.setResultMap(map);
return result;
}
}
在controller中示例了一個ResultInfo對象,並設置view為index(模板路徑為page/index.page),並設置將請求參數直接賦值。而EHHttpserver在調用controller后,將ResultInfo傳遞給invokViewHandler處理。invokViewHandler和invokeController一樣,只是一個適配方法,其內部調用ViewHandler進行處理,ViewHandler將找到對應模板,並將其中替換符(這里定義為${XXXXX})替換為對應參數的值,其代碼如下:
/** * 處理頁面信息 * @author guojing * @date 2014-3-3 */ public class ViewHandler { /** * 處理View模板,只提供建單變量(格式${XXX})替換,已廢棄 * @return */ public String processView(ResultInfo resultInfo) { // 獲取路徑 String path = analysisViewPath(resultInfo.getView()); String content = ""; if (IOUtil.isExist(path)) { content = IOUtil.readFile(path); } if (StringUtil.isEmpty(content)) { return ""; } // 替換模板中的變量,替換符格式:${XXX} for (String key : resultInfo.getResultMap().keySet()) { String temp = ""; if (null != resultInfo.getResultMap().get(key)) { temp = resultInfo.getResultMap().get(key).toString(); } content = content.replaceAll("\\$\\{" + key + "\\}", temp); } return content; } /** * 解析路徑(根據Controller返回ResultInfo的view),已廢棄 * @param viewPath * @return */ private String analysisViewPath(String viewPath) { String path = Constants.CLASS_PATH + (Constants.VIEW_BASE_PATH == null ? "/" : Constants.VIEW_BASE_PATH+"/") + viewPath + ".page"; return path; } }
在ViewHandler處理完后,就可以返回數據了,因為處理不同,這里把動態請求和靜態請求分開處理,代碼如下;
/** * 響應請求 * * @param httpExchange * 請求-響應的封裝 * @param code * 返回狀態碼 * @param msg * 返回信息 * @throws IOException */ private void responseToClient(HttpExchange httpExchange, Integer code, String msg) throws IOException { switch (code) { case 200: { // 成功 byte[] bytes = msg.getBytes(); httpExchange.sendResponseHeaders(code, bytes.length); OutputStream out = httpExchange.getResponseBody(); out.write(bytes); out.flush(); httpExchange.close(); } break; case 302: { // 跳轉 Headers headers = httpExchange.getResponseHeaders(); headers.add("Location", msg); httpExchange.sendResponseHeaders(code, 0); httpExchange.close(); } break; case 404: { // 錯誤 byte[] bytes = "".getBytes(); httpExchange.sendResponseHeaders(code, bytes.length); OutputStream out = httpExchange.getResponseBody(); out.write(bytes); out.flush(); httpExchange.close(); } break; default: break; } } /** * 響應請求,返回靜態資源 * * @param httpExchange * @param code * @param bytes * @throws IOException */ private void responseStaticToClient(HttpExchange httpExchange, Integer code, byte[] bytes) throws IOException { httpExchange.sendResponseHeaders(code, bytes.length); OutputStream out = httpExchange.getResponseBody(); out.write(bytes); out.flush(); httpExchange.close(); }
四、測試項目
至此,我們已經完成了之前預期的功能,現在我們來測試一下到底能否運行。我們在src/main/view/下本別建立如下文件:
其中index.page是動態模板頁,test.js是一個js文件,而tx.jpg是一張圖片。各代碼如下:
index.page <html> <head> <script type="text/javascript" src="/js/test.js"></script> </head> <body> <h1>Hello,${name}</h1> <img src="/pic/tx.jpg" title="tx" /> <script type="text/javascript"> hello(); </script> </body <html> test.js function hello(){ console.log("hello!") }
下邊我們啟動項目,輸出如下:
可以看到啟動成功,用瀏覽器打開:http://localhost:8899/index.page?name=guojing,發現響應頁面如下:
五、總結
至此版本learn-1完成,基於該項目我們已經能夠實現一個簡單的網站,但是也就只是比純靜態網站多了頁面數據渲染,並不能真正的實現動態交互。那么如何才能做到呢?答案就是提供session支持,下一版本我們將加入session支持,是其更加完善。
最后附上源碼(github)地址:源代碼
