实验四 网络通信
一、实验目的
1.学习移动互联网云服务的 HTTP 协议、OAuth2 协议和 Web API。
2.了解 HTTPS、SSL、OAuth 等相关术语以及和网络安全相关的原理和背景。
3.学习并掌握使用 Web API 完成网络云服务的数据接入。
4.通过百度云盘开放 API 的接入,在移动端实现网络云盘资源的管理。
二、 知识要点
1.HTTP 协议
HTTP 协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。HTTP/1.1 是目前应用最为广泛的 HTTP 版本,2015 年 5月正式发布的互联网标准 HTTP/2,截止 2019 年全球有近 40%的网站支持了HTTP/2。
2.OAuth2 协议
Oauth(Open Authorization)协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth 是安全的。OAuth 2.0 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。OAuth 2.0 关注客户端开发者的简易性,同时为 Web 应用、桌面应用、手机和智能设备提供专门的认证流程。
3.Web API
Web API 是网络应用程序接口。包含了广泛的功能,网络应用通过 API 接口,可以实现存储服务、消息服务、计算服务等能力,利用这些能力可以进行开发出强大功能的网络应用。当前比较流行的 Web API:REST、RPC、GraphQL、WebSocket、WebHook、HTTP Streaming。
a) REST(Representational State Transfer)表现层状态转换,根基于超文本传输协议(HTTP)之上而确定的一组约束和属性,是一种设计提供万维网络服务的软件构建风格。符合或兼容于这种架构风格(简称为 REST 或 RESTful)的网络服务,允许客户端发出以统一资源标识符访问和操作网络资源的请求,而与预先定义好的无状态操作集一致化。因此表现层状态转换提供了在互联网络的计算系统之间,彼此资源可交互使用的协作性质(interoperability)。
b) RPC(Remote Procedure Call)远程过程调用,该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC 是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
c) GraphQL 是一种为 API 接口和查询已有数据运行时环境的查询语言. 它提供了一套完整的和易于理解的 API 接口数据描述, 给客户端权力去精准查询他们需要的数据, 而不用再去实现其他更多的代码, 使 API 接口开发变得更简单高效, 支持强大的开发者工具。
d) WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,WebSocket API也被 W3C 定为标准。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
e) WebHook 是一种 web 回调或者 http 的 push API,是向 APP 或者其他应用提供实时信息的一种方式。Webhook 在数据产生时立即发送数据,也就是你能实时收到数据。这一种不同于典型的 API,需要用了实时性需要足够快的轮询。这无论是对生产还是对消费者都是高效的,唯一的缺点是初始建立困难。Webhook有时也被称为反向 API,因为他提供了 API 规则,你需要设计要使用的 API。Webhook 将向你的应用发起 http 请求,典型的是 post 请求,应用程序由请求驱动。
f) HTTP Streaming 是一种数据推送式的网络传输技术,它允许服务器无限期保持使用单个 HTTP 连接请求连续发送数据,技术上来讲这违反反了 HTTP 约定,但它提供了一种有效的在服务器和客户端之间传输各种动态数据或者其它流式数据,而无需反复发起请求。目前主流 API 服务提供上均提供此类型网络数据服务,访问以下网址了解企业应用详情:Twitter、DataSift、Superfeedr、Gitter。
g) HTTP Long-polling HTTP 长轮询,是一种通过长轮询方式实现"服务器推"的技术,它弥补了 HTTP 简单的请求应答模式的不足,极大地增强了程序的实时性和交互性。HTTP 长轮询一般应用与 WebIM、ChatRoom 和一些需要及时交互的网站应用中。访问一下网址了解企业应用详情:Dropbox、Amazon 、SQS、Livefyre、Consul。
三、 实验内容
1.课堂实验
a) 文件页面使用百度网盘文件列表API获取根目录的所有文件或者文件夹信息;并支持多级目录的前进和返回。
b) 文件页面实现“加载中”和“加载错误”等页面状态的效果,提升整个 APP 的人性化体验。
c) 我的页面“头像”、“昵称”、“VIP 类型” 接入百度网盘用户信息 API,美化效果后,动态更新展示。
d) 我的页面“网盘容量指示条”和“网盘容量信息”接入百度网盘容量信息 API,美化效果后,动态更新展示。
2.课后练习
a) 参考“百度账户接入指南”,使用 WebView 组件完成OAuth2 的接入,获取“token”和“expireIn”并存储;启动APP 时检查是否过期以及二次授权登录。
b) 搜索页面接入百度网盘“文件搜索”API,并根据当前目录递归搜索子目录满足关键词的文件或者文件夹,并支持多级目录的前进和返回。
c) 实现网盘用户信息和容量信息数据的离线缓存,无网络时加载本地缓存数据,否则实时请求网络刷新。
四、实验结果
1)、课堂实验
1、代码
///描述百度网盘的文件信息
class BdDiskFile extends DiskFile{
/// 文件在服务器修改的时间
int serverMtime;
int unlist;
/// 文件在云端的唯一标识ID
int fsId;
int operId;
/// 文件在服务器创建的时间
int serverCtime;
/// 文件在客户端修改的时间
int localMtime;
/// 包含三个尺寸的缩略图URL
Thumbs thumbs;
int share;
/// 文件的md5值,只有是文件类型时
String md5;
/// 文件在客户端创建时间
int localCtime;
BdDiskFile({this.serverMtime, this.unlist, this.fsId, this.serverCtime,
this.localMtime, this.thumbs, this.share, this.md5, this.localCtime});
BdDiskFile.fromJson(Map<String ,dynamic> json):super.fromJson(json){
isDir = json['isdir'];
serverFilename = json['server_filename'];
serverMtime = json['server_mtime'];
unlist = json['unlist'];
fsId = json['fs_id'];
operId = json['oper_id'];
serverCtime = json['server_ctime'];
localMtime = json['local_mtime'];
thumbs = json['thumbs'] != null ? new Thumbs.fromJson(json['thumbs']) : null;
share = json['share'];
md5 = json['md5'];
localCtime = json['local_ctime'];
}
Map<String ,dynamic> toJson(){
final Map<String,dynamic> data = super.toJson();
data['isdir'] = this.isDir;
data['server_filename'] = this.serverFilename;
data['server_mtime'] = this.serverMtime;
data['unlist'] = this.unlist;
data['fs_id'] = this.fsId;
data['oper_id'] = this.operId;
data['server_ctime'] = this.serverCtime;
data['local_mtime'] = this.localMtime;
if(this.thumbs != null)
data['thumbs'] = this.thumbs.toJson();
data['share'] = this.share;
data['md5'] = this.md5;
data['local_ctime'] = this.localCtime;
return data;
}
}
百度网盘容量类 用于描述容量信息
/// 描述百度网盘容量信息
class BdDiskQuota{
int errno;
int used;
int total;
int requestId;
BdDiskQuota(this.errno, this.used, this.total, this.requestId);
BdDiskQuota.fromJson(Map<String ,dynamic> json){
errno = json['errno'];
used = json['used'];
total = json['total'];
requestId = json['requestId'];
}
Map<String,dynamic> toJson(){
final Map<String,dynamic> data = new Map<String,dynamic>();
data['errno'] = this.errno;
data['used'] = this.used;
data['total'] = this.total;
data['requestId'] = this.requestId;
return data;
}
}
百度网盘用户类 用于描述用户信息
/// 描述百度网盘用户信息
class BdDiskUser{
String avatarUrl;
String baiduName;
String errmsg;
int errno;
String netdiskName;
String requestId;
int uk;
int vipType;
BdDiskUser({this.avatarUrl, this.baiduName, this.errmsg, this.errno,
this.netdiskName, this.requestId, this.uk, this.vipType});
BdDiskUser.fromJson(Map<String ,dynamic> json){
avatarUrl = json['avatar_url'];
baiduName = json['baidu_name'];
errmsg = json['errmsg'];
errno = json['errno'];
netdiskName = json['netdisk_name'];
requestId = json['request_id'];
uk = json['uk'];
vipType = json['vip_type'];
}
Map<String,dynamic> toJson(){
final Map<String,dynamic> data = new Map<String,dynamic>();
data['avatar_url'] = this.avatarUrl;
data['baidu_name'] = this.baiduName;
data['errmsg'] = this.errmsg;
data['errno'] = this.errno;
data['netdisk_name'] = this.netdiskName;
data['request_id'] = this.requestId;
data['uk'] = this.uk;
data['vip_type'] = this.vipType;
return data;
}
}
百度网盘客户端类
/// 百度网盘客户端类
class BdDiskApiClient{
HttpClient httpClient;
final protocal = "https";
final host = "pan.baidu.com";
final userInfoPath = '/rest/2.0/xpan/nas';
final diskQuotaPath = '/api/quota';
final diskFilePath = '/rest/2.0/xpan/file';
BdDiskApiClient({this.httpClient}) {
this.httpClient = httpClient ?? HttpClient();
}
//返回上一步获取的 token 信息;这里设
//计为异步,便于后期自动获取 token。
get accessToken async{
var token = await AppConfig.instance.token;
return token;
}
///完成百度网盘用户信息的获取
Future<BdDiskUser> getUserInfo() async{
// step 1: get HttpClientRequest
HttpClientRequest request = await httpClient.getUrl(Uri.https(
host,
userInfoPath,
{'method':'uinfo','access_token':'${await accessToken}'}));
// step 2: get HttpClientResponse
HttpClientResponse response = await request.close();
// step 3: consume HttpClientResponse
var responseBody = await response.transform(Utf8Decoder()).join();
// step 4: decoke json response
var json = jsonDecode(responseBody);
return BdDiskUser.fromJson(json);
}
///完成百度网盘容量信息的获取
Future<BdDiskQuota> getDiskQuota() async{
HttpClientRequest request = await httpClient.getUrl(Uri.https(
host,
diskQuotaPath,
{'access_token':'${await accessToken}'}));
HttpClientResponse response = await request.close();
var responseBody = await response.transform(Utf8Decoder()).join();
var json = jsonDecode(responseBody);
return BdDiskQuota.fromJson(json);
}
///完成百度网盘文件信息的获取
Future<List<BdDiskFile>> getListFile(String dir,
{String order = 'name',
int start = 0,
int limit = 1000,
String desc = '0',
String web = '',
int folder = 0,
int showempty = 1}) async{
HttpClientRequest request = await httpClient.getUrl(Uri.https(
host,
diskFilePath,
{'method':'list',
'access_token':'${await accessToken}',
'dir':'$dir',
'order':'$order',
'start':'$start',
'limit':'$limit',
'desc':desc,
'web':web,
'folder':'$folder',
'showempty':'$showempty'
}));
HttpClientResponse response = await request.close();
var responseBody = await response.transform(Utf8Decoder()).join();
var json = jsonDecode(responseBody);
if(json['errno'] != 0)
throw Exception('Baidu Pan file api response error code ${json['errno']}');
var list = (json['list'] as List<dynamic>);
return list.map((f) => BdDiskFile.fromJson(f)).toList();
}
///完成百度网盘文件信息的搜索获取
Future<List<BdDiskFile>> getSearchFile(String key,
{String dir = '/',
int page = 1,
int num = 1000,
String recursion = '0',
String web = '',}) async{
HttpClientRequest request = await httpClient.getUrl(Uri.https(
host,
diskFilePath,
{'method':'search',
'access_token':'${await accessToken}',
'dir':'$dir',
'page':'$page',
'num':'$num',
'recursion':'$recursion',
'web':web,
'key':key
}));
HttpClientResponse response = await request.close();
var responseBody = await response.transform(Utf8Decoder()).join();
var json = jsonDecode(responseBody);
if(json['errno'] != 0)
throw Exception('Baidu Pan file api response error code ${json['errno']}');
var list = (json['list'] as List<dynamic>);
return list.map((f) => BdDiskFile.fromJson(f)).toList();
}
///完成百度网盘文件下载地址的获取
}
百度网盘数据管理
///描述网盘文件数据的管理
class BdDiskFileStore extends FileStore{
final BdDiskApiClient apiClient;
BdDiskFileStore({BdDiskApiClient apiClient})
: this.apiClient = apiClient ?? BdDiskApiClient();
@override
Future<List<DiskFile>> list(String dir,
{String order = 'name', int start = 0, int limit = 1000}) {
return apiClient.getListFile(dir,order: order,start: start,limit: limit);
}
@override
Future<List<DiskFile>> search(String key,
{String dir = "/", int recursion = 1, int page = 1, int num = 1000}) {
return apiClient.getSearchFile(key,dir:dir,recursion:recursion.toString(),page:page,num:num);
}
}
用户中心页面获取用户数据和网盘容量数据并渲染到页面上
@override
void initState() {// 初始化用户数据
bdDiskApiClient = BdDiskApiClient();// 初始化
// 获取用户信息
getUserInfo().then((user){
setState(() {
_avatar_url = user.avatarUrl;
_username = user.baiduName;
_vip_type = user.vipType;
});
});
// 获取容量信息
getDiskQuota().then((quota){
setState(() {
_used = quota.used;
_total = quota.total;
_quota_des = Utils.getFileSize(_used)+"/"+Utils.getFileSize(_total);
});
});
}
Future getUserInfo() async{
BdDiskUser user = await bdDiskApiClient.getUserInfo();
return user;
}
Future getDiskQuota() async{
BdDiskQuota quota = await bdDiskApiClient.getDiskQuota();
return quota;
}
2、结果截图
文件页面使用百度网盘文件列表API获取根目录的所有文件或者文件夹信息;并支持多级目录的前进和返回。
文件页面实现“加载中”和“加载错误”等页面状态的效果
我的页面“头像”、“昵称”、“VIP 类型” 接入百度网盘用户信息 API,美化效果后,动态更新展示。
我的页面“网盘容量指示条”和“网盘容量信息”接入百度网盘容量信息 API,美化效果后,动态更新展示。
2)、课后练习
1、代码
离线缓存用户信息
import 'package:flutter_app02/config/constant.dart';
import 'package:flutter_app02/entity/bd_disk_api_client.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'bd_disk_user.dart';
class UserRepository{
final BdDiskApiClient apiClient;
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
UserRepository({BdDiskApiClient apiClient})
: this.apiClient = apiClient ?? BdDiskApiClient();
/// 获取用户信息, 网络异常时, 返回缓存信息
Future<BdDiskUser> getUserInfo() async{
BdDiskUser user;
var prefs = await _prefs;
try{
user = await apiClient.getUserInfo();
}catch(e){
if(prefs.containsKey(Constant.keyUserInfo))
return BdDiskUser.fromJson(prefs.getJson(Constant.keyUserInfo));
return null;
}
prefs.setJson(Constant.keyUserInfo,user.toJson());
return user;
}
}
存储获取的token信息
class BdOAuth2Token{
/// 登录授权
String accessToken;
/// 鉴权过期时间 单位秒
int expiresIn;
/// 鉴权创建时间 单位秒
int createTime;
bool get isExpired =>
(createTime + expiresIn) <= DateTime.now().millisecondsSinceEpoch ~/ 1000;
BdOAuth2Token(this.accessToken,{this.expiresIn,this.createTime}){
this.createTime ??= DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
BdOAuth2Token.fromJson(Map<String,dynamic> json){
this.accessToken = json['access_token'];
this.expiresIn = json['expires_in'];
this.createTime = json['create_time'];
}
Map<String ,dynamic> toJson(){
final Map<String ,dynamic> json = new Map<String,dynamic>();
json['access_token'] = this.accessToken;
json['expires_in'] = this.expiresIn;
json['create_time'] = this.createTime;
return json;
}
}
首页检查token
_checkOAuth2Result(BuildContext context,String url) async{
url = url.replaceFirst("#", "?");
Uri uri = Uri.parse(url);
if(uri == null) return ;
if(uri.pathSegments.contains("login_success") &&
uri.queryParameters.containsKey('access_token')){
var prefs = await _prefs;
var token = BdOAuth2Token(uri.queryParameters['access_token'],
expiresIn: int.parse(uri.queryParameters['expires_in']));
prefs.setJson(Constant.keyBdOAuthToken,token.toJson());
Navigator.push(context,
MaterialPageRoute(builder: (context)=>HomePage()));
}
}
///使用 Dart 最新语言特性
/// “extension-methods”即函数扩展,对 SharedPreferences 扩展支持 json
/// 存储和读取
extension SharedPreferencesExtension on SharedPreferences{
Future<bool> setJson(String key,Map<String,dynamic> json){
assert(json != null);
assert(key != null);
var value = jsonEncode(json);
return this.setString(key,value);
}
Map<String ,dynamic> getJson(String key){
assert(key != null);
var value = this.getString(key);
var json = jsonDecode(value);
return json;
}
}
2、结果截图
参考“百度账户接入指南”,使用 WebView 组件完成OAuth2 的接入,获取“token”和“expireIn”并存储;启动APP 时检查是否过期以及二次授权登录。
搜索页面接入百度网盘“文件搜索”API,并根据当前目录递归搜索子目录满足关键词的文件或者文件夹,并支持多级目录的前进和返回。
搜索a