天天看點

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

前言

一般來說,一個用戶端APP并非獨立存在的,很多時候需要與伺服器互動。大體可分為兩方面的資料,正常字元串資料和檔案資料,因為這兩種資料很可能傳輸方式不一樣,比如字元串之類的資料,使用HTTP協定,選擇json或xml作為資料傳輸結構,而以json最友善簡潔,是以我們近年來的項目,就直接使用json,不再使用xml了。但是作為檔案,使用HTTP協定顯然不夠利索,而直接使用TCP協定是更好的選擇。檔案傳輸一般都是在服務端有服務一直在監聽相應的端口,客戶隻需要使用TCP協定,根據服務端制定的規則上傳檔案即可,今天不做過多介紹。這裡主要介紹基于HTTP協定的API。

服務端的Api

Api項目結構

在具體講述細節之前,先看看我們目前正在使用的Api項目結構,所有對外釋出的接口實際上都是通過每個Controller來實作的。

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

Api文檔

由于Api是對外釋出的,一旦釋出并有用戶端在使用時,穩定性就變得非常重要。是以一個良好的Api至少要滿足穩定性這個基本要求,是以Api的約定文檔變得非常重要,這是以後維護的基礎。這是我們的文檔結構

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

我們對外釋出的Api的域名是 http://api.kankanbaobei.com 如果你直接通路,肯定是錯誤的,因為沒有給出任何有效的接口名稱。如果你體驗過我們的手機APP,裡面有很多圖檔清單,這個圖檔清單的接口名稱是:/file/list 那麼擷取圖檔清單的基本Url是:http://api.kankanbaobei.com/file/list 如果你通路這個,不會出現找不到的錯誤了,但是會出現以下錯誤:

{"Success":false,"Code":11,"Description":"請求的Token錯誤"}      

這個時候Api的安全驗證機制起作用了,那怎麼才能擷取的正确的資料呢?為此我們還是先看看Api安全驗證機制是怎麼設計的吧。先看下面這張圖:

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

token是對用戶端傳入字元串的驗證,具體驗證方式看上去比較複雜,實際上了解了就不複雜,說明如下:

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

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

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

不出意外,你通路上圖中的網址,即可看到結果,由于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類庫中,以便共享到其他項目中:

APP并非一個人在戰鬥,還有API—Xamarin.Android回憶錄

同樣,我們還是使用的是與服務端相同的資料結構,拷貝過來就可以,仍然使用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),希望對你有那麼一點點用處。

謝謝。

歡迎轉載,但保留版權,謝謝!