前言

一般来说,一个客户端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>();
this.ClassList = new List<File_Classes>();
} #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") >= )//判断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", );
string description = EagleRequest.FormToString("description", string.Empty);
try
{
int state = FileManager.UpdateFileDescription(description, fileId, base.UserId);
if (state > )
{
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 * );
StringBuilder newQuery = new StringBuilder(keyValues.Count * );
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),希望对你有那么一点点用处。

谢谢。

APP并非一个人在战斗,还有API—Xamarin.Android回忆录的更多相关文章

  1. 如何使用Google Map API开发Android地图应用

    两年前开发过的GoogleMap已经大变样,最近有项目要用到GoogleMap,重新来配置Android GoogleMap开发环境,还真是踩了不少坑. 一.下载Android SDK Manager ...

  2. [置顶] Xamarin android 调用Web Api(ListView使用远程数据)

    xamarin android如何调用sqlserver 数据库呢(或者其他的),很多新手都会有这个疑问.xamarin android调用远程数据主要有两种方式: 在Android中保存数据或调用数 ...

  3. Xamarin.Android 调用Web Api(通过ListView展示远程获取的数据)

    xamarin.android如何调用sqlserver 数据库呢(或者其他的),很多新手都会有这个疑问.xamarin.android调用远程数据主要有两种方式: 在Android中保存数据或调用数 ...

  4. 【Android API】Android 4.1 API官方文档详解

    原文:http://android.eoe.cn/topic/summary 翻译:[eoeAndroid原创团队]kris.流风而逝.贼寇在何方.snowxwyo.lsy4833406 更新日期:2 ...

  5. Google 地图 API for Android

    原文:Introduction to Google Maps API for Android 作者:Eunice Obugyei 译者:kmyhy 从健康类 app Runkeeper 到游戏 app ...

  6. Android Google 地图 API for Android

    从健康类 app Runkeeper 到游戏 app 精灵宝可梦,位置服务对现代 app 来说越来越重要. 在本文中,我们将创建一个 app,名字就叫做 City Guide.这个 app 允许用户搜 ...

  7. 跨过几个坑,终于完成了我的第一个Xamarin Android App!

    时间过得真快,距离上次发随笔又是一年多.作为上次发的我的第一个WP8.1应用总结的后继,这次同样的主要功能,改为实现安卓版APP.前几个月巨硬收购Xamarin,把Xamarin集成到VS里了,大大方 ...

  8. Xamarin.Android 入门之:Android API版本设置

    一.引言 Xamarin.Android有几个Android API级别设置,确定多个版本的Android应用程序的兼容性.本博客解释了这些设置意味着什么,如何配置它们,以及它们在运行时对您的应用程序 ...

  9. 【Xamarin挖墙脚系列:Xamarin.Android的API设计准则】

    原文:[Xamarin挖墙脚系列:Xamarin.Android的API设计准则] 前言 楼主也是看着Xamarin的官方文档来的.基本也是照猫画虎.英语勉强凑合.翻译的不对的地方,大家多多指教.(这 ...

随机推荐

  1. JS调用Android、Ios原生控件

    在上一篇博客中已经和大家聊了,关于JS与Android.Ios原生控件之间相互通信的详细代码实现,今天我们一起聊一下JS调用Android.Ios通信的相同点和不同点,以便帮助我们在进行混合式开发时, ...

  2. MVVM模式和在WPF中的实现(二)数据绑定

    MVVM模式解析和在WPF中的实现(二) 数据绑定 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在WPF中 ...

  3. DynamicObject - 代理对象的种类

    开箱即用,DynamicProxy提供了多种代理对象,主要分成两个大类: 基于继承(Inheritance-based) 基于继承的代理是通过继承一个代理类来实现,代理拦截对类的虚(virtual)成 ...

  4. FREERTOS 手册阅读笔记

    郑重声明,版权所有! 转载需说明. FREERTOS堆栈大小的单位是word,不是byte. 根据处理器架构优化系统的任务优先级不能超过32,If the architecture optimized ...

  5. JavaScript权威指南 - 对象

    JavaScript对象可以看作是属性的无序集合,每个属性就是一个键值对,可增可删. JavaScript中的所有事物都是对象:字符串.数字.数组.日期,等等. JavaScript对象除了可以保持自 ...

  6. EF6 对多个数据库,多个DBContext的情况 进行迁移的方法。

    参见: http://stackoverflow.com/questions/21537558/multiple-db-contexts-in-the-same-db-and-application- ...

  7. 【JS基础】正则表达式

    正则表达式的() [] {}有不同的意思. () 是为了提取匹配的字符串.表达式中有几个()就有几个相应的匹配字符串. (\s*)表示连续空格的字符串. []是定义匹配的字符范围.比如 [a-zA-Z ...

  8. 屌丝giser成长记-大学篇

    作为一名屌丝giser的我,刚接触gis专业是2007年的大一,好悲催,当时gis这个专业是被调剂的,我压根都不知道gis为何物,那时候gis冷门的一逼,报名这个专业的寥寥无几.记得那时候得知被调剂到 ...

  9. BPM SharePoint解决方案分享

    一.需求分析 SharePoint作为微软推出的协同类平台产品,为客户提供了门户.内容.文档.流程.社区.搜索.BI等一系列的解决方案,然而其流程功能由于设计理念差异,不能完全满足客户的需求,主要原因 ...

  10. django 第三天 有关库使用

    项目中经常会用到第三方的lib和app,有些lib和app会进行不断更新,更新后可能会存在冲突,因此可以创建externals目录,下面欧app和libs.app存放django-cms,haysta ...