Flutter-Dart網路

一杯咖啡七勺糖發表於2021-03-21

一、單執行緒模型下的非同步操作

為什麼強調是單執行緒:Dart是單執行緒模型,單執行緒模型,單執行緒模型!!!

什麼是單執行緒:就是你是一個人在戰鬥

什麼是非同步: 比如你要燒水(耗時操作),並不需要傻傻地等著水開才能去做下一件事(掃地) 只要開火(方法呼叫),然後你就可以去掃地(執行非同步任務下面的方法),水燒開鳴叫(回撥), 去沖水(處理非同步任務結果)。

Dart非同步程式設計的方式:Future和Stream

Future相當於40米大砍刀,Stream相當於一捆40米大砍刀 dart提供了關鍵字async(非同步)和await(延遲執行),相當於普通的便捷的小匕首

//根據名稱讀取檔案
readFile(name) {
//建立檔案物件
var file = File(name);
return file.readAsString();
}

//讀取檔案成功
readOk() async{
var result = await readFile(r"C:\Users\Administrator\Desktop\應龍.txt");
print(result);
}

main() {
readOk();
print("我是第幾?");
}

複製程式碼

dart 非同步處理

Future寫法:

main() {
File(r"C:\Users\Administrator\Desktop\應龍.txt").readAsString().then((result) {
print(result);
});
print("我是第幾?");
}

複製程式碼

二、Dart中的IO操作

移動端的檔案讀取問題 path_provider: ^0.4.1:提供了三個路徑,勉強用用吧

localPath() async {
try {
print('臨時目錄: ' + (await getTemporaryDirectory()).path);
//----/data/user/0/com.toly1994.toly/cache
print('文件目錄: ' + (await getApplicationDocumentsDirectory()).path);
//----/data/user/0/com.toly1994.toly/app_flutter
print('sd卡目錄: ' + (await getExternalStorageDirectory()).path);
//----/storage/emulated/0
} catch (err) {
print(err);
}
}
複製程式碼

動態許可權申請問題 simple_permissions: ^0.1.9:提供了動態許可權申請

readFormSD() async {
try {
var perm =
SimplePermissions.requestPermission(Permission.ReadExternalStorage);
var sdPath = getExternalStorageDirectory();
sdPath.then((file) {
perm.then((v) async {
var res = await readFile(file.path + "/應龍.txt");
print(res);
});
});
} catch (err) {
print(err);
}
}
複製程式碼

三、Dart中的網路請求操作:

1.通過HttpClient發起HTTP請求

Dart IO庫中提供了Http請求的一些類,我們可以直接使用HttpClient來發起請求。使用HttpClient發起請求分為五步:

建立一個HttpClient

 HttpClient httpClient = new HttpClient();

複製程式碼

開啟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); //可以直接新增輸入流

複製程式碼

等待連線伺服器

HttpClientResponse response = await request.close();

複製程式碼

這一步完成後,請求資訊就已經傳送給伺服器了,返回一個HttpClientResponse物件,它包含響應頭(header)和響應流(響應體的Stream),接下來就可以通過讀取響應流來獲取響應內容。

讀取響應內容

String responseBody = await response.transform(utf8.decoder).join();

複製程式碼

我們通過讀取響應流來獲取伺服器返回的資料,在讀取時我們可以設定編碼格式,這裡是utf8。

請求結束,關閉HttpClient

httpClient.close();

複製程式碼

關閉client後,通過該client發起的所有請求都會中止。

示例


import 'dart:convert';

import 'dart:io';

複製程式碼

//MARK:建立網路請求

  void requestData(@required String url) async {

    try {

      //請求

      //建立一個httpclient

      HttpClient _httpClient = HttpClient();

      //開啟http連結

      HttpClientRequest request = await _httpClient.getUrl(Uri.parse(url));

      //使用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();

      //讀取響應內容

      String string = await response.transform(utf8.decoder).join();

      //輸出響應頭

      print(string);

      text = string;

      //關閉client後,通過改client發起的所有請求都會終止

      _httpClient.close();

    } catch (e) {

      print("請求失敗" + e);

    } finally {

      setState(() {

        print("請求結束");

      });

    }

  }

複製程式碼

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認證的基本過程:

客戶端傳送http請求給伺服器,伺服器驗證該使用者是否已經登入驗證過了,如果沒有的話, 伺服器會返回一個401 Unauthozied給客戶端,並且在響應header中新增一個 “WWW-Authenticate” 欄位,例如: WWW-Authenticate: Basic realm="admin"

