起因
作為全職PHPer偶爾需要客串下Androider,最近公司的一個項目需要Android的用戶端(主要圖片特效處理及其上傳),自己就客串下Androider.
之前有過Android開發經驗所以做這個挺順手的,幾乎所有東西直接github中拿過來改改就用,不過在處理圖片上傳的時候選擇了xUtils這個
開源工具類,用起來確實比較好用,挺方便的,例如如下代碼就可以實現上傳:
RequestParams params = newRequestParams();
params.addBodyParameter("file", file);
HttpUtils httpUtils = newHttpUtils();
httpUtils.send(HttpRequest.HttpMethod.POST, UPLOAD_URL, params, newRequestCallBack<String>() {
@Override
//上傳失敗處理方法
publicvoidonFailure(HttpException arg0, String msg) {
alert(msg);
}
@Override
//上傳進度處理
publicvoidonLoading(longtotal,longcurrent,
booleanisUploading) {
if(isUploading) {
Log.i(LOG_NAME, "upload:"+ current +"/"+ total);
}
}
@Override
//上傳成功處理
publicvoidonSuccess(ResponseInfo<String> responseInfo) {
alert(responseInfo.result);
Log.i(LOG_NAME, responseInfo.result);
}
});
可以看到用起來比較方便,如果自己寫還是比較麻煩的。不過最讓人頭疼的不是使用方法,而是作為接收端為PHP的話是接收不到上傳的檔案,最後經證實
不僅僅是PHP C# 也有問題, 網上搜素了下不少人都遇到問題不過沒有解決方案,看來只能自己動手解決了
問題分析
既然要解決問題,那麼就需要分析bug可能出現的地方,既然是HTTP上傳那麼我們得知道在PHP 在HTTP協議中檔案是怎麼處理上傳的,直接在官方文檔
就可以找到
PHP 能夠接受任何來自符合 RFC-1867 標準的瀏覽器(包括 Netscape Navigator 3 及更高版本,打了補丁的 Microsoft Internet Explorer 3 或者更高版本)上傳的檔案。
這個是PHP官方文檔中給出的解釋, PHP在處理上傳的時候遵循的是RFC-1867標準,那麼我們接下來看看什麼是RFC-1867。
這裡我給出一個RFC-1867的說明文檔地址 RFC-1867 說明 ,太長了就不放在這裡了只拿核心重點內容過來看看:
# The client might send back the following data use POST method:
Content-type: multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="field1"
Joe Blow
--AaB03x
content-disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
這裡通俗點講,RFC-1867通過HTTP協議傳輸特定的格式來實現上傳檔案,比如我們上傳一個檔案名稱字叫做hello.txt的文本到服務端,那麼發起請求的HTTP格式應該就如下:
POST /up.phpHTTP/1.1
Host:creturn.com
Content-Length:294
Content-type:multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="file"; filename="hello.txt"
Content-Type: text/plain
hello
--AaB03x--
這裡HTTP頭部協議沒有寫完整隻是為了說明問題寫的格式。在這些描述資訊中
第一個Content-type(http頭部描述資訊)值multipart/form-data 是告訴伺服器此次發送的資料是上傳檔案資料, boundary是告訴伺服器檔案資料之間分割標示符是 AaB03x
在HTTP body資料中以—AaB03x 分割多個檔案,每個分隔字元下面的描述資訊是作為上傳檔案的描述資訊
發送結束後需要以分隔字元加“—”符號進行標示
如果這三點沒有問題那麼就能正確上傳,當然次執行個體中肯定不成功因為HTTP頭部協議描述資訊簡短切不爭取(比如長度)
解決過程
既然上面已經對問題進行分析了,同樣也知道了只要發送過程是按照RFC-1867的標準進行發送那麼至少PHP是能夠接收到上傳的檔案,那麼接下來我們要解決的就是如果判斷
或者查看xUtils傳送檔案過程中是否遵循了RFC-1867標準
那麼如何查看xUtils是否發送了正確的資料格式? 有兩種方案,一個是利用代理工具抓包,另外一個方案就是直接抓包
這裡就說說直接抓包,代理抓包可以google一大堆。
pc 上面建立無線熱點分享給手機,這樣所有的資料都通過電腦走,不會用pc分享熱點的google一大堆,或者為了偷懶買個傳說中的mini WIFI都行
抓包工具window推薦smartSniff, linux或者os x直接就tcpdump也行
先寫個簡單的app裝到手機上這裡給出上傳代碼:
String UPLOAD_URL = "http://www.creturn.com/up.php";
File file = newFile(Environment.getExternalStorageDirectory() ,"hello.txt");
RequestParams params = newRequestParams();
params.addBodyParameter("file", file);
HttpUtils httpUtils = newHttpUtils();
httpUtils.send(HttpRequest.HttpMethod.POST, UPLOAD_URL, params, newRequestCallBack<String>() {
@Override
//上傳失敗處理方法
publicvoidonFailure(HttpException arg0, String msg) {
alert(msg);
}
@Override
//上傳進度處理
publicvoidonLoading(longtotal,longcurrent,
booleanisUploading) {
if(isUploading) {
Log.i(LOG_NAME, "upload:"+ current +"/"+ total);
}
}
@Override
//上傳成功處理
publicvoidonSuccess(ResponseInfo<String> responseInfo) {
alert(responseInfo.result);
Log.i(LOG_NAME, responseInfo.result);
}
});
上面代碼作用是把sdcard根目錄的hello.txt檔案上傳到UPLOAD_URL, 所以在sdcard根目錄放一個hello.txt檔案裡面內容隨便寫點
php服務端這邊就直接列印上傳的檔案資訊就行,代碼很簡單:
<?php
print_r($_FILES);
?>
如果上傳成功就會反饋上傳檔案的資訊,寫好app裝到手機串連好wifi然後在pc上面抓包,我這裡用的是smart sniffer
抓包
開啟smart sniffer
選擇菜單 Options -> Capture Options
選擇你分享wifi的網卡
確定點擊開始抓包
在手機app上操作上傳檔案時候就可以看到抓包工具中已經有相應的http資料,抓包工具會對所有流量抓取所以如果有其他包幹擾還可以
設定相應的過濾規則,這裡就不闡述google就能找到
看看我們抓到的包內容:
圖種可以看到我們上傳的HTTP包資訊,不過很明顯反饋的資訊提示是沒有上傳成功的。
那麼接下來我們怎麼去分析這個?怎麼去排查問題?很多人應該能夠想到,要是有個正確參照物不就很容易分析出問題出處?
那麼我們在建立一個html檔案用瀏覽器同樣上傳sdcard裡面的hello.txt檔案,html內容如下:
<html>
<body>
<formaction="http://www.creturn.com/up.php"method="post"enctype="multipart/form-data">
<labelfor="file">Filename:</label>
<inputtype="file"name="file"id="file"/>
<br/>
<inputtype="submit"name="submit"value="Submit"/>
</form>
</body>
</html>
用同樣的方法抓包看看正確的包內容是什麼樣的:
POST /up.phpHTTP/1.1
Host:www.creturn.com
Connection:keep-alive
Content-Length:294
Cache-Control:max-age=0
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin:http://222.73.234.196
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryIexdOW8e2EZyciDK
Referer:http://222.73.234.196/up.html
DontTrackMeHere:gzip,deflate,sdch
Accept-Language:zh-CN,zh;q=0.8
------WebKitFormBoundaryIexdOW8e2EZyciDK
Content-Disposition: form-data; name="file"; filename="hello.txt"
Content-Type: text/plain
hello upload
------WebKitFormBoundaryIexdOW8e2EZyciDK
Content-Disposition: form-data; name="submit"
Submit
------WebKitFormBoundaryIexdOW8e2EZyciDK--
可以看到處理HTTP頭部描述資訊和包體裡面多了一個Submit,幾乎一樣
之前說過上傳過程中的幾個重點,然後對比下我們發現Content-Type描述資訊多了一個charset字元編碼描述資訊
那麼要做測試的話肯定就要把不同的地方去掉,然後對包進行回放看看是否成功
包回放指的是包資料包重新發送一次
回放資料包有兩種方法,一種直接修改xUtils源碼重新上傳。這裡說一個簡單的方法window內建的telnet ,用telnet 連結的伺服器80連接埠
手動發送資料
注意: windows7預設沒有安裝需要在控制台->程式和功能->開啟或者關閉windows功能中開啟
如何進行手動發送?按照我們之前的想法去掉charset描述資訊然後手動發送,那麼先去掉charset資訊後的包內容放入記事本:
POST /up.phpHTTP/1.1
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.73.11 (KHTML, like Gecko) Version/7.0.1 Safari/537.73.11
Content-Length:233
Content-Type:multipart/form-data; boundary=6wYgRevA02R_Uy4EJP31EcIJtsBlZtRv
Host:wwwcreturn.com
Connection:Keep-Alive
DontTrackMeHere:gzip
--6wYgRevA02R_Uy4EJP31EcIJtsBlZtRv
Content-Disposition: form-data; name="file"; filename="hello.txt"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
hello upload
--6wYgRevA02R_Uy4EJP31EcIJtsBlZtRv--
開啟cmd 然後輸入telnet www.creturn.com 80 然後黏貼進去看看效果
為了印證我們的才行可以把charset加上去和去掉的進行對比看看是不是加了之後就收不到上傳檔案的資訊。
其實根據HTTP協議來講理論上加不加charset應該不會影響上傳,但結果這個問題確實是由於charset引起的。
接下來就簡單了找到根源解決就行,在源碼裡面進行搜尋 boundary ,找到地方根據作者寫的方法注釋掉其中添加charset的代碼:
protectedStringgenerateContentType(
finalString boundary,
finalCharset charset) {
StringBuilder buffer = newStringBuilder();
buffer.append("multipart/"+ multipartSubtype +"; boundary=");
buffer.append(boundary);
//這裡就是需要注釋掉的代碼
/*if (charset != null) {
buffer.append("; charset=");
buffer.append(charset.name());
}*/
returnbuffer.toString();
}