打造一款屬於本身的web服務器——從簡單開始

    距離開篇已通過了好久,期間完善了一下以前的版本,目前已經可以無缺運行,基本上該有的功能都有了,此外將原來的測試程序改成示例項目,新項目只需按照示例項目結構實現controller和view便可,詳情見: easy-httpserverdemo-httpsrever
    此次咱們將首先來實現一個簡單版本,該版本只包括一些基本的功能,後續版本也將在此基礎上一步步改進。 javascript

1、準備工做

    俗話說的好,工欲善其事,必先利其器。咱們在開始開發以前應作好以下準備(真的很簡單):html

  • java開發環境,IDE依據我的愛好,JDK1.6+(1.6以後才自帶httpserver)
  • maven環境,項目使用maven構建
  • git,若是你想clone個人代碼作參考的話,固然github也支持直接下載zip包

、功能和結構設計

   咱們動手寫代碼以前應該先肯定好項目的功能,並設計好項目的結構,雖然咱們目前須要實現的很簡單,可是仍是應該簡單的進行一番設計。項目計劃的功能以下:html5

  • 基本的http請求接收和響應
  • 能夠分別處理動態和靜態資源請求,對於靜態請求直接返回對應資源,對於動態請求處理後返回
  • 簡單的模板處理,經過替代符替換的方法實現模板數據渲染
  • 簡單的log支持,不是必須,可是卻頗有用

    看起來並無多少功能,可是其實僅僅這幾個功能咱們就能完成一個小型的動態網站了。在這裏須要提一點,若是你還一點都不瞭解web服務器的工做流程,能夠先看 這篇博客瞭解一下。這裏咱們先看一些本次實現的服務器的工做流程圖(用在線的 gliffy畫的,挺不錯):
java

    如今功能方面已經清晰了,那麼咱們據此來分析一下項目結構的設計,對於http的請求和響應處理咱們目前直接使用jdk自帶的httpserver處理(httpserver使用);咱們須要實現一個比較核心的模塊實現各部分之間的銜接,根據httpserver咱們能夠實現一個EHHttpHandler來處理,其實現了HttpHandler接口,主要功能是接收http請求,判斷類型,解析參數,調用業務處理conroller,調用視圖處理Viewhandler,最後響應http請求。此外,爲了處理業務和視圖渲染咱們需實現controller和view相關的類。具體結構代碼見後邊。 git

3、實現代碼

一、新建並配置項目

    首先咱們須要新建一個maven項目,首先創建以下結構: github

    其中主要幾個文件夾和類的功能以下:web

  • 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以下:
apache

<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>
pom.xml

二、實現主服務類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();
    }
}
View Code

    能夠看到上邊代碼使用了httpserver,那麼接收的請求又是如何處理的呢?咱們能夠看到httpserver綁定了EHHttpserver類,而具體的處理就是在該類內完成的,下邊咱們就來看看該類的實現。服務器

三、實現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);
        }
    }
View Code

    能夠看到首先根據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;
    }
}
View Code

    在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();
    }
View Code

4、測試項目

    至此,咱們已經完成了以前預期的功能,如今咱們來測試一下到底可否運行。咱們在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,發現響應頁面以下:

5、總結

    至此版本learn-1完成,基於該項目咱們已經可以實現一個簡單的網站,可是也就只是比純靜態網站多了頁面數據渲染,並不能真正的實現動態交互。那麼如何才能作到呢?答案就是提供session支持,下一版本咱們將加入session支持,是其更加完善。    
  最後附上源碼(github)地址:源代碼

相關文章
相關標籤/搜索