zoukankan      html  css  js  c++  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.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())

    写入响应头后再写入响应体,也就是请求的资源内容。

  • 相关阅读:
    Codeforces 177G2 Fibonacci Strings KMP 矩阵
    Codeforces Gym100187C Very Spacious Office 贪心 堆
    Codeforces 980F Cactus to Tree 仙人掌 Tarjan 树形dp 单调队列
    AtCoder SoundHound Inc. Programming Contest 2018 E + Graph (soundhound2018_summer_qual_e)
    BZOJ3622 已经没有什么好害怕的了 动态规划 容斥原理 组合数学
    NOIP2016提高组Day1T2 天天爱跑步 树链剖分 LCA 倍增 差分
    Codeforces 555C Case of Chocolate 其他
    NOIP2017提高组Day2T3 列队 洛谷P3960 线段树
    NOIP2017提高组Day2T2 宝藏 洛谷P3959 状压dp
    NOIP2017提高组Day1T3 逛公园 洛谷P3953 Tarjan 强连通缩点 SPFA 动态规划 最短路 拓扑序
  • 原文地址:https://www.cnblogs.com/mikevictor07/p/4060173.html
Copyright © 2011-2022 走看看