訪問Tomcat伺服器返回資料亂碼
前序:
在網路中,資料的傳輸,最常用的格式有兩種:XML和JSON 。
今天在做一個app版本更新檢查。流程是:
1、Andriod客戶端 向 Tomcat伺服器 發起Http請求。
2、伺服器響應並返回資料。返回的資料中,包含了新版app的特性和更新內容。並通過一個Dialog 對話方塊的形式,來告知使用者,新版的app作了那些方面的改進。也就是呼叫dialog.setMessage()來設定訊息內容,結果發現全是亂碼。
3、之前一直沒遇到這種情況,後來在QQ群了問了才知道,原來這個涉及到了編碼的問題。
客戶端和服務端傳送資料的過程:
先貼個Tomcat webapps目錄底下的升級檔案內容圖:
這個檔案,我是直接在桌面新建一個txt文件,然後強制修改檔案型別為json的。之後用記事本開啟,在裡面編寫內容。最後經過個人驗證,這種做法是很有問題的,也是埋下亂碼的一個伏筆。
再貼個亂碼的圖:
一、先梳理一下:客戶端訪問網路的流程:
首先一定要清楚的一點:網路傳遞的是位元組流,所以從伺服器到android的轉換過程如下:
在分析這個圖之前。我們先來,理一理app訪問網路的一個思路:
1、客戶端使用 HttpURLConnection 向伺服器請求響應。
2、伺服器接收到請求後,響應請求,並返回資料。
3、客戶端接收到資料。此時,客戶端一般都是接受到一個InputStream物件(輸入流)。
4、用InputStream 生產各種物件,最後轉成一個String 物件,也就是一個字串。然後開始對這個字串進行解析(這時候 XML和JSON 這兩個格式 的解析方法 就派上用場了)。
5、資料解析完成後,各個變數拿到自己的物件後。各幹各的去了。
但是,大家有沒有想過,我們拿到的資料,是用什麼編碼方式得出了的資料?這也是為什麼會出現亂碼的關鍵點了。
需要再清楚的一點:
1、在進行XML 或者JSON 資料解析之前,我們能利用的資源,有且只有一個,也就是伺服器返的唯一的一樣東西—-InputStream(輸入流)。
2、 我們拿到這個輸入流之後,經過一連串處理,得到一個字串。具體怎麼處理,等下看程式碼就行了。
3、 接著根據字串的格式,xml格式?還是Json格式?進行資料解析。然後拿到我們想要的東西。
好了,廢話不多說,我們還是來貼程式碼吧。
有人說中文是最美的語言,但我認為是最操蛋的語言。因為無論你說什麼,都他媽的可以有各種意思!
這裡給大家提供一個比較標準的Android訪問網路的程式碼:
private void sendRequestWithHttpURLConnection() {
//訪問網路,首要就是--開啟子執行緒。
new Thread(new Runnable() {
@Override
public void run() {
//下面4個變數定義在try catch塊外面,是因為如果定義在try裡面,finally裡面就拿不到變量了,
//關閉不了物件,會造成記憶體洩露。
HttpURLConnection connection = null;
InputStream is = null;
BufferedReader buffer = null;
String result = null;
try {
//這裡我訪問的是我Tomcat的伺服器資料。
URL url = new URL("http://1r667695p8.iok.la:37179/mydata/get_data.json");
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
is = connection.getInputStream();
buffer = new BufferedReader(new InputStreamReader(is));
StringBuilder response = new StringBuilder();
String line;
//開啟連線後,子執行緒會迴圈讀buffer的資料,直到null為止。不符合條件,才會往下執行。
while ((line = buffer.readLine()) != null) {
response.append(line);
}
//轉換為字串
result = response.toString();
//字串解析(用什麼方法解析,主要看:字串是什麼格式 xml 還是json格式)
parseJSONWithJSONObject(result);
} catch (Exception e) {
Log.d("異常捕獲", "http post error");
} finally {
//這幾個一定要關閉,否則會造成記憶體洩漏。
if (buffer != null) {
try {
buffer.close();
} catch (IOException ignored) {
}
}
if (is != null) {
try {
is.close();
} catch (IOException ignored) {
}
}
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
上面的程式碼,估計大家都很熟悉。但是不知道大家有沒有想過這幾行程式碼:
從14行到18行:
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(“GET”);
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
is = connection.getInputStream();
我們一起來看一看,14行程式碼開啟一個連線(connection),接著設定一些引數,18行就去接收輸入流了。問題來了,我們都知道,程式都是一行一行執行的。而訪問網路,肯定也是需要時間的。你憑什麼在18行,就可以把輸入流賦值給is變數?也就是說CPU執行到
is = connection.getInputStream();
的時候,按照函式的返回值的思路,我們都認為,它只是一個執行connection.getInputStream()完成後的一個返回值,但是其實,is 相當於一個盒子,connection.getInputStream()應該是啟動了某個操作,這個操作則不斷的去獲取資料,直到資料沒有了。執行緒只是啟動這個操作,之後就不管它了,執行緒接著往下走。
is = connection.getInputStream();
//這行程式碼是先執行getInputStream()這個方法,應該是開啟了一個執行緒(有待驗證!)
//資料解析,程式碼很簡單。
private void parseJSONWithJSONObject(String result) {
try {
JSONObject obj = new JSONObject(result);
String apkUrl = obj.getString("url"); //APK下載路徑
String updateMessage = obj.getString("updateMessage");//版本更新說明
int apkCode = obj.getInt("versionCode"); //新版APK對於的版本號
//取得已經安裝在手機的APP的版本號 versionCode
int versionCode = getCurrentVersionCode();
//對比版本號判斷是否需要更新
if (apkCode > versionCode) {
showDialog(updateMessage, apkUrl);
}
} catch (JSONException e) {
LogX.d(TAG, "parse json error");
}
}
對話方塊的方法,下圖
private void showDialog(String content, final String downloadUrl) {
//字串 content 可能需要先轉碼
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setTitle(R.string.dialog_choose_update_title);
builder.setMessage(content)
.setPositiveButton(R.string.dialog_btn_confirm_download, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
//下載apk檔案
goToDownloadApk(downloadUrl);
}
})
.setNegativeButton(R.string.dialog_btn_cancel_download, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
}
});
AlertDialog dialog = builder.create();
//點選對話方塊外面,對話方塊不消失
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
其中Sting型別的引數content 。就是我們要顯示的中文了,只不過它現在還是經過編碼的字串。當我們顯示到手機螢幕的時候,Android會幫我們解碼。拿到對應的字符集的字。如果我們拿到的不是utf-8的編碼,那麼此時,我們是需要進行轉碼的,轉成utf-8。然後給Android顯示端。 (主要是要明白,解碼在那個階段地方,上面的程式碼沒能體現出轉碼的地方,因為我們拿到的就是utf-8)
二、現在來談談編碼方式
1、編碼的問題
在談編碼方式之前,大家先來做一個小實驗:
大家在電腦上新建一個txt型別的文件,然後雙擊開啟,在裡面輸入:聯通2字 。
接著儲存並退出。然後直接雙擊開啟剛才的txt文件,你會發現,咦,怎麼是亂碼了!!!
這是為什麼呢?這就涉及到了編碼方式的問題了。這是因為我們的儲存和開啟的方式並不是同一種。這樣的話,解碼的過程就出錯了,看到的就是亂碼了。我們的電腦上都有很多不同格式型別的檔案,比如: txt文件、記事本、word文件等等,那麼這個編碼方式是由誰來確定的呢?是我們系統平臺Windows、Linux,還是我們編輯的軟體呢。回想一下剛才的小實驗,和Android studio 中常用的xml 檔案。答案肯定是編輯軟體了。
也就是說,我們在儲存的時候,當前的編輯軟體是先把我們的內容通過編碼的方式,打包成ByteArray,然後交給系統,系統幫我們儲存資料包。如果是由系統編碼,那系統不累死了,而且市場上那麼多型別的檔案,系統怎麼知道你這個軟體要使用那一種編碼?所以系統的責任只是儲存。它不管你裡面是什麼,只要是資料包就ok。
整個流程,我們可以用下面一個圖來描述:
對同一個字元,如果採用了不同字符集來編碼,那麼生成的值可能是不一樣。比如,對同一個中文,採用不同的字符集來編碼,得到的數值都不一樣。那麼解碼的時候,如果用另一套字符集,那肯定會亂碼。
2、解碼的問題
好了,既然知道了儲存的時候使用的是某一種編碼方式,那麼開啟的時候,肯定也要使用相應的解碼方式,這樣才能獲得正確的資料。
三、本次亂碼的問題的原因
由上面的知識知道,儲存的時候會有一個編碼的過程,開啟的時候會有一個解碼的過程。但是,Tomcat並不提供一個開啟過程,而是在啟動的時候,自動去載入webapps 底下目錄的資源。
1、也就是說,這些資源(其實就是ByteArray)是被載入到記憶體中的,並不像我們直接使用軟體去開啟,所以我們不能從視覺上,直觀的看到載入到記憶體中的資料有沒有變化,或者說存不存在亂碼的問題。換句話說,我們並不知道,Tomcat有沒有對系統交給它的ByteArray進行解碼。
2、Tomcat在響應請求的時候,傳送資料之前會不會對自己記憶體的資料進行編碼呢?
換句話說,如果Tomcat在載入資源的時候有進行解碼,而它用了卻用了自己特有的方式去解碼,那麼Tomcat載入到記憶體中的資料本身就已經成亂碼了,如果在傳送的時候,Tomcat再進行一次編碼,那就是亂上加亂,火上澆油了。這樣,客戶端完全沒法解碼了!
如果我們能搞清楚上面的兩個問題,那麼一切都會迎刃而解了。然而,理想是美好的,現實卻是殘酷的。由於知識所限,我們根本沒法驗證這些東西。
Tomcat的整個載入流程我們可以這麼來描述:
四、問題的解決。
雖然以上的大多數問題,我們都瞭解了。但是,我們根本就無法去驗證。
比如:你寫了一個json型別的檔案,你怎麼知道你當前的編輯軟體用的是什麼編碼格式呢?
如下圖所示:
這時候,我想起了,我們Android Studio 中使用的編碼方式是utf-8。那麼我們可不可以用Android Studio 去開啟我們這個json型別的檔案呢?之後修改並儲存。這樣的話,我們儲存在系統上的資料,就是採用utf-8這種編碼方式了。這時候,只好動手來試一試了。
然後我們用Android Studio去開啟我們的json升級檔案。
什麼鬼,怎麼中文全是亂碼?到這裡,大家應該明白了吧,Android Studio 是使用utf-8 來編碼的,解碼當然也用utf-8了。換個角度來說,這也證明了,我們之前用記事本儲存資料的時候,它的編碼並不是utf-8。那麼解碼方式不對,也就亂碼了!所以我們只好在Android Studio開啟的方式下,手動修改下。
修改好之後,儲存。然後啟動我們的Tomcat,再用APP端向服務端申請資料。
耶,終於不是亂碼了!
從這個實驗中,我們可以得出兩個結論:
1、Tomcat在傳送資料的時候,是會對資料進行編碼的,用的是utf-8編碼。因為Android在顯示的時候,是一定會對資料進行解碼的。如果Tomcat不進行編碼,那Android端得到的就是亂碼。
2、Tomcat載入資源的時候,肯定是會對資源進行解碼的。它的解碼方式也是utf-8,為什麼說肯定會解碼呢,因為不解碼的話,資料在記憶體中做了各種加減乘除運算後,鬼知道你是什麼了。
小結:儲存資料要編碼—載入資料要解碼—傳送服務要編碼—接收資料要解碼 。四個環節的方式都要一樣,這樣才不會亂碼。
通過這件事情,我明白了,原來我們系統上儲存的資源,都是經過編碼的ByteArray
最後給大家一個,android端向服務端post資料時,編碼的轉換流程圖:
張鴻洋的部落格:
一定要注意這句:伺服器(Tomcat)預設使用iso-8859-1解碼。Iso-8859-1是不支援中文的,也就是說不做處理,中文是一定亂碼的。
這裡說的是:解碼用的是iso-8859-1,並不是編碼。很多人的部落格都是亂寫的。小編用的是Tomcat6.0。就沒有去設定這個玩意。
在TOMCAT的配置檔案的server.xml中更改: