---
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);
}
});
}
}
```
**--實做--**
> 
### 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());
}
}
});
}
```
**--實做--**
> 
### 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 訪問本地
> 
* 設定 config 在 /ect/apache2/apache.conf,在這邊可以看到設定的目錄
> 
* 從上面可以看出來,**默認根目錄在 var/www/ 目錄底下**,該目錄下有一個 http 的目錄,在這底下新增本地訪問的檔案
:::danger
本定網頁要創建在 **/var/www/html** 目錄才能訪問,創建在 var/www 目錄下是不能透過 127.0.0.1/<File/> 訪問的
:::
**--訪問--**
> 使用遊覽器訪問 127.0.0.1/XmlFile.xml
>
> 
## 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());
}
}
});
}
}
```
**--實做--**
> 
### 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 格式
> 
2. JsonArray 格式
> 
:::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");
}
}
});
}
}
```
**--實做--**
> 
### 使用 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");
}
}
});
}
}
```
**--實做--**
> 
## Appendix & FAQ
:::info
xml 使用 SAX 實做時,分析異常,沒有正常 app 結尾,而是 age 遺留下來的,似乎沒有呼叫 startElement...
> 
:::
###### tags: `Android 基礎`