簡易 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,中間使用空格分隔,請求頭每個屬性一行,使用\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.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())

寫入響應頭后再寫入響應體,也就是請求的資源內容。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM