• 简易 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())

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

  • 相关阅读:
    【bzoj4399】魔法少女LJJ 并查集+权值线段树合并
    【bzoj4059】[Cerc2012]Non-boring sequences 分治
    【bzoj4390】[Usaco2015 dec]Max Flow LCA
    【bzoj4127】Abs 树链剖分+线段树
    【bzoj1222】[HNOI2001]产品加工 背包dp
    【bzoj4966】总统选举 随机化+线段树
    protected internal == internal
    框架的一点小随笔
    WPF 的 数据源属性 和 数据源
    Python 运算符重载
  • 原文地址:https://www.cnblogs.com/mikevictor07/p/4060173.html
Copyright © 2020-2023  润新知