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

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

1、準備工做

    俗話說的好,工欲善其事,必先利其器。咱們在開始開發以前應作好以下準備(真的很簡單):
  • java開發環境,IDE依據我的愛好,JDK1.6+(1.6以後才自帶httpserver)
  • maven環境,項目使用maven構建
  • git,若是你想clone個人代碼作參考的話,固然github也支持直接下載zip包

、功能和結構設計

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

  • 基本的http請求接收和響應
  • 能夠分別處理動態和靜態資源請求,對於靜態請求直接返回對應資源,對於動態請求處理後返回
  • 簡單的模板處理,經過替代符替換的方法實現模板數據渲染
  • 簡單的log支持,不是必須,可是卻頗有用
     看起來並無多少功能,可是其實僅僅這幾個功能咱們就能完成一個小型的動態網站了。在這裏須要提一點,若是你還一點都不瞭解web服務器的工做流程,能夠先看 這篇博客 瞭解一下。這裏咱們先看一些本次實現的服務器的工做流程圖(用在線的 gliffy 畫的,挺不錯):

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

3、實現代碼

一、新建並配置項目

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


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

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

<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>

二、實現主服務類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類,而具體的處理就是在該類內完成的,下邊咱們就來看看該類的實現。

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

4、測試項目

    至此,咱們已經完成了以前預期的功能,如今咱們來測試一下到底可否運行。咱們在src/main/view/下本別創建以下文件: github

    其中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,發現響應頁面以下: web

5、總結

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