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


    距離開篇已經過了很久,期間完善了一下之前的版本,目前已經能夠完好運行,基本上該有的功能都有了,此外將原來的測試程序改為示例項目,新項目只需按照示例項目結構實現controller和view即可,詳情見: easy-httpserverdemo-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>
pom.xml

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

    可以看到上邊代碼使用了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);
        }
    }
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

四、測試項目

    至此,我們已經完成了之前預期的功能,現在我們來測試一下到底能否運行。我們在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)地址:源代碼



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM