1. 程式人生 > >訪問Tomcat伺服器返回資料亂碼

訪問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中更改: