通常来讲,一个客户端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),但愿对你有那么一点点用处。
谢谢。