android端實現http服務器,具備文件上傳等功能,純JAVA實現,無依賴包

最近需要在我們的安卓設備上實現通過網頁訪問設備,進行相關配置、上傳數據等操作,因此就需要在安卓端實現一個http服務器。(其實代碼也可以用於PC端,只不過PC端已經有太多成熟的框架了,JDK7/8之後貌似就內置了一個輕量的HTTP服務器)。

採用java socket實現的http服務器網上有較多的例子,但是例子大部分都比較簡單,不具備文件上傳的功能,於是結合網上的列子動手寫了個具備文件上傳、請求資源文件、處理請求的簡單的http服務器,需要的朋友可以參考下:
這裏寫圖片描述
- 創建socket 監聽,客戶端瀏覽器在打開地址後,就會向服務器建立一個tcp連接(http協議是基於TCP協議封裝的)
- 服務器獲取客戶端的輸入流並解析頭部,也就是說需要解析瀏覽器封裝的HTTP協議信息
- 根據解析的頭部信息判斷客戶端請求的資源類型,並進行相應的處理

處理http請求的部分,這裏不再詳述,大家也可直接參考代碼,重點說下文件上傳部分:
1、根據Content-Type來判斷當前是否爲文件上傳請求,即:
this.contentType.startsWith(“multipart/form-data”);表示當前爲上傳文件請求
2、接下來就是根據RFC協議去解析請求體中的上傳文件數據了, RFC協議中規定的http上傳文件文件格式如下:
這裏寫圖片描述
其中的——————————–1878979834就是“邊界值”,是文件數據的開始、結束標誌。
一開始也就是在這裏遇到的難處,不知道是不是有的人也跟我一開始的做法類似,使用bufferedReader去按行解析,最後卻發現提取出來的數據與實際的文件不一致、不完整。並不是編碼格式的原因。原因應該是bufferedReader默認會提前緩存了一部分數據,導致最終按行讀取獲取到的數據不完整(距離問題解決過去了很久,具體原因是不是這個原因記得不是太清)。
總之最後發現在如果文件上傳的話就不能採用bufferdReader去按行讀取。既然這樣那就得自己去按字節解析,也就是說自己去實現readLine,可以看到,我實現了兩個readline方法,第一個讀取一行後返回改行的字符串,用於解析http請求的頭部。第二個將一行的數據(包括換行符)讀取到byte[]用於提取上傳的文件。以及一個快速比較byte數組的方法,用於快速判斷當前是否讀取到了文件的邊界,代碼如下:

    /**
     * 讀取一行
     * @return 
     */
    private String readLine() {
        try {
            byte[] buffer = new byte[1024];//默認緩存1024
            int b = 0, pos = 0;
            while ((b = mStream.read()) >= 0) {
                buffer[pos] = (byte) b;
                if (pos > 0 && buffer[pos] == 0x0A && buffer[pos - 1] == 0x0D) {//讀取到換行符0d 0a或13 10 表示換行
                    return new String(buffer, 0, pos - 1);
                }
                pos++;//當前座標+1
                if (pos == buffer.length) {//緩衝區已滿則擴充緩存區
                    byte[] old = buffer;
                    buffer = new byte[old.length + 1024];//每次擴充1024
                    System.arraycopy(old, 0, buffer, 0, old.length);
                }
            }
            return new String(buffer, 0, pos);
        } catch (Exception ex) {
            return null;
        }
    }
    /**
     * 讀取一行byte,返回讀取的長度
     * @param buffer
     * @return
     */
    private int readLine(byte[] buffer) {
        int b = 0, pos = 0;
        try {
            while (pos<buffer.length&&(b = mStream.read()) !=-1) {
                buffer[pos] = (byte) b;
                if (pos > 0 && buffer[pos] == 0x0A && buffer[pos - 1] == 0x0D) {//讀取到換行符0d 0a或13 10 表示換行
                    return pos+1;//返回讀取的長度
                }
                pos++;//當前座標+1
            }
        } catch (Exception ex) {
        }
        return pos;
    }
    /**
     * 比較byte數組
     * 
     * @param src
     * @param des
     * @return
     */
    private boolean startsWith(byte[] src, byte[] des) {
        if (src.length < des.length) {
            return false;
        }
        for (int i = 0; i < des.length; i++) {
            if (src[i] != des[i]) {
                return false;
            }
        }
        return true;
    }

其實關鍵就是需自己去實現readline方法,下面是獲取客戶端輸入流後,解析輸入流的過程代碼:

