首页 > 代码库 > 简易 HTTP Server 实现(JAVA)

简易 HTTP Server 实现(JAVA)

该简易的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.1Accept: */*Accept-Language: zh-CNUser-Agent: Accept-Encoding: gzip, deflateHost: www.baidu.comConnection: Keep-Alive

1. GET / HTTP/1.1代表是GET 请求,请求路径为/,协议版本为HTTP 1.1,中间使用空格分隔,请求头每个属性一行,使用\n换行(WINDOWS为\r\n)。

当解析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.1Accept: */*User-Agent: Host: Pragma: no-cacheCookie: Content-Length: 25count=1&viewid=lNe3tRpyVj

请求头后换行,再封装POST请求数据:count=1&viewid0=lNe3tRpyVj

解析POST请求包时,读取请求头后再读取数据,存入Map中。检查请求类型如下:

//Request method checkif(!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中。

-----------------未完待续-------------------

简易 HTTP Server 实现(JAVA)