APP並不是一我的在戰鬥,還有API—Xamarin.Android回憶錄

前言

通常來講,一個客戶端APP並不是獨立存在的,不少時候須要與服務器交互。大致可分爲兩方面的數據,常規字符串數據和文件數據,由於這兩種數據極可能傳輸方式不同,好比字符串之類的數據,使用HTTP協議,選擇json或xml做爲數據傳輸結構,而以json最方便簡潔,因此咱們近年來的項目,就直接使用json,再也不使用xml了。可是做爲文件,使用HTTP協議顯然不夠利索,而直接使用TCP協議是更好的選擇。文件傳輸通常都是在服務端有服務一直在監聽相應的端口,客戶只須要使用TCP協議,根據服務端制定的規則上傳文件便可,今天不作過多介紹。這裏主要介紹基於HTTP協議的API。算法

服務端的Api

Api項目結構

在具體講述細節以前,先看看咱們目前正在使用的Api項目結構,全部對外發布的接口實際上都是經過每一個Controller來實現的。json

QQ圖片20140710135155

Api文檔

因爲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安全驗證機制是怎麼設計的吧。先看下面這張圖:服務器

image

token是對客戶端傳入字符串的驗證,具體驗證方式看上去比較複雜,實際上理解了就不復雜,說明以下:數據結構

image

具體算法以下:(兄弟們,我是否是比較夠誠意呢)ide

image

不出意外,你訪問上圖中的網址,便可看到結果,因爲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設計基本上說清楚,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類庫中,以便共享到其餘項目中:

image

一樣,咱們仍是使用的是與服務端相同的數據結構,拷貝過來就能夠,仍然使用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),但願對你有那麼一點點用處。

謝謝。

相關文章
相關標籤/搜索