前言
一般來說,一個客戶端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),希望對你有那么一點點用處。
謝謝。