手游海外SDK实战——Android客户端之网络篇
一、前言
随着国内手游版号申请难度的增加,以及防沉迷等一系列政策的影响,很多国内开发者纷纷开始寻求海外发行之路。那么手游出海首要的是需要一套适合海外发行和运营的手游SDK联运系统。
本系列我们就来开发一套这样的SDK,我们暂且称这套SDK为UGSDK。该SDK已经开发完成,如果有兴趣或者想体验完整功能的同学,可以加我们的海外技术交流QQ群:1055996444。
整个UGSDK项目,暂时可以分为三大部分——Android客户端SDK部分、iOS客户端SDK部分以及服务端部分(目前不考虑H5游戏部分)。
本篇主要介绍UGSDK项目中Android客户端部分中的网络和协议部分。
二、基础Http组件
对于熟悉Android开发的同学, 一提到http组件,第一想到的可能就是okhttp3了。 如果是开发一款普通的app,我们当然可以直接使用okhttp3这样优秀的开源http组件了。 但是记住,我们开发的是SDK产品。
SDK产品和普通的app最大的区别在于交付。 普通的app开发完成,我们只需要保证能生成最终的apk并运行正常即可。但是对于手游SDK产品, 我们最终发布的是SDK包,最终会在不同的游戏中嵌入我们的SDK包。你依赖的任何第三方组件,在不同的游戏产品中也可能被依赖。像okhttp3这样优秀且知名的组件就更不用说了。
如果SDK产品中我们依赖了okhttp3的比如3.0版本, 后面我们将SDK提供给某个游戏产品接入的时候, 游戏产品中也依赖了okhttp,比如依赖的是4.0版本。那么当游戏在编译或者运行的时候, 很可能因为两个不同版本的okhttp组件而出现错误。
所以,在开发SDK产品的时候, 我们需要坚持这样的原则:“能自己造的轮子,坚决自己造”,尽可能让让所有的组件独一无二。当然,你也可以对这些开源组件修改包名,从而达到避免冲突的目的。 但是这样不利于后期的版本升级和维护。
其实,Android本身已经提供了HttpURLConnection这样的组件, 我们基于该组件,适当地进行一个封装,就可以完成基础的http请求(GET or POST)。完全没必要引入一个第三方http组件库。
在UGSDK中,我们在之前介绍的ug-common模块中,封装一个简单的httpclient组件:
上面的基础http请求组件中,我们简单介绍下各个类的作用:
1 2 3 4 5 6 |
1、entity: 该包名下面主要是封装了不同类型的请求体。因为我们的协议请求内容类型是application/x-www-form-urlencoded格式,所以我们只需要封装一个UrlEncodedFormEntity即可。 2、methods: 该包名下面实现基本的http请求方法, 最常用的就是GET和POST了。 3、ssl: 该包名下面处理https相关的支持。 4、DefaultHttpClient: 实现IHttpClient接口,提供默认的同步http请求实现。 5、AsyncHttpClient: 实现IHttpClient接口, 提供异步的http请求实现。 6、HttpClient: 对外提供的http请求API类,外部模块调用只需要调用这个类提供的对应api接口即可。 |
上面这是简单介绍了UGSDK中封装的基础http组件,你也可以基于HttpURLConnection自己简单封装,或者直接使用改名后的okhttp等组件。都是一样的目的,只是为了发送http或者https协议。
三、网络协议模块
在UGSDK中, 不同的业务模块中,我们都需要和服务端进行交互。 在设计和服务端的交互协议时,我们尽可能采用统一的协议格式。
1、请求参数设计
- 所有协议的参数均采用application/x-www-form-urlencoded格式发送。
- 所有协议均采用HTTP POST 方法发送。
- 所有字段发送之前需要进行url encode处理。
- 所有协议都有几个固定的协议参数:appID、timestamp以及sign(协议参数签名)。
- 所有协议参数签名规则,使用MD5(key1=val1&key2=val2&…..&secretKey=appKey)。出了sign之外的所有非空参数,按照key顺序排序,然后按key1=val1&key2=val2&…,最后附加&secretKey=appKey生成待签名串。最终使用MD5 32位大写算法生成签名,放到协议字段sign中一起发送给服务端。
2、响应设计
- 所有协议的响应参数格式均为application/json格式。
- 所有协议的响应格式均按照如下格式:
12345{"code": 0,"msg": "success","data": {...}}
- 所有协议的响应格式中,code为0时,表示操作成功;其他状态为失败。只有code为0时,data字段才可能有值。
3、协议模块封装
为了尽可能将协议部分通用的规则进行固定化, 我们封装一个简单的网络协议组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
public class ApiRequest { /** * 服务端异常 */ public static final int S_CODE_EXCEPTION = -1; /** * 成功状态 */ public static final int S_CODE_SUCCESS = 0; /** * 失败状态 */ public static final int S_CODE_FAILURE = 1; /** * 协议成功状态 */ public static final int R_CODE_SUCCESS = 0; /** * 协议失败状态 */ public static final int R_CODE_FAIL = 1; /** * 协议网络失败状态 */ public static final int R_CODE_NETWORK_ERROR = 2; /** * 协议网络响应解析失败状态 */ public static final int R_CODE_RESPONSE_ERROR =3; /** * 协议异常状态 */ public static final int R_CODE_EXCEPTION = 4; /** * 请求url */ private String url; /** * 请求参数 */ private Map<String, String> params; private ApiRequest() {} public static class Builder { private String url; private Map<String, String> params; public Builder(String url) { this.url = url; this.params = new HashMap<>(); } public Builder addParam(String key , String val) { if (TextUtils.isEmpty(key) || TextUtils.isEmpty(val)) { Log.d(Constants.TAG, "ApiRequest ignore a null param.key:"+key+";val:"+val); return this; } this.params.put(key, val); return this; } public ApiRequest build() { ApiRequest req = new ApiRequest(); req.url = this.url; req.params = this.params; return req; } } /** * 执行协议请求 * @param callback */ public void execute(final IResponseCallback callback) { if(params == null){ params = new HashMap<>(); } if (!params.containsKey("appID")) { params.put("appID", GlobalConfig.getInstance().getAppID()); } if (!params.containsKey("timestamp")) { params.put("timestamp", System.currentTimeMillis()/1000+""); } if (!params.containsKey("sign")) { params.put("sign", SignUtils.sign(params, GlobalConfig.getInstance().getAppKey())); } HttpClient.getInstance().post(getUrl(url), params, new IHttpClientListener() { @Override public void onSuccess(String content) { Log.d(Constants.TAG, "api request result. url:" + url + "; result:" + content); if (callback != null){ try { JSONObject json = new JSONObject(content); int code = json.optInt("code", S_CODE_FAILURE); String msg = json.optString("msg", ""); if (code == S_CODE_SUCCESS) { callback.onSuccess(json); } else { callback.onFailed(R_CODE_FAIL, msg); } }catch (Exception e) { Log.e(Constants.TAG, "api request failed", e); callback.onFailed(R_CODE_RESPONSE_ERROR, "api response parse failed"); } } } @Override public void onFail() { if(callback != null){ callback.onFailed(R_CODE_NETWORK_ERROR, "api request failed. network error"); } } }); } /** * 获取协议访问根地址 * @return */ protected String getBaseUrl() { // String baseUrl = Constants.BASE_URL; String baseUrl = GlobalConfig.getInstance().getLocalConfig("ug.server.url"); while (baseUrl.endsWith("/")) { baseUrl = baseUrl.substring(0, baseUrl.length()-1); } return baseUrl; } /** * 获取协议完整地址 * @param relativeUrl * @return */ protected String getUrl(String relativeUrl) { if (TextUtils.isEmpty(relativeUrl)) { return getBaseUrl(); } if (relativeUrl.startsWith("http") || relativeUrl.startsWith("https")) { return relativeUrl; } if (!relativeUrl.startsWith("/")) { relativeUrl = "/" + relativeUrl; } return getBaseUrl() + relativeUrl; } } |
4、网络协议调用示例
对于各个协议调用的时候, 直接调用上面封装的ApiRequest组件就可以完成了。 我们看其中邮箱注册协议的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public static void register(String email, String password, String code, String deviceID, final IApiListener<UGUser> listener) { ApiRequest req = new ApiRequest.Builder("/user/register") .addParam("email", email) .addParam("password", password) .addParam("code", code) .addParam("deviceID", deviceID) .addParam("channelID", "0") .build(); req.execute(new IResponseCallback() { @Override public void onSuccess(JSONObject json) { UGUser result = parseLoginResult(Constants.LoginType.Email, json); if (result != null) { listener.onSuccess(result); } else { listener.onFailed(ApiRequest.R_CODE_RESPONSE_ERROR, "login response parse failed"); } } @Override public void onFailed(int code, String msg) { listener.onFailed(code, msg); } }); } |
好了,本篇我们介绍了在UGSDK中基础http请求组件的封装以及网络协议的规则制定和封装。感兴趣的同学可以加入我们的技术交流Q裙哦(1055996444)。
本文出自 U8SDK技术博客,转载时请注明出处及相应链接。
本文永久链接: http://www.uustory.com/?p=2398