該簡易的J2EE WEB容器缺失很多功能,卻可以提供給大家學習HTTP容器大緻流程。
注:容器功能很少,隻供學習。
1. 支援靜态内容與Servlet,不支援JSP
2. 僅支援304/404
3. 該設計參考Jetty容器
GIT位址:https://git.oschina.net/redcode/jerry.git
一、HTTP請求處理流程:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SY2ADOlNGZwEzYhFGMzIzM0MDZ5AjNhJjM3IzN3UjMi9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
HTTP包的解析直接使用Socket讀取InputStream,再根據HTTP協定讀取HTTP請求頭于資料體,HTTP GET請求頭類似如下:
GET / 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 enumHttpMethod {
GET,
POST;public static booleanisAccept(String method) {for(HttpMethod m : HttpMethod.values()) {if(m.name().equals(method)) {return true;
}
}return false;
}public staticHttpMethod getMethod(String method){for(HttpMethod m : HttpMethod.values()) {if(m.name().equals(method)) {returnm;
}
}return null;
}
}
POST 請求需讀取 Content-Length 屬性,即需要知道POST包中的參數包大小,當TCP包被拆分通過幾條鍊路到達目的地時,根據包長度使得服務端能合理的等待資料到來。
//Read headers
String line;int contentLength = 0;
HashMap headers = new HashMap();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));
}
二、總體設計說明:
1.從Main函數開始說明應該的設計方法,有些機制可用于其他軟體的設計。
部署結構如下:
%HOME%/lib
protected class ConnectorEndPoint extends SocketEndPoint implementsRunnable {public ConnectorEndPoint(Socket socket) throwsIOException {super(socket);
socket.setSoTimeout(7000);
}public voiddispatch() {
threadPool.dispatch(this);
}
@Overridepublic voidrun() {
......
}
}
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碼,這個标志類似于MD5、SHA1等檔案摘要,用于标志檔案是否改變,如果未改變,則傳回304,節省伺服器資源(CPU、磁盤與網絡等)
,隻是MD5與SHA1計算檔案摘要需要的CPU周期較長,固計算方法修改如下:
publicString getWeakETag() {try{
StringBuilder b= new StringBuilder(32);
b.append("M/\"");int length=uri.length();long lhash=0;for (int i=0; i
lhash= 31*lhash +uri.charAt(i);
B64Code.encode(file.lastModified()^lhash, b);
B64Code.encode(length^lhash, b);
b.append('"');returnb.toString();
}catch(IOException e) {throw newRuntimeException(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= newStringBuilder();
headers.append(response.getHttpVersion()+ " "
+ response.getStatus() + " " + respStat +StringUtil.CRLF);//write headers
for(Map.Entryheader : response.getHeaders().entrySet()){
headers.append(header.getKey()+ ": " + header.getValue() +StringUtil.CRLF);
}
headers.append(StringUtil.CRLF);//響應頭寫入完畢必須空一行,這也是協定規定,以區分響應體
out.write(headers.toString().getBytes())
寫入響應頭後再寫入響應體,也就是請求的資源内容。