客户端后端研发的必修课

2018-01-29  本文已影响0人  coder_小刀

前言

本文从APP后端研发同学的角度,带来一些我们的思考和总结。

接口规范

客户端和服务端通常采用Http(s) 协议,JSON(P)数据格式交互。通常定义的JSON格式如下:

{
    "code": 0, //接口状态值
    "randomcode": "1515121404158",//随机状态码
    "md5": null, //用来判断数据是否更新
    "codeMsg": "成功", // 提示语,Toast使用
    "data": { //业务数据
    }
}

我们对JSON数据格式的基本要求如下:

  1. JSON数据格式要保持良好的结构,符合标准的JSON规范
    • key必须为字符串, 使用双引号而不是单引号 , 字段命名尽量做到见名知意,命名规则建议采用驼峰命名
    • 当Value值为null时,也需要返回对应的Key ,制定协议时包含哪些字段,都需要返回
  2. data 值的数据类型为Object,不允许修改。
{
    "data": {"list":[]} //good
    "data": []   // bad
    "data": "123123"  // bad
}
  1. status需要表示状态值时,字段从1+开始,0是一种未赋值的默认状态,1:进行中,2:待支付,3:已完成,4:已取消
  2. 客户端尽量只负责展示逻辑,不处理业务逻辑。
  3. 接口返回字段除了约定的必要字段,不允许返回多余的字段。
    反例:不定义Model,直接把底层的数据库表对应的Bean返回给前端,导致多传输很多不必要的字段。
  4. 不允许拿变量命名json的key值
