該簡易的J2EE WEB容器缺失不少功能,卻能夠提供給你們學習HTTP容器大體流程。html
注:容器功能不多,只供學習。git
1. 支持靜態內容與Servlet,不支持JSPweb
2. 僅支持304/404服務器
3. 該設計參考Jetty容器網絡
GIT地址:https://git.oschina.net/redcode/jerry.gitapp
1、HTTP請求處理流程:webapp
HTTP包的解析直接使用Socket讀取InputStream,再根據HTTP協議讀取HTTP請求頭於數據體,HTTP GET請求頭相似以下:socket
GET / HTTP/1.1
Accept: */*
Accept-Language: zh-CN
User-Agent:
Accept-Encoding: gzip, deflate
Host: www.baidu.com
Connection: Keep-Alive
1. 如GET / HTTP/1.1表明是GET 請求,請求路徑爲/,協議版本爲HTTP 1.1,中間使用空格分隔,請求頭每一個屬性一行,使用\n換行(WINDOWS爲\r\n)。ide
當解析Socket的InputStream的時候首先讀取第一行,代碼相似以下:函數
BufferedReader br = new BufferedReader( new InputStreamReader(socket.getInputStream()) ); String reqCmd = br.readLine(); if(reqCmd == null){ return null; //數據包不正常,忽略 } String[] cmds = reqCmd.split("\\s");
2. POST 請求包相似以下:
POST /login HTTP/1.1 Accept: */* User-Agent: Host: Pragma: no-cache Cookie: Content-Length: 25 count=1&viewid=lNe3tRpyVj
請求頭後換行,再封裝POST請求數據:count=1&viewid0=lNe3tRpyVj
解析POST請求包時,讀取請求頭後再讀取數據,存入Map中。檢查請求類型以下:
//Request method check if(!HttpMethod.isAccept(cmds[0])) { return null; }
接受的請求類型枚舉:
public enum HttpMethod { GET, POST; public static boolean isAccept(String method) { for(HttpMethod m : HttpMethod.values()) { if(m.name().equals(method)) { return true; } } return false; } public static HttpMethod getMethod(String method){ for(HttpMethod m : HttpMethod.values()) { if(m.name().equals(method)) { return m; } } return null; } }
POST 請求需讀取 Content-Length 屬性,即須要知道POST包中的參數包大小,當TCP包被拆分經過幾條鏈路到達目的地時,根據包長度使得服務端能合理的等待數據到來。
//Read headers String line; int contentLength = 0; HashMap<String,String> headers = new HashMap<String, String>(); while( (line = br.readLine()) != null && !line.equals("") ) { int idx = line.indexOf(": "); if(idx == -1) { continue; } if(HttpHeaders.CONTENT_LENGTH.equals(line)) { contentLength = Integer.parseInt(line.substring(idx+2).trim()); } headers.put(line.substring(0, idx), line.substring(idx+2)); }
2、整體設計說明:
1. 從Main函數開始說明應該的設計方法,有些機制可用於其餘軟件的設計。
部署結構以下:
%HOME%/lib/* ----依賴包
%HOME%/conf/* -----配置文件夾
%HOME%/startup.sh ---啓動SHELL
%HOME%/logs/* ----日誌文件夾
%HOME%/webapps/* ----頁面部署路徑
這個設計方法很相似於TOMCAT。ECLIPSE包結構截圖以下:
工程啓動類 org.mike.jerry.launcher.Main
lib類加載器 org.mike.jerry.launcher.ClassPath
服務加載類 org.mike.jerry.launcher.Bootstrap,該類中讀取配置並啓動服務端口監聽。
配置文件conf/config.properties 默認配置80端口,啓動後使用 http://127.0.0.1便可訪問。
2. 請求接受與線程池
真正處理請求即爲org.mike.jerry.server.SocketConnector ,啓動與接受請求:
protected ServerSocket newServerSocket(String host, int port,int backlog) throws IOException{ ServerSocket ss= host==null? new ServerSocket(port,backlog): new ServerSocket(port,backlog,InetAddress.getByName(host)); return ss; } public void accept() throws IOException { log.info("Server started ..."); while(started){ Socket socket = serverSocket.accept(); ConnectorEndPoint connector = new ConnectorEndPoint(socket); connector.dispatch(); } }
每次請求開啓一個ConnectorEndPoint線程處理,該線程從線程池中獲取(org.mike.jerry.server.util.thread.ThreadPool),處理以下:
/* Request Handler */ protected class ConnectorEndPoint extends SocketEndPoint implements Runnable { public ConnectorEndPoint(Socket socket) throws IOException { super(socket); socket.setSoTimeout(7000); } public void dispatch() { threadPool.dispatch(this); } @Override public void run() { ...... } }
3. HTTP包解析器
HTTP包解析類由org.mike.jerry.http.HttpRequestDecoder工做,HTTP請求處理都位於org.mike.jerry.http包中。
請求解析工做有幾點:
1. 讀取請求頭,區分GET POST,獲取請求頭屬性,GET讀取URL中的符號「?」並解析參數,POST須要根據Content-Length再讀取請求體中的請求參數。
把解析完成的數據存入Request中,根據Servlet設計規範,Request中須要存儲請求體放入ServletInputStream in中,以供容器使用者在Servlet中能讀取到InputStream.
2. 請求讀取完畢後 把Resuqet交與 ResourceHandler 處理,讀取所須要請求的資源。
4. 讀取資源
資源的讀取中,默認請求爲/的會固定讀取/index.html文件,該屬性本應該在web.xml中配置,不過爲了學習簡易,硬編碼於此。
1. 首先檢查這路徑是否在Servlet中有匹配的,若是沒有,則進行下一步。
2. 從webapps文件夾中讀取請求的文件,若是不存在,則返回404,若是存在,則進行下一步。
3. 讀取請求中的ETag碼,這個標誌相似於MD五、SHA1等文件摘要,用於標誌文件是否改變,若是未改變,則返回304,節省服務器資源(CPU、磁盤與網絡等)
,只是MD5與SHA1計算文件摘要須要的CPU週期較長,固計算方法修改以下:
public String getWeakETag() { try{ StringBuilder b = new StringBuilder(32); b.append("M/\""); int length=uri.length(); long lhash=0; for (int i=0; i<length;i++) lhash= 31*lhash + uri.charAt(i); B64Code.encode(file.lastModified()^lhash, b); B64Code.encode(length^lhash, b); b.append('"'); return b.toString(); } catch (IOException e) { throw new RuntimeException(e); } }
5. 若是文件發生改變,則從新讀取文件字節流,放入響應包Response中。
5. 響應HTTP包封裝
5.1 響應頭輸出: 首先獲取socket輸出流,再寫出頭信息,127.0.0.1抓包工具可以使用rawcap,獲得pcap包後使用wireshark查看,格式相似於:
HTTP/1.1 200 OK
ETag: M/"AJMRnIhabgYAJMQ2H/NnL0"
Date: Wed, 5 Nov 2014 09:58:17 GMT
Content-Length: 1102
Last-Modified: Wed, 2 Jul 2014 23:01:08 GMT
Connection: Keep-Alive
Content-Type: text/html
Server: M
Cache-Control: private
相應代碼如:
OutputStream out = socket.getOutputStream(); //config status message String respStat = HttpStatus.getMessage(response.getStatus()); StringBuilder headers = new StringBuilder(); headers.append(response.getHttpVersion() + " " + response.getStatus() + " " + respStat + StringUtil.CRLF); //write headers for(Map.Entry<String, String> header : response.getHeaders().entrySet()){ headers.append(header.getKey() + ": " + header.getValue() + StringUtil.CRLF); } headers.append(StringUtil.CRLF);//響應頭寫入完畢必須空一行,這也是協議規定,以區分響應體 out.write(headers.toString().getBytes())
寫入響應頭後再寫入響應體,也就是請求的資源內容。