## 通過HttpClient發起HTTP請求
Dart IO庫中提供了Http請求的一些類,我們可以直接使用HttpClient來發起請求。使用HttpClient發起請求分為五步:
1. 創建一個HttpClient
```
HttpClient httpClient = new HttpClient();
```
2. 打開Http連接,設置請求頭
```
HttpClientRequest request = await httpClient.getUrl(uri);
```
這一步可以使用任意Http method,如`httpClient.post(...)`、`httpClient.delete(...)`等。如果包含Query參數,可以在構建uri時添加,如:
```
Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
"xx":"xx",
"yy":"dd"
});
```
通過HttpClientRequest可以設置請求header,如:
```
request.headers.add("user-agent", "test");
```
如果是post或put等可以攜帶請求體方法,可以通過HttpClientRequest對象發送request body,如:
```
String payload="...";
request.add(utf8.encode(payload));
//request.addStream(_inputStream); //可以直接添加輸入流
```
3. 等待連接服務器
```
HttpClientResponse response = await request.close();
```
這一步完成后,請求信息就已經發送給服務器了,返回一個HttpClientResponse對象,它包含響應頭(header)和響應流(響應體的Stream),接下來就可以通過讀取響應流來獲取響應內容。
4. 讀取響應內容
```
String responseBody = await response.transform(utf8.decoder).join();
```
我們通過讀取響應流來獲取服務器返回的數據,在讀取時我們可以設置編碼格式,這里是utf8。
5. 請求結束,關閉HttpClient
```
httpClient.close();
```
關閉client后,通過該client發起的所有請求都會中止。
#### 示例
我們實現一個獲取百度首頁html的例子,示例效果如下:

點擊“獲取百度首頁”按鈕后,會請求百度首頁,請求成功后,我們將返回內容顯示出來并在控制臺打印響應header,代碼如下:
```
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
class HttpTestRoute extends StatefulWidget {
@override
_HttpTestRouteState createState() => new _HttpTestRouteState();
}
class _HttpTestRouteState extends State<HttpTestRoute> {
bool _loading = false;
String _text = "";
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints.expand(),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
RaisedButton(
child: Text("獲取百度首頁"),
onPressed: _loading ? null : () async {
setState(() {
_loading = true;
_text = "正在請求...";
});
try {
//創建一個HttpClient
HttpClient httpClient = new HttpClient();
//打開Http連接
HttpClientRequest request = await httpClient.getUrl(
Uri.parse("https://www.baidu.com"));
//使用iPhone的UA
request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
//等待連接服務器(會將請求信息發送給服務器)
HttpClientResponse response = await request.close();
//讀取響應內容
_text = await response.transform(utf8.decoder).join();
//輸出響應頭
print(response.headers);
//關閉client后,通過該client發起的所有請求都會中止。
httpClient.close();
} catch (e) {
_text = "請求失敗:$e";
} finally {
setState(() {
_loading = false;
});
}
}
),
Container(
width: MediaQuery.of(context).size.width-50.0,
child: Text(_text.replaceAll(new RegExp(r"\s"), ""))
)
],
),
),
);
}
}
```
控制臺輸出:
```
I/flutter (18545): connection: Keep-Alive
I/flutter (18545): cache-control: no-cache
I/flutter (18545): set-cookie: .... //有多個,省略...
I/flutter (18545): transfer-encoding: chunked
I/flutter (18545): date: Tue, 30 Oct 2018 10:00:52 GMT
I/flutter (18545): content-encoding: gzip
I/flutter (18545): vary: Accept-Encoding
I/flutter (18545): strict-transport-security: max-age=172800
I/flutter (18545): content-type: text/html;charset=utf-8
I/flutter (18545): tracecode: 00525262401065761290103018, 00522983
```
#### HttpClient配置
HttpClient有很多屬性可以配置,常用的屬性列表如下:
屬性含義idleTimeout對應請求頭中的keep-alive字段值,為了避免頻繁建立連接,httpClient在請求結束后會保持連接一段時間,超過這個閾值后才會關閉連接。connectionTimeout和服務器建立連接的超時,如果超過這個值則會拋出SocketException異常。maxConnectionsPerHost同一個host,同時允許建立連接的最大數量。autoUncompress對應請求頭中的Content-Encoding,如果設置為true,則請求頭中Content-Encoding的值為當前HttpClient支持的壓縮算法列表,目前只有"gzip"userAgent對應請求頭中的User-Agent字段。可以發現,有些屬性只是為了更方便的設置請求頭,對于這些屬性,你完全可以通過HttpClientRequest直接設置header,不同的是通過HttpClient設置的對整個httpClient都生效,而通過HttpClientRequest設置的只對當前請求生效。
#### HTTP請求認證
Http協議的認證(Authentication)機制可以用于保護非公開資源。如果Http服務器開啟了認證,那么用戶在發起請求時就需要攜帶用戶憑據,如果你在瀏覽器中訪問了啟用Basic認證的資源時,瀏覽就會彈出一個登錄框,如:

