--- title: 'Android 網路請求' disqus: kyleAlien --- Android 網路請求 === ## OverView of Content [TOC] ## 使用 HttpURLConnection * 原本 Android 原生的網路請求 api 還有 HttpClient,但由於 api 數量過多,不易維護,所以已經在 Android 6.0 中移除 * HttpURLConnection 屬於 Java 原生厙 `import java.net.HttpURLConnection`,使用 [**URL**](https://hackmd.io/M5PlGj6_TEiHok7IK5A2LA?view#Java-URI) 來獲取其實例 (URL 就可以連接網路) `java.net.URL;` * 一般順序如下 :::info 1. 創建 URL 2. 使用 openConnection 獲取 HttpURLConnection 實例 3. 設定請求頭 & 其他連線設定 4. 取得返回輸入流並顯示 ::: | 類 | 方法 | 功能 | | -------- | -------- | - | | URL | new URL(String) | 指向 URL 網址 | | URL | openConnection() | 返回與遠程對象連接的實例 | | HttpURLConnection | setRequestMethod(String) | 設定 [**Http 請求頭**](https://hackmd.io/M5PlGj6_TEiHok7IK5A2LA?view#Http-Request-%E7%B5%90%E6%A7%8B) | | HttpURLConnection | setConnectTimeout(long) | 連接超時 | | HttpURLConnection | setReadTimeout(long) | 讀取超時 | | HttpURLConnection | getInputStream() | 獲取網路返回的 in 數據 | | HttpURLConnection | getOutputStream() | 獲取網路返回的 out 數據 | ### GET 請求 ```java= public class MainActivity extends AppCompatActivity { private TextView MyTxt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyTxt = findViewById(R.id.MyTxt); new Thread(new Runnable() { @Override public void run() { try { String str = GET_HUC(); updateUI(str); } catch (IOException e) { e.printStackTrace(); } } }).start(); } private String GET_HUC() throws IOException { URL url = new URL("https://www.google.com/"); // 從 URL 中獲取 HttpURLConnection 實例 HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // 設定請求頭 GET connection.setRequestMethod("GET"); // 設定連線超時 connection.setConnectTimeout(8000); // 讀取超時 connection.setReadTimeout(8000); // 獲取網路端返回的輸入流 InputStream inputStream = connection.getInputStream(); InputStreamReader reader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(reader); StringBuilder stringBuilder = new StringBuilder(); String line; while((line = bufferedReader.readLine()) != null) { stringBuilder.append(line); } bufferedReader.close(); reader.close(); inputStream.close(); return stringBuilder.toString(); } private void updateUI(final String str) { runOnUiThread(new Runnable() { @Override public void run() { MyTxt.setText(str); } }); } } ``` **--實做--** > ![](https://i.imgur.com/1k2LVEO.png) ### POST 請求 * 傳送數據至服務器,以下演示一個使用 Post 提交密碼的方法 ```java= private void POST_HUC(String acount, String password) throws IOException { URL url = new URL("https://www.google.com/"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); OutputStream o = connection.getOutputStream(); DataOutputStream dos = new DataOutputStream(o); // 寫出資料 dos.writeBytes("username=" + acount + "&password" + password); dos.close(); o.close(); } ``` :::warning 1. 耗時操作作用在子線程中,並使用 runOnUiThread() 將任務排序至 Handler 中 2. 別忘了在 Manifeset 中聲明權限,否則 APP 會直接跳掉(++連警告都不給你++) > <uses-permission android:name="android.permission.INTERNET"/> ::: ## 使用 okHttp * [**okHttp**](https://github.com/square/okhttp) 是由 Square 公司開發(Retrofit 也是該公司開發),使用簡單,並且封裝了許多 Http 協議的細節 * 在使用時因為是第三方庫,必須加入依賴,**該依賴會下載 OkHttp 厙 & Okio 厙(通訊基礎**) ```groovy= dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) ... implementation 'com.squareup.okhttp3:okhttp:3.14.7' } ``` | 類 | 方法 | 功能 | | -------- | -------- | -------- | | OkHttpClient | new OkHttpClient() | 建立客戶端使用 | | Request | new Request.Builder() | **請求頭的創建**,可在這裡設置請求頭的參數 | | RequestBody | new FromBody.Builder() | **==POST== 請求體的創建(GET 沒有請求體所以不用)** | | Call | OkHttpClient.newCall(Request) | **Call 為 interface,用剛剛創建的客戶端來建立任務,該任務接收 Request 類** | | Response | Call.execute()/Call.enqueue(CallBack) | **execute 任務的返回,enqueue 不會有返回類,但是使用 CallBack 接口(該接口內就有 Response 引數)** | * 一般步驟如下 :::info 1. 創建客戶端 OkHttpClient 2. 創建請求頭 Request.Builder() 3. 使用剛剛創建的客戶端建立任務 OkHttpClient.newCall(Request) 並且接收請求頭 4. 執行任務 (同步 / 異步) 5. 接口請求 Response ::: :::warning * OkHttp3 有使用到 [**Lambda**](https://hackmd.io/gV5_yfM-RHuVX257ur-P6A#Lambda-%E8%A1%A8%E9%81%94%E5%BC%8F),所以必須在 gradle 中使用 compileOptions 設定 JDK 1.8以上 ```groovy= android { compileSdkVersion 29 buildToolsVersion "29.0.3" ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } ``` ::: ### Get 請求 * 由於最後的請求 Response 有分為 **同步(execute)、異步(enqueue** 兩種所以以下會演示兩種方法 1. 同步 execute,同步方法要使用 new Thread(new Runnable()) 執行任務 ```java= private void GET_OK_Async() { // 建立客戶端 OkHttpClient client = new OkHttpClient(); // 建立請求頭 Request request = new Request.Builder() .url("https://www.google.com/") .build(); // 使用客戶端建立 Call 任務連線 Call call = client.newCall(request); // 同步請求,會堵塞 try { Response response = call.execute(); if(response.body() != null) { updateUI(response.body().string()); } } catch (IOException e) { e.printStackTrace(); } } ``` 2. 異步 enqueue ```java= private void GET_OK_Sync() { // 客戶端 OkHttpClient client = new OkHttpClient(); // 建立請求頭 Request request = new Request.Builder() .url("https://www.google.com/") .build(); // 使用客戶端建立 Call 任務連線 Call call = client.newCall(request); // 異步請求 call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { MyTxt.setText(e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if(response.body() != null) { updateUI(response.body().string()); } } }); } ``` **--實做--** > ![](https://i.imgur.com/KzYfEXj.png) ### POST 請求 * 多創建一個請求體 RequestBody ```java= public static void POST_OK() { // 創建客戶端 OkHttpClient client = new OkHttpClient(); // 創建 POST 的請求體 RequestBody body = new FormBody.Builder() .add("username", "admin") .add("password", "123456") .build(); // 創建請求頭,並記得加入請求體 Request request = new Request.Builder() .url("https://www.google.com/") .post(body) .build(); // 創建任務 Call call = client.newCall(request); // 異步請求 call.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if(response.body() != null) { } } }); } ``` ## 傳輸格式 * 在網路上傳輸資料要會透過幾種固定格式(就如同 protocol),**最常用的是 XML、JSON**,使用 Apache 創建一個本地服務端(127.0.0.1)來 **模擬我們訪問伺服器** (這樣才方便以下測試 API 使用) :::danger Anrdoid API 在 28 以上時就會強制使用 https 來訪問網路,使用 http 會報出 CLEARTEXT communication to ...的錯誤,[**參考簡書**](https://www.jianshu.com/p/57047a84e559) 1. 改 https (本地就無法使用) 2. targetSdkVersion 降到 27 以下 3. res/xml 目錄(沒有 xml 目錄就自訂)下定義一個 xml 檔,名子自訂但**不能有大寫**,並指定到 Manifest.xml 中 ```xml= <!--新創檔案--> <?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true" /> </network-security-config> <!--Manifest 中新增--> <application ... android:networkSecurityConfig="@xml/my_http"> ... </application> ``` 4. 使用 **Android 虛擬機訪問本地 ip 為 10.0.2.2** ::: ### 安裝 Apache * Ubuntu 中透過指令視窗安裝,可參考[**鳥哥的文章**](http://linux.vbird.org/linux_server/0360apache.php#www_basic_basic) ```shell= # 安裝 sudo apt-get install -y apache2 # 更新 sudo apt-get update ``` * 安裝好後在網頁輸入 127.0.0.1 訪問本地 > ![](https://i.imgur.com/Cl7XhSw.png) * 設定 config 在 /ect/apache2/apache.conf,在這邊可以看到設定的目錄 > ![](https://i.imgur.com/yFT2jrY.png) * 從上面可以看出來,**默認根目錄在 var/www/ 目錄底下**,該目錄下有一個 http 的目錄,在這底下新增本地訪問的檔案 :::danger 本定網頁要創建在 **/var/www/html** 目錄才能訪問,創建在 var/www 目錄下是不能透過 127.0.0.1/<File/> 訪問的 ::: **--訪問--** > 使用遊覽器訪問 127.0.0.1/XmlFile.xml > > ![](https://i.imgur.com/0FUXuur.png) ## XML 格式 解析 XML 格式有 3 種較常用的方式,Pull、SAX、DOM,以下會實做 Pull、SAX 這兩種方法,以下創建一個 XmlFile.xml 檔案作為測試用 ```xml= <apps> <app> <id>1</id> <name>Pan</name> <age>10</age> </app> <app> <id>2</id> <name>Kyle</name> <age>13</age> </app> <app> <id>3</id> <name>Alien</name> <age>21</age> </app> </apps> ``` ### Pull 解析 * 會一次性讀取所有檔案到內存中做解析,相對來說速度較快,但是十分消耗內存容易 OOM ```java= // Pull 解析 xml 文件 public class PullDemo { public static void Start(String str) { try { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser pullParser = factory.newPullParser(); // 拿到解析器 pullParser.setInput(new StringReader(str)); // 設定資料 int event = pullParser.getEventType(); // 獲取解析事件!! String id = "", name = "", age = ""; while (event != XmlPullParser.END_DOCUMENT) { String node = pullParser.getName(); // 取得接點分析 switch (event) { case XmlPullParser.START_DOCUMENT: Log.e("ParseXml", "開始解析"); break; case XmlPullParser.START_TAG: // 開始節點 ex: <app> if(node.equals("id")) { id = pullParser.nextText(); // 該 node 內容 } else if(node.equals("name")) { name = pullParser.nextText(); } else if(node.equals("age")) { age = pullParser.nextText(); } break; case XmlPullParser.END_TAG: // 結束節點 ex: </app> if(node.equals("app")) { Log.e("ParseXml", "id: " + id + ", name: " + name + ", id: " + id); } else { Log.e("ParseXml", "END_TAG: " + pullParser.nextText()); } break; } event = pullParser.next(); // 逐行解析 } } catch (XmlPullParserException | IOException e) { e.printStackTrace(); } } } // 使用者 使用 public class MainActivity extends AppCompatActivity { private TextView MyTxt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyTxt = findViewById(R.id.MyTxt); // 創建客戶端 OkHttpClient client = new OkHttpClient(); // 請求頭 Request request = new Request.Builder() // 虛擬機中 10.0.2.2 就是本地的 127.0.0.1 .url("http://10.0.2.2/XmlFile.xml") .build(); // 任務 + 執行 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d("ParseXml", "Fail: " + e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if(response.body() != null) { PullDemo.Start(response.body().string()); } } }); } } ``` **--實做--** > ![](https://i.imgur.com/udUqeeg.png) ### SAX 解析 * SAX 其語意更加的清晰,使用一個類**繼承 [DefaultHandler](https://developer.android.com/reference/org/xml/sax/helpers/DefaultHandler) 這個 class**,它屬於 `org.xml.sax.helpers.DefaultHandler` 並重寫以下 5 個方法(這 5 個方法都是空實現) | Function Name | 作用 | | -------- | -------- | | startDocument | 開始解析文件 | | startElement | 當接收到 node 開頭時,ex: <app\> | | characters | **這個方法 ==可能被調用多次==** 包括一些換行&空白符號也會調用 | | endElement | 接收到 node 結尾時,ex: </app\> | | endDocument | 文件解析結束 | ```java= // 使用 SAX 工具 public class SAXDemo { public static void Start(String str) throws IOException, ParserConfigurationException, SAXException { SAXParserFactory factory = SAXParserFactory.newInstance(); XMLReader xmlReader = factory.newSAXParser().getXMLReader(); // 創建解析器 MyParse myParse = new MyParse(); // 設定解析器 xmlReader.setContentHandler(myParse); xmlReader.parse(new InputSource(new StringReader(str))); } // 分析工具 private static class MyParse extends DefaultHandler { private String nodeName; private StringBuilder id, name, age; @Override public void startDocument() throws SAXException { Log.e("ParseXml", "start Document"); id = new StringBuilder(); name = new StringBuilder(); age = new StringBuilder(); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { Log.e("ParseXml", "startElement: " + localName); nodeName = localName; } @Override public void characters(char[] ch, int start, int length) throws SAXException { // ch 單個字元 start 開頭 length 長度 if(nodeName.equals("id")) { id.append(ch, start, length); } else if (nodeName.equals("name")) { name.append(ch, start, length); } else if (nodeName.equals("age")) { age.append(ch, start, length); } } @Override public void endElement(String uri, String localName, String qName) throws SAXException { Log.e("ParseXml", "endElement: " + nodeName); if(nodeName.equals("app")) { // id name age 都包含了回車 Log.e("ParseXml", "id: " + id.toString().trim() + ", name: " + name.toString().trim() + ", age: " + age.toString().trim()); // 一個 node 結束後 stringBuilder 長度要設定為 0 id.setLength(0); name.setLength(0); age.setLength(0); } } @Override public void endDocument() throws SAXException { Log.e("ParseXml", "end Document"); id = null; name = null; age = null; } } } // 使用 public class MainActivity extends AppCompatActivity { private TextView MyTxt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyTxt = findViewById(R.id.MyTxt); // 創建客戶端 OkHttpClient client = new OkHttpClient(); // 請求頭 Request request = new Request.Builder() // 虛擬機中 10.0.2.2 就是本地的 127.0.0.1 .url("http://10.0.2.2/XmlFile.xml") .build(); // 任務 + 執行 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d("ParseXml", "Fail: " + e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if(response.body() != null) { try { SAXDemo.Start(response.body().string()); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } } } }); } ``` **--實做--** > ## JSON 格式 與 XML 比較起來 **JSON 數據所需傳輸所消耗的數據量更小**,以下在本地創建一個 JsonFile.json 檔案模擬伺服器 ```jsonld= [ {"id":4, "name":"Pan", "age": 15}, {"id":5, "name":"Kyle", "age":18}, {"id":6, "name":"Alien", "age":25} ] ``` ### 使用 JsonObject * 使用 Android API 屬於 `org.json.*` 包下,主要有分為 **JsonObject, JsonArray**,**兩者可相互套用** | API | 使用符號 | 功能 | | -------- | -------- | -------- | | JsonObject | **{ <key\> ==:== <value\> }** | 它是一種 Key&Value 相對格式,使用分號 `:` 區分開 | | JsonArray | **[ <value\> ==,== <value\> ]** | 它是一種 Array 格式,使用逗號 `,` 區分開 | 1. JsonObject 格式 > ![](https://i.imgur.com/6B3yLjT.png) 2. JsonArray 格式 > ![](https://i.imgur.com/szFgoTf.png) :::info 其 value 值可以是 String, boolean, **null** ::: ```java= // 解析 Json 檔案 public class JsonDemo { public static void Start(String str) throws JSONException { JSONArray array = new JSONArray(str); // 循環 array for(int i = 0; i < array.length(); i++) { JSONObject object = array.getJSONObject(i); String id = object.optString("id"); String name = object.optString("name"); String age = object.optString("age"); Log.e("ParseJson", "id: " + id + ", name: " + name + ", age: " + age); } } } // 使用者 public class MainActivity extends AppCompatActivity { private TextView MyTxt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyTxt = findViewById(R.id.MyTxt); // 創建客戶端 OkHttpClient client = new OkHttpClient(); // 請求頭 Request request = new Request.Builder() // 虛擬機中 10.0.2.2 就是本地的 127.0.0.1 .url("http://10.0.2.2/JsonFile.json") .build(); // 任務 + 執行 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d("ParseJson", "Fail: " + e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if(response.body() != null) { try { JsonDemo.Start(response.body().string()); } catch (JSONException e) { e.printStackTrace(); } } else { Log.d("ParseJson", "No Response"); } } }); } } ``` **--實做--** > ![](https://i.imgur.com/MwvlAYl.png) ### 使用 Gson * Gson 是 google 公司開發的開源厙,**可以更加簡化解析 Json 數據的工作**,由於是第三方厙,所以必須在 gradle 中加上 gson 依賴 ```groovy= dependencies { ... implementation 'com.google.code.gson:gson:2.8.5' } ``` * Gson 可以把數據讀取程對象,**它會自動幫使用者創建這個對象**,所以這個對象就十分的重要,**該對象是個 [POJO](https://hackmd.io/kj2UbsScTZ6bNvf4PMxq6A?both#POJO-amp-JavaBean) 類(Setter、Getter**),Gson 會自動幫你創建該對象 ```java= // Json 數據字串 {"id":4, "name":"Pan", "age": 15} // JavaBean 類 private static class Info { private long id; private String name; private int age; .... } // 讀取到 json 數據 Gson gson = new Gson(); // 透過 反射創建 使用者需要的 Info 類,jsonData 就是上面的字串 Info info = gson.from(jsonData, Info.class); ``` * 當我們有期望的數據時需要用的 TypeToken 類,以下期望收到一個 List 數據 ```java= // Gson 工具 public class GsonDemo { public static void Start(String str) { Gson gson = new Gson(); List<Info> infoList = gson.fromJson(str, new TypeToken<List<Info>>() {}.getType()); for(Info f:infoList) { Log.e("ParseJson", "id: " + f.getId() + ", name: " + f.getName() + ", age: " + f.getAge()); } } // JavaBean 類 private static class Info { private long id; private String name; private int age; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } } // 使用者 public class MainActivity extends AppCompatActivity { private TextView MyTxt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyTxt = findViewById(R.id.MyTxt); // 創建客戶端 OkHttpClient client = new OkHttpClient(); // 請求頭 Request request = new Request.Builder() // 虛擬機中 10.0.2.2 就是本地的 127.0.0.1 .url("http://10.0.2.2/JsonFile.json") .build(); // 任務 + 執行 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d("ParseJson", "Fail: " + e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if(response.body() != null) { GsonDemo.Start(response.body().string()); } else { Log.d("ParseJson", "No Response"); } } }); } } ``` **--實做--** > ![](https://i.imgur.com/nyL5rNW.png) ## Appendix & FAQ :::info xml 使用 SAX 實做時,分析異常,沒有正常 app 結尾,而是 age 遺留下來的,似乎沒有呼叫 startElement... > ![](https://i.imgur.com/jtc8Z5i.png) ::: ###### tags: `Android 基礎`