public MyRequest(InputStream in) {
        mStream = new BufferedInputStream(in);
        try {
            //讀取第一行, 請求地址
            String line = this.readLine();
            if (line == null) {
                return;
            }
            // 獲得請求的資源的地址
            String resource = line.substring(line.indexOf("/"), line.lastIndexOf("/") - 5);
            this.requestUrl = URLDecoder.decode(resource, "UTF-8");// 反編碼 URL地址
            this.method = new StringTokenizer(line).nextElement().toString();// 獲取請求方法, GET或者POST
            // 讀取所有瀏覽器發送過來的請求參數頭部信息
            int contentLen = 0;// 如果爲POST方法,則會有消息體長度
            while ((line = this.readLine()) != null) {
                Log.d(TAG, line);
                if (line.startsWith("Content-Length")) {
                    contentLen = Integer.parseInt(line.split(":")[1].trim());
                }
                if (line.startsWith("Content-Type")) {
                    this.contentType = line.split(":")[1].trim();
                }
                if (line.equals("")) {// 空行表示頭結束
                    break;
                }
            }
            if ("POST".equalsIgnoreCase(this.method) && contentLen > 0) {// 顯示 POST表單提交的內容, 這個內容位於請求的主體部分
                try {
                    if (this.contentType != null && this.contentType.startsWith("multipart/form-data")) {// 上傳文件請求
                        this.filePath=this.extractRFCFile();
                    } else {
                        byte[] buffer = new byte[contentLen];
                        mStream.read(buffer, 0, buffer.length);// 不能調用readline,否則會導致阻塞
                        String postTextBody = new String(buffer);
                        this.params = parseQueryParam(postTextBody);
                    }
                } catch (Exception ex) {
                    logger.error("讀取POST數據出錯", ex);
                }
            }
            int queryIndex = this.requestUrl.indexOf("?");
            if (queryIndex > 0) {
                this.url=this.requestUrl.substring(0,queryIndex);
                if (this.params == null) {
                    this.params = new HashMap<String, String>();
                }
                this.params.putAll(parseQueryParam(this.requestUrl.substring(queryIndex + 1)));
            }else{
                this.url=this.requestUrl;
            }
        } catch (Exception ex) {
            logger.error("解析http請求爲request出錯", ex);
        }
    }

下面是提取上傳文件並寫到本地臨時文件的代碼:

/**
     * 提取http上傳的文件
     * @return 
     */
    private String extractRFCFile() {
        byte[] boundary = this.readLine().getBytes();// 根據RFC協議第一行是邊界
        Log.d(TAG, "上傳數據,邊界爲:" + new String(boundary));
        String line;
        while ((line = this.readLine()) != null) {
            Log.d(TAG, line);
            if (line.startsWith("Content-Disposition")) {
                //獲取上傳的文件名
            }
            if (line.equals("")) {// 空行表示頭結束
                break;
            }
        }
        String savePath=Confing.CLIENT_BASEPATH+"upload.tmp";
        try {
            FileOutputStream fos = new FileOutputStream(savePath);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = this.readLine(buffer)) > 0) {
                if (this.startsWith(buffer, boundary)) {
                    Log.d(TAG, "文件讀取結束");
                    break;
                }
                fos.write(buffer, 0, count);//會導致多一個換行符
            }
            fos.flush();
            fos.close();
        } catch (Exception ex) {
            logger.error("提取HTTP上傳的文件出錯", ex);
            return null;
        }
        return savePath;
    }

下面是接受一個客戶端socket接入後的處理部分代碼:

static class HttpWorkHandle extends Thread {
        private Socket client;

        public HttpWorkHandle(Socket socket) {
            this.client = socket;
        }

        public void run() {
            try {
                if (client != null) {
                    logger.info("連接到服務器的用戶:" + client);
                    InputStream ins = null;
                    OutputStream out = null;
                    try {
                        ins = client.getInputStream();
                        out = client.getOutputStream();
                        MyRequest request = new MyRequest(ins);
                        if (request.method == null) {
                            return;
                        }
                        MyResponse response = new MyResponse(out);
                        //根據contentType判斷是不是業務類請求
                        if (request.contentType.startsWith("application/x-www-form-urlencoded") 
                                || request.contentType.startsWith("application/json")
                                || request.contentType.startsWith("multipart/form-data")
                                || request.url.endsWith(".do")) {
                            PrintWriter pw = new PrintWriter(out, true);
                            String responseText = new WebDispatcher(mContext).doRespond(request, response);
                            pw.println("HTTP/1.0 200 OK");// 返回應答消息,並結束應答
                            pw.println("Content-Type:text/plain;charset=UTF-8");
                            pw.println();// 根據 HTTP 協議, 空行將結束頭信息
                            if (responseText != null) {
                                pw.print(responseText);
                            }
                            pw.close();
                        } else {// 請求資源文件
                            try {
                                String resource;
                                if (request.requestUrl.equals("/") || request.requestUrl.endsWith(SERVNAME)) {
                                    resource = "web/index.html";
                                } else {
                                    resource = "web" + request.requestUrl.substring(request.requestUrl.indexOf(SERVNAME) + SERVNAME.length());
                                }
                                InputStream stream = null;
                                try {
                                    stream = mContext.getAssets().open(resource);
                                } catch (Exception ex) {
                                    Log.d("HTTPServer", "資源文件不存在:" + resource);
                                }
                                if (stream != null) {
                                    byte[] buffer = new byte[1024 * 4];
                                    int len;
                                    while ((len = stream.read(buffer)) != -1) {
                                        out.write(buffer, 0, len);
                                    }
                                    out.flush();
                                    stream.close();
                                } else {
                                    out.write(this.errorMessage().getBytes());// 返回失敗信息
                                }
                            } catch (Exception ex) {
                                logger.error("客戶端請求資源文件出錯", ex);
                            }
                        }
                    } catch (Exception ex) {
                        logger.error("HTTP服務器錯誤", ex);
                    } finally {
                        if (ins != null) {
                            ins.close();
                        }
                        if (out != null) {
                            out.close();
                        }
                        client.close();
                    }
                }
            } catch (Exception ex) {
                logger.error("處理客戶端請求出錯", ex);
            }
        }

以下是別人總結的Http協議相關的東西,結合這個基本就可以理解在解析http頭部時的一些步驟了:
** HTTP請求包括的內容
客戶端連上服務器後,向服務器請求某個web資源,稱之爲客戶端向服務器發送了一個HTTP請求。
一個完整的HTTP請求包括如下內容:一個請求行、若干消息頭、以及實體內容,範例:
  