我們先看看Basic認證的基本過程:
1. 客戶端發送http請求給服務器,服務器驗證該用戶是否已經登錄驗證過了,如果沒有的話, 服務器會返回一個401 Unauthozied給客戶端,并且在響應header中添加一個 “WWW-Authenticate” 字段,例如:
```
WWW-Authenticate: Basic realm="admin"
```
其中"Basic"為認證方式,realm為用戶角色的分組,可以在后臺添加分組。
2. 客戶端得到響應碼后,將用戶名和密碼進行base64編碼(格式為用戶名:密碼),設置請求頭Authorization,繼續訪問 :
```
Authorization: Basic YXXFISDJFISJFGIJIJG
```
服務器驗證用戶憑據,如果通過就返回資源內容。
注意,Http的方式除了Basic認證之外還有:Digest認證、Client認證、Form Based認證等,目前Flutter的HttpClient只支持Basic和Digest兩種認證方式,這兩種認證方式最大的區別是發送用戶憑據時,對于用戶憑據的內容,前者只是簡單的通過Base64編碼(可逆),而后者會進行哈希運算,相對來說安全一點點,但是為了安全起見,**無論是采用Basic認證還是Digest認證,都應該在Https協議下**,這樣可以防止抓包和中間人攻擊。
HttpClient關于Http認證的方法和屬性:
1. `addCredentials(Uri url, String realm, HttpClientCredentials credentials)`
該方法用于添加用戶憑據,如:
```
httpClient.addCredentials(_uri,
"admin",
new HttpClientBasicCredentials("username","password"), //Basic認證憑據
);
```
如果是Digest認證,可以創建Digest認證憑據:
```
HttpClientDigestCredentials("username","password")
```
2. `authenticate(Future<bool> f(Uri url, String scheme, String realm))`
這是一個setter,類型是一個回調,當服務器需要用戶憑據且該用戶憑據未被添加時,httpClient會調用此回調,在這個回調當中,一般會調用`addCredential()`來動態添加用戶憑證,例如:
```
httpClient.authenticate=(Uri url, String scheme, String realm) async{
if(url.host=="xx.com" && realm=="admin"){
httpClient.addCredentials(url,
"admin",
new HttpClientBasicCredentials("username","pwd"),
);
return true;
}
return false;
};
```
一個建議是,如果所有請求都需要認證,那么應該在HttpClient初始化時就調用`addCredentials()`來添加全局憑證,而不是去動態添加。
#### 代理
可以通過`findProxy`來設置代理策略,例如,我們要將所有請求通過代理服務器(192.168.1.2:8888)發送出去:
```
client.findProxy = (uri) {
// 如果需要過濾uri,可以手動判斷
return "PROXY 192.168.1.2:8888";
};
```
`findProxy` 回調返回值是一個遵循瀏覽器PAC腳本格式的字符串,詳情可以查看API文檔,如果不需要代理,返回"DIRECT"即可。
在APP開發中,很多時候我們需要抓包來調試,而抓包軟件(如charles)就是一個代理,這時我們就可以將請求發送到我們的抓包軟件,我們就可以在抓包軟件中看到請求的數據了。
有時代理服務器也啟用了身份驗證,這和http協議的認證是相似的,HttpClient提供了對應的Proxy認證方法和屬性:
```
set authenticateProxy(
Future<bool> f(String host, int port, String scheme, String realm));
void addProxyCredentials(
String host, int port, String realm, HttpClientCredentials credentials);
```
他們的使用方法和上面“HTTP請求認證”一節中介紹的`addCredentials`和`authenticate` 相同,故不再贅述。
#### 證書校驗
Https中為了防止通過偽造證書而發起的中間人攻擊,客戶端應該對自簽名或非CA頒發的證書進行校驗。HttpClient對證書校驗的邏輯如下:
1. 如果請求的Https證書是可信CA頒發的,并且訪問host包含在證書的domain列表中(或者符合通配規則)并且證書未過期,則驗證通過。
2. 如果第一步驗證失敗,但在創建HttpClient時,已經通過SecurityContext將證書添加到證書信任鏈中,那么當服務器返回的證書在信任鏈中的話,則驗證通過。
3. 如果1、2驗證都失敗了,如果用戶提供了`badCertificateCallback`回調,則會調用它,如果回調返回`true`,則允許繼續鏈接,如果返回`false`,則終止鏈接。
綜上所述,我們的證書校驗其實就是提供一個`badCertificateCallback`回調,下面通過一個示例來說明。
##### 示例
假設我們的后臺服務使用的是自簽名證書,證書格式是PEM格式,我們將證書的內容保存在本地字符串中,那么我們的校驗邏輯如下:
```
String PEM="XXXXX";//可以從文件讀取
...
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
if(cert.pem==PEM){
return true; //證書一致,則允許發送數據
}
return false;
};
```
`X509Certificate`是證書的標準格式,包含了證書除私鑰外所有信息,讀者可以自行查閱文檔。另外,上面的示例沒有校驗host,是因為只要服務器返回的證書內容和本地的保存一致就已經能證明是我們的服務器了(而不是中間人),host驗證通常是為了防止證書和域名不匹配。
對于自簽名的證書,我們也可以將其添加到本地證書信任鏈中,這樣證書驗證時就會自動通過,而不會再走到`badCertificateCallback`回調中:
```
SecurityContext sc=new SecurityContext();
//file為證書路徑
sc.setTrustedCertificates(file);
//創建一個HttpClient
HttpClient httpClient = new HttpClient(context: sc);
```
注意,通過`setTrustedCertificates()`設置的證書格式必須為PEM或PKCS12,如果證書格式為PKCS12,則需將證書密碼傳入,這樣則會在代碼中暴露證書密碼,所以客戶端證書校驗不建議使用PKCS12格式的證書。
#### 總結
值得注意的是,HttpClient提供的這些屬性和方法最終都會作用在請求header里,我們完全可以通過手動去設置header來實現,之所以提供這些方法,只是為了方便開發者而已。另外,Http協議是一個非常重要的、使用最多的網絡協議,每一個開發者都應該對http協議非常熟悉。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示