该简易的J2EE WEB容器缺失很多功能,却可以提供给大家学习HTTP容器大致流程。
注:容器功能很少,只供学习。
1. 支持静态内容与Servlet,不支持JSP
2. 仅支持304/404
3. 该设计参考Jetty容器
GIT地址:https://git.oschina.net/redcode/jerry.git
一、HTTP请求处理流程:
HTTP包的解析直接使用Socket读取InputStream,再根据HTTP协议读取HTTP请求头于数据体,HTTP GET请求头类似如下:
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,中间使用空格分隔,请求头每个属性一行,使用 换行(WINDOWS为 )。
当解析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)); }
二、总体设计说明:
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码,这个标志类似于MD5、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())
写入响应头后再写入响应体,也就是请求的资源内容。