簡易 HTTP Server 實現(JAVA)

該簡易的J2EE WEB容器缺失不少功能,卻能夠提供給你們學習HTTP容器大體流程。html

注:容器功能不多,只供學習。git

1. 支持靜態內容與Servlet,不支持JSPweb

2. 僅支持304/404服務器

3. 該設計參考Jetty容器網絡

GIT地址:https://git.oschina.net/redcode/jerry.gitapp

1、HTTP請求處理流程:webapp

HTTP包的解析直接使用Socket讀取InputStream,再根據HTTP協議讀取HTTP請求頭於數據體,HTTP GET請求頭相似以下:socket

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)。ide

當解析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));
}

 

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碼,這個標誌相似於MD五、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())

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

相關文章
相關標籤/搜索