前言
一般來說,一個用戶端APP并非獨立存在的,很多時候需要與伺服器互動。大體可分為兩方面的資料,正常字元串資料和檔案資料,因為這兩種資料很可能傳輸方式不一樣,比如字元串之類的資料,使用HTTP協定,選擇json或xml作為資料傳輸結構,而以json最友善簡潔,是以我們近年來的項目,就直接使用json,不再使用xml了。但是作為檔案,使用HTTP協定顯然不夠利索,而直接使用TCP協定是更好的選擇。檔案傳輸一般都是在服務端有服務一直在監聽相應的端口,客戶隻需要使用TCP協定,根據服務端制定的規則上傳檔案即可,今天不做過多介紹。這裡主要介紹基于HTTP協定的API。
服務端的Api
Api項目結構
在具體講述細節之前,先看看我們目前正在使用的Api項目結構,所有對外釋出的接口實際上都是通過每個Controller來實作的。
Api文檔
由于Api是對外釋出的,一旦釋出并有用戶端在使用時,穩定性就變得非常重要。是以一個良好的Api至少要滿足穩定性這個基本要求,是以Api的約定文檔變得非常重要,這是以後維護的基礎。這是我們的文檔結構
我們對外釋出的Api的域名是 http://api.kankanbaobei.com 如果你直接通路,肯定是錯誤的,因為沒有給出任何有效的接口名稱。如果你體驗過我們的手機APP,裡面有很多圖檔清單,這個圖檔清單的接口名稱是:/file/list 那麼擷取圖檔清單的基本Url是:http://api.kankanbaobei.com/file/list 如果你通路這個,不會出現找不到的錯誤了,但是會出現以下錯誤:
{"Success":false,"Code":11,"Description":"請求的Token錯誤"}
這個時候Api的安全驗證機制起作用了,那怎麼才能擷取的正确的資料呢?為此我們還是先看看Api安全驗證機制是怎麼設計的吧。先看下面這張圖:
token是對用戶端傳入字元串的驗證,具體驗證方式看上去比較複雜,實際上了解了就不複雜,說明如下:
具體算法如下:(兄弟們,我是不是比較夠誠意呢)
不出意外,你通路上圖中的網址,即可看到結果,由于url太長,我做個連結:
點選這裡檢視結果
傳回的資料結構如下,也就是你在手機APP上看到的圖檔清單,代碼太長,我保留了兩張圖檔的代碼量。
{
"Success": true,
"Code": 200,
"Description": "Ok",
"FileList": [
{
"ChildrenList": [],
"ClassList": [],
"CreateTime": "2014-07-07 16:11:49",
"Description": "",
"Id": 15228,
"Tidied": false,
"Type": 3,
"Url": "http://baobei.oss.aliyuncs.com/uploadfile/other/9ac/9acc2e13e4ac8b98a7cd49a9902ea0a7_861.mp4",
"UserId": 861,
"RecordingDate": "2014-07-07 16:11:49",
"FileSize": 1132580,
"Thumbnail": "http://baobei.oss.aliyuncs.com/uploadfile/other/9ac/9acc2e13e4ac8b98a7cd49a9902ea0a7_861_480_960.jpg",
"State": 1
},
{
"ChildrenList": [
{
"Id": 925,
"RealName": "王軍"
}
],
"ClassList": [],
"CreateTime": "2014-05-02 22:35:13",
"Description": "我們正在做早操",
"Id": 7702,
"Tidied": false,
"Type": 3,
"Url": "http://baobei.oss.aliyuncs.com/uploadfile/initdata/video_2.mp4",
"UserId": 861,
"RecordingDate": "2014-05-02 22:35:13",
"FileSize": 7196151,
"Thumbnail": "http://baobei.oss.aliyuncs.com/uploadfile/initdata/video_2_480_960.jpg",
"State": 1
}
]
}
當正式使用者使用的話,上面的url是隻能夠使用一次的,如果多次使用,會出現以下錯誤的:
{"Success":false,"Code":13,"Description":"請求的序列号錯誤"}
不知道你注意到上面系統參數裡面有這個callid參數沒?這是個時間戳,主要防重播攻擊。系統會要求每次請求的CallId必須大于上一次的CallId。
另外還有一個很重要的參數version,這個參數表示api的版本,api不可能不變,但變動不應該影響用戶端已經在使用的api,是以用version來表示不同的api版本,保證以往釋出的api版本的穩定,要回顧這些系統級的參數,請參考上面系統級參數那張圖。
Api設計總結
經過了以上的折磨後,我想我應該把Api設計基本上說清楚,Api設計總結如下:
1,定義全局規則,比如采用的字元編碼,統一傳回的資料格式等
2,定義系統級參數,每次通路都需要帶上的參數。比如apikey,version,callid,token等
3,說明token簽名規則
4,定義每個接口具體的參數
總體說來,每個url由這4部分組成
1,Api域名,如我們的 http://api.kankanbaobei.com
2,接口名稱,比如我們擷取老師檔案清單的接口名稱:/file/list
3,接口參數,包括系統級參數和接口參數
4,計算出來的token
如果接口是post方式,比如修改密碼,那麼 送出的url是前面兩部分,後面的參數需要post送出。
Api代碼實作
通過api傳回的資料結構是相對固定的,我們使用的NewtonSoft.Json序列化實體結構,我們的結構大體如下(具體屬性有所删除,但不影響閱讀):
namespace BaoBei.Api.Services
{
public class Result
{
/// <summary>
/// 執行是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 執行結果代号
/// </summary>
public int Code { get; set; }
/// <summary>
/// 執行結果描述資訊
/// </summary>
public string Description { get; set; }
/// <summary>
/// 公共資料,一般用于除特定類型以外的資料
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Data { get; set; }
/// <summary>
/// 使用者資訊
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Api_UserInfo UserInfo { get; set; }
/// <summary>
/// 檔案結果集,目前隻能以集合的方式直接指派
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public List<Api_File> FileList { get; set; }
}
}
NewtonSoft.Json下面的這個特性非常友善,在傳回資料結構中,不是所有的屬性都傳回,而是根據實際情況,傳回接口所需要的結構,比如不需要UserInfo屬性,則不為其指派即可,傳回的資料結構中就沒有這個屬性。這樣設計上也比較友善,而接口傳回的資料也比較整齊。
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
資料結構難免會嵌套,比如上面的 Api_File (為什麼這個類名叫 Api_File,其實沒别的原因,主要是和系統共享項目(BaoBei.Core)中的File實體區分)
比如這個類中有以下兩個屬性 ChildrenList 和 ClassList
[Serializable]
public class Api_File
{
public List<File_Children> ChildrenList { get; private set; }
public List<File_Classes> ClassList { get; private set; }
#region 構造函數
/// <summary>
/// 預設構造函數
/// </summary>
public Api_File(){
this.ChildrenList = new List<File_Children>(10);
this.ClassList = new List<File_Classes>(5);
}
#region CreateTime:建立時間
/// <summary>
/// 建立時間
/// </summary>
public DateTime? CreateTime{get;set;}
#endregion
#region Description:對檔案的描述
/// <summary>
/// 對檔案的描述
/// </summary>
public String Description{get;set;}
#endregion
#region Id:檔案Id
/// <summary>
/// 檔案Id
/// </summary>
public Int32 Id{get;set;}
#endregion
}
基本資料結構弄清楚了後,看看一些安全驗證的代碼,實際上安全驗證的代碼就是根據Api文檔來寫的。
#region 安全驗證
/// <summary>
/// 安全驗證
/// </summary>
/// <param name="apiKey">apiKey,目前就是使用者Id</param>
/// <param name="maxRestrictTimes">每分鐘最大請求次數</param>
/// <param name="currentCallId">目前請求序列号</param>
/// <param name="secret">使用者的密鑰</param>
/// <param name="collection">請求參數集合</param>
/// <returns></returns>
public static Result Verify(int apiKey, int maxRestrictTimes, long currentCallId, string secret, NameValueCollection collection)
{
if (!VerifyToken(collection, secret))
return ApiUtils.GetResult(false, CodeConstants.TokenInvalid, "請求的Token錯誤");
if (!VerifyCallIdIsOk(apiKey, currentCallId))
return ApiUtils.GetResult(false, CodeConstants.CallIdInvalid, "請求的序列号錯誤");
if (!VerifyOutOfRestrictTimes(apiKey, maxRestrictTimes))
return ApiUtils.GetResult(false, CodeConstants.OutOfRequestTimes, "在一分鐘内已經達到最大請求次數");
return ApiUtils.GetResult(true, CodeConstants.Success, "Ok");
}
#endregion
傳回的Result就是那個資料結構,這個時候傳回的是公共部分,就是無論哪個接口傳回的資料,都會包含這個公共部分,就是Success,Code,Description,具體可參看前面那個資料傳回結構代碼,裡面也有說明。具體每個接口傳回的資料,還是以擷取檔案接口為例(不好意思,讓你失望了),我剛才看了看擷取檔案清單的代碼非常長,我這裡以修改檔案描述為例,完整代碼如下:
/// <summary>
/// 修改檔案描述
/// </summary>
/// <returns></returns>
[HttpPost]
public ContentResult Description()
{
base.IsPost = true;//目前請求的是否是Post方式
if (base.Version.CompareTo("1.0") >= 0)//判斷Api版本
{
NameValueCollection collection = Request.Form;
Result result = ApiUtils.Verify(base.UserId, UserInfoProvider.Instance.GetMaxRestrictRequestTimes(base.UserId),
base.CurrentCallId, UserInfoProvider.Instance.GetUserSecret(base.UserId), collection);
if (!result.Success)
return Content(ApiUtils.Serialize(result));
int fileId = EagleRequest.FormToInt32("fileId", 0);
string description = EagleRequest.FormToString("description", string.Empty);
try
{
int state = FileManager.UpdateFileDescription(description, fileId, base.UserId);
if (state > 0)
{
result.Description = "Ok";
return Content(ApiUtils.Serialize(result));
}
return Content(ApiUtils.GetResultJson(false, CodeConstants.ExecuteFailed, "操作失敗,無權限或者不存在該檔案"));
}
catch (Exception e)
{
Logger.Error(e);
return Content(ApiUtils.GetResultJson(false, CodeConstants.Exception, "錯誤:" + e.Message));
}
}
else
{
return Content(ApiUtils.GetResultJson(false, CodeConstants.ApiVersionInvalid, "Api版本号錯誤"));
}
}
其實所有的接口都會有前面幾句驗證的代碼,以上為Api代碼的實作,基本流程是這樣的,不知道是否對你那麼一些用處?
用戶端使用Api
首先還是需要擷取到資料,是以需要有個請求資料的公共方法,這些公共方法都在PCL類庫中,以便共享到其他項目中:
同樣,我們還是使用的是與服務端相同的資料結構,拷貝過來就可以,仍然使用NewtonSoft.Json反序列化,非常友善。以擷取檔案清單為例,核心代碼如下:
private void GetFileList(int count, int fileId, bool nextPage, int specifiedTeacherId, DateTime? startCreateTime, DateTime? endCreateTime, bool? tidied, OnFinishRequestApiResultCallback callback)
{
Dictionary<string, string> keyValues = ApiSettings.ApiSystemKeyValues;
keyValues.Add("count", count.ToString());
keyValues.Add("fileid", fileId.ToString());
keyValues.Add("nextpage", nextPage.ToString());
keyValues.Add("specifiedTeacherId", specifiedTeacherId.ToString());
if (tidied.HasValue)
{
keyValues.Add("tidied", tidied.Value.ToString());
}
if (startCreateTime.HasValue)
{
keyValues.Add("startcreatetime", startCreateTime.Value.ToString());
}
if (endCreateTime.HasValue)
{
keyValues.Add("endcreatetime", endCreateTime.Value.ToString());
}
HttpClient httpClient = new HttpClient();
httpClient.Get(Url.Create(ListActionName, keyValues), callback);
}
其中很關鍵的Url.Create方法的代碼如下:
public static string Create(string apiMethodName, Dictionary<string, string> keyValues)
{
keyValues = keyValues.OrderBy(o => o.Key).ToDictionary(key => key.Key, value => value.Value);//進行字段排序
StringBuilder code = new StringBuilder(keyValues.Count * 20);
StringBuilder newQuery = new StringBuilder(keyValues.Count * 20);
foreach (string key in keyValues.Keys)
{
code.Append(key + "=" + keyValues[key]);
newQuery.Append(key + "=" + Uri.EscapeDataString(keyValues[key]) + "&");
}
return string.Format("{0}{1}/?{2}", ApiSettings.Domain, apiMethodName, newQuery.ToString() +
"token=" + Sha1.Create(code.ToString() + ApiSettings.ApiSecret));
}
至此,這以後就是表現層調用這些資料了,這一節與Xamarin.Android關系甚少,但是确實必須的,不然往後可能不清楚整個流程是如何設計的,不利于了解,我個人認為是這樣。
今天先寫到這裡,算是對Api有一個大概流程的介紹(不知道你看是否覺得清晰,O(∩_∩)O),希望對你有那麼一點點用處。
謝謝。
歡迎轉載,但保留版權,謝謝!