//good
[{"name":"书籍1","chapters":500},{"name":""书籍2","chapters":180},{...},...]
//bad
[{"书籍1":500},{"书籍2":180},{...},...]
  1. 禁止程序拼接json字符串,防止包含特殊转义字符,导致客户端解析json失败

说明:此处没有统一iOS、Android、Server的JSON解析框架

通用设计

APP的后端系统有些设计是通用的,各APP可以相互借鉴。

  1. 通用参数
    对于一些客户端每次请求都需要的参数信息,我们暂且称之为通用参数,譬如:os、appid、appversion、channel、imei 等。对于这些信息客户端统一放在Header中。

  2. 接口签名
    接口签名的目的是保证服务端收到的请求都来自客户端,一定程度增加接口被抓取的难度。
    我们采用的方法是使用拦截器处理,客户端和服务端可以约定一些通用参数、时间戳等,前后端分别计算md5值,然后对比。
    示例代码如下:

@Override
public ActionResult preExecute(BeatContext beat) {
  HttpServletRequest request = beat.getRequest();

  String signVal = request.getHeader(HEADER_SIGN);
  if (StringUtils.isBlank(signVal)) {
    log.info("hsign is not exist.");
    RespJsonResult jsonResult = RespJsonResult.creatFailEntity(ResponseCodeConsts.AUTH_ERR_HEADER_NOT_EXIST, "guess error lowest");
    return new JsonViewResult(JsonHelper.toJSONString(jsonResult));
  }
  StringBuffer headerBuffer = new StringBuffer(100);
  for (String headerKey : headerKeylist) {
    String headerValue = request.getHeader(headerKey);
    if (headerValue != null && headerValue.trim().length() > 0) {
      headerBuffer.append(headerValue).append("_");
    } else {
      headerBuffer.append("null_");
    }
  }
  String source = headerBuffer.append("你的秘钥").toString();
  String localSign = MD5.encode(source);
  if (!signVal.equals(localSign)) {
    log.info("sign is illegal.source = " + source + ", localSign = " + localSign + ", hsign = " + signVal);
    RespJsonResult jsonResult = RespJsonResult.creatFailEntity(ResponseCodeConsts.AUTH_ERR_HEADER_NOT_PERMITTED, "guess error lower");
    return new JsonViewResult(JsonHelper.toJSONString(jsonResult));
  }
  return null;
}
  1. 配置中心
    动态化是一个APP的标配,小到一个icon的图标,大到一个页面模块的布局。特别是针对客户端这种 C/S 结构 ,各种开关、三方入口、动态链接等都需要从配置中心获取,不写死是我们的基本原则。
    另外,动态配置,从产品的角度,也可以灵活调整运营策略,达到快速试错的目的。
    整体架构见:


    配置中心.png
  1. 跨域问题
    跨域问题是由于javascript语言安全限制中的同源策略造成的,简单来说,同源策略是指一段脚本只能读取来自同一来源的窗口和文档的属性,这里的同一来源指的是主机名、协议和端口号的组合。
    解决跨域问题通常采用JSONP和Access-Control-Allow-Origin。
String callback = RequestUtils.getStringPara(beat, "callback", null);
if (callback != null){
    return new JsonpActionResult(callback + "("+text+")");
} else {
    return new JsonActionResult(text);
}
beat.getResponse().addHeader("Access-Control-Allow-Origin","www.daojia.com");
  1. 节省流量
    对于某些不经常变动的数据,特别是配置数据,譬如:城市列表数据等,我们可以将接口返回的数据缓存在客户端。服务端对比接口返回数据的md5值是否变化,如果无变化,不返回数据,客户端直接使用本地缓存数据。这样可以减少接口网络数据量的传输,节省用户流量。
    整体流程见:


    数据接口缓存.png
  2. 多版本支持
    APP后端的API比前后端分离系统的API生命周期更长,一般我们的客户端在线上至少保留3-4个版本,还不包含我们针对一些特殊渠道定制的安装包。这样就要求我们后端接口在设计更加通用。
    我们通常的做法:
    对于一些修改较小的接口通过app版本号判断做数据兼容
    对于一些修改较大的接口,通过新增接口 api/v56/order/list 的方式解决
    对于业务逻辑部分,通过app版本号获取对应的实现类

  3. 接口管理
    与APP交互的后端web站点通常有多个,对此我们尽量做到统一收口。好处在于统一权限认证、接口签名、参数校验……


    服务端统一收口.png

8.接口文档管理
接口文档管理包括接口的描述,接口入参出参字段说明,通常这一部分文档的形成在需求详设阶段。
这一块我们做的不好,目前也没有找到比较好的方案。

问题排查技巧

问题排查最困难的就是保留现场。

在APP测试过程中,我们怎么去判断出现问题归属前端还是后端?

后端RD:“ iOS没问题,android的有问题,那android的你们去查吧!” 这样对吗?
iOS/ Android RD:“ 我本地打断点,看你们返回的数据有问题呀 ”
QA:“这个bug到底提给后端RD,还是提给客户端RD呢? 容我喝杯茶想想? 抓个包看看吧! Fiddler 、Charles 走起……”

其实:客户端和服务端通信现在唯一依赖的就是服务端返回的JSON数据,保留住现场,就能立马确认问题归属。

怎么保留现场,系统实时记录接口返回值,通过接口返回的 result 来判断,如果result 符合,那问题归属端上,否则 服务端RD 乖乖去排查问题。
日志格式示例如下:

17:47:51,570  INFO jsonresult:45[51] - uid=123456789 , mobile=0 , uri = /api/guest/address/isopencity,result = {"data":{"centerLng":"39.9930060000","isOpenCity":"0","centerCityId":"1","centerCityName":"北京","centerLat":"116.3367130000","centerPoiName":"五道口"},"code":0,"codeMsg":"成功","randomcode":"1516268871570","md5":null}
18:12:25,883  INFO jsonresult:45[48] - uid=-1 , mobile=0, uri = /api/signalbomb/system/last/,result = {"data":{"lastSystem":{"id":953170805129486336,"title":"附近-服务提醒","pushTime":1516088603000,"content":"您与敏焕大号的订单-姐姐的小店,将于01月16日 15:32开始服务,记得准时服务哦~","url":"https://m-dop71.djtest.cn/user/order/detail?role=merchant&orderid=1122089194802467776"},"newCounts":"13"},"code":0,"codeMsg":"成功","randomcode":"1516270345883","md5":null}
上一篇下一篇

猜你喜欢

热点阅读