最近需要在我們的安卓設備上實現通過網頁訪問設備,進行相關配置、上傳數據等操作,因此就需要在安卓端實現一個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
**