通常來講,一個客戶端APP並不是獨立存在的,不少時候須要與服務器交互。大致可分爲兩方面的數據,常規字符串數據和文件數據,由於這兩種數據極可能傳輸方式不同,好比字符串之類的數據,使用HTTP協議,選擇json或xml做爲數據傳輸結構,而以json最方便簡潔,因此咱們近年來的項目,就直接使用json,再也不使用xml了。可是做爲文件,使用HTTP協議顯然不夠利索,而直接使用TCP協議是更好的選擇。文件傳輸通常都是在服務端有服務一直在監聽相應的端口,客戶只須要使用TCP協議,根據服務端制定的規則上傳文件便可,今天不作過多介紹。這裏主要介紹基於HTTP協議的API。算法
在具體講述細節以前,先看看咱們目前正在使用的Api項目結構,全部對外發布的接口實際上都是經過每一個Controller來實現的。json
因爲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是對客戶端傳入字符串的驗證,具體驗證方式看上去比較複雜,實際上理解了就不復雜,說明以下:數據結構
具體算法以下:(兄弟們,我是否是比較夠誠意呢)ide
不出意外,你訪問上圖中的網址,便可看到結果,因爲url太長,我作個連接:函數
點擊這裏查看結果post
返回的數據結構以下,也就是你在手機APP上看到的圖片列表,代碼太長,我保留了兩張圖片的代碼量。ui
{ "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設計總結以下:
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返回的數據結構是相對固定的,咱們使用的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代碼的實現,基本流程是這樣的,不知道是否對你那麼一些用處?
首先仍是須要獲取到數據,因此須要有個請求數據的公共方法,這些公共方法都在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),但願對你有那麼一點點用處。
謝謝。