 HTTP請求的細節——請求行
  請求行中的GET稱之爲請求方式,請求方式有:POST、GET、HEAD、OPTIONS、DELETE、TRACE、PUT,常用的有: GET、 POST
  用戶如果沒有設置,默認情況下瀏覽器向服務器發送的都是get請求,例如在瀏覽器直接輸地址訪問,點超鏈接訪問等都是get,用戶如想把請求方式改爲post,可通過更改表單的提交方式實現。
  不管POST或GET,都用於向服務器請求某個WEB資源,這兩種方式的區別主要表現在數據傳遞上:如果請求方式爲GET方式,則可以在請求的URL地址後以?的形式帶上交給服務器的數據,多個數據之間以&進行分隔,例如:GET /mail/1.html?name=abc&password=xyz HTTP/1.1
  GET方式的特點:在URL地址後附帶的參數是有限制的,其數據容量通常不能超過1K。
  如果請求方式爲POST方式,則可以在請求的實體內容中向服務器發送數據,Post方式的特點:傳送的數據量無限制。
 HTTP請求的細節——消息頭
  HTTP請求中的常用消息頭
  Accept代表發送端(客戶端)希望接受的數據類型
Content-Type代表發送端(客戶端|服務器)發送的實體數據的數據類型
  Accept-Charset: 瀏覽器通過這個頭告訴服務器,它支持哪種字符集
  Accept-Encoding:瀏覽器通過這個頭告訴服務器,支持的壓縮格式
  Accept-Language:瀏覽器通過這個頭告訴服務器,它的語言環境
  Host:瀏覽器通過這個頭告訴服務器,想訪問哪臺主機
  If-Modified-Since: 瀏覽器通過這個頭告訴服務器,緩存數據的時間
  Referer:瀏覽器通過這個頭告訴服務器,客戶機是哪個頁面來的 防盜鏈
  Connection:瀏覽器通過這個頭告訴服務器,請求完後是斷開鏈接還是何持鏈接
以application開頭的媒體格式類型:
• application/xhtml+xml :XHTML格式
• application/xml : XML數據格式
• application/atom+xml :Atom XML聚合格式
• application/json : JSON數據格式
• application/pdf :pdf格式
• application/msword : Word文檔格式
• application/octet-stream : 二進制流數據(如常見的文件下載)
• application/x-www-form-urlencoded : 中默認的encType,form表單數據被編碼爲key/value格式發送到服務器(表單默認的提交數據的格式)
• multipart/form-data : 需要在表單中進行文件上傳時,就需要使用該格式
 HTTP響應包括的內容
  一個HTTP響應代表服務器向客戶端回送的數據,它包括: 一個狀態行、若干消息頭、以及實體內容 。
  
 HTTP響應的細節——狀態行
  狀態行格式: HTTP版本號 狀態碼 原因敘述
舉例:HTTP/1.1 200 OK
  狀態碼用於表示服務器對請求的處理結果,它是一個三位的十進制數。響應狀態碼分爲5類,如下所示:

 5.3、HTTP響應細節——常用響應頭
  HTTP響應中的常用響應頭(消息頭)
  Location: 服務器通過這個頭,來告訴瀏覽器跳到哪裏
  Server:服務器通過這個頭,告訴瀏覽器服務器的型號
  Content-Encoding:服務器通過這個頭,告訴瀏覽器,數據的壓縮格式
  Content-Length: 服務器通過這個頭,告訴瀏覽器回送數據的長度
  Content-Language: 服務器通過這個頭,告訴瀏覽器語言環境
  Content-Type:服務器通過這個頭,告訴瀏覽器回送數據的類型
  Refresh:服務器通過這個頭,告訴瀏覽器定時刷新
  Content-Disposition: 服務器通過這個頭,告訴瀏覽器以下載方式打數據
  Transfer-Encoding:服務器通過這個頭,告訴瀏覽器數據是以分塊方式回送的
  Expires: -1 控制瀏覽器不要緩存
  Cache-Control: no-cache
  Pragma: no-cache
**

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章