其中"Basic"為認證方式,realm為使用者角色的分組,可以在後臺新增分組。

客戶端得到響應碼後,將使用者名稱和密碼進行base64編碼(格式為使用者名稱:密碼),設定請求頭Authorization,繼續訪問 : Authorization: Basic YXXFISDJFISJFGIJIJG

伺服器驗證使用者憑據,如果通過就返回資源內容。

注意,Http的方式除了Basic認證之外還有:Digest認證、Client認證、Form Based認證等,目前Flutter的HttpClient只支援Basic和Digest兩種認證方式,這兩種認證方式最大的區別是傳送使用者憑據時,對於使用者憑據的內容,前者只是簡單的通過Base64編碼(可逆),而後者會進行雜湊運算,相對來說安全一點點,但是為了安全起見,無論是採用Basic認證還是Digest認證,都應該在Https協議下,這樣可以防止抓包和中間人攻擊。

HttpClient關於Http認證的方法和屬性:


addCredentials(Uri url, String realm, HttpClientCredentials credentials)
該方法用於新增使用者憑據,如:
httpClient.addCredentials(_uri,

 "admin", 

  new HttpClientBasicCredentials("username","password"), //Basic認證憑據

);

複製程式碼

如果是Digest認證,可以建立Digest認證憑據: HttpClientDigestCredentials("username","password")

authenticate(Future 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對證書校驗的邏輯如下:

如果請求的Https證書是可信CA頒發的,並且訪問host包含在證書的domain列表中(或者符合通配規則)並且證書未過期,則驗證通過。

如果第一步驗證失敗,但在建立HttpClient時,已經通過SecurityContext將證書新增到證書信任鏈中,那麼當伺服器返回的證書在信任鏈中的話,則驗證通過。

如果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協議非常熟悉。

2.網路操作

Dio http庫

通過上一節介紹,我們可以發現直接使用HttpClient發起網路請求是比較麻煩的,很多事情得我們手動處理,如果再涉及到檔案上傳/下載、Cookie管理等就會非常繁瑣。幸運的是,Dart社群有一些第三方http請求庫,用它們來發起http請求將會簡單的多,本節我們介紹一下目前人氣較高的dio庫。

dio是一個強大的Dart Http請求庫,支援Restful API、FormData、攔截器、請求取消、Cookie管理、檔案上傳/下載、超時等。

引入

dio: ^2.1.3

匯入並建立dio例項:

import 'package:dio/dio.dart';

Dio dio = new Dio();

接下來就可以通過 dio例項來發起網路請求了,注意,一個dio例項可以發起多個http請求,一般來說,APP只有一個http資料來源時,dio應該使用單例模式。

示例

發起 GET 請求 :

Response response;

response=await dio.get("/test?id=12&name=wendu")

print(response.data.toString());

對於GET請求我們可以將query引數通過物件來傳遞,上面的程式碼等同於:

response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"})

print(response);

發起一個 POST 請求:

response=await dio.post("/test",data:{"id":12,"name":"wendu"})

發起多個併發請求:

response= await Future.wait([dio.post("/info"),dio.get("/token")]);

下載檔案:

response=await dio.download("https://www.google.com/",_savePath);

傳送 FormData:

FormData formData = new FormData.from({

   "name": "wendux",

   "age": 25,

});

response = await dio.post("/info", data: formData)

複製程式碼

如果傳送的資料是FormData,則dio會將請求header的contentType設為“multipart/form-data”。

通過FormData上傳多個檔案:

FormData formData = new FormData.from({

   "name": "wendux",

   "age": 25,

   "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),

   "file2": new UploadFileInfo(new File("./upload.txt"), "upload2.txt"),

     // 支援檔案陣列上傳

   "files": [

      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),

      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")

    ]

});

response = await dio.post("/info", data: formData)

複製程式碼

值得一提的是,dio內部仍然使用HttpClient發起的請求,所以代理、請求認證、證書校驗等和HttpClient是相同的,我們可以在onHttpClientCreate回撥中設定,例如:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {

    //設定代理 

    client.findProxy = (uri) {

      return "PROXY 192.168.1.2:8888";

    };

    //校驗證書

    httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){

      if(cert.pem==PEM){

      return true; //證書一致,則允許傳送資料

     }

     return false;

    };   

  };

複製程式碼

相關文章