游戏SDK架构设计之代码实现——网络框架
创始人
2025-05-29 06:28:37

OKHttp 源码解析(一)
OKHttp 源码解析(二)拦截器

前言

前篇介绍了游戏SDK的基本架构设计,其中一个模块是基础工具库,基础工具库的内容包括:统一封装的网络框架,可以使用okhttp、volley、retrofit,或自行写的异步任务也可以。还有存储工具类、文件工具类、热修复工具类等。这个篇幅先介绍一下网络框架。

由于游戏SDK是提供给CP使用的,为避免不必要的第三方库冲突,也同时减少包体,游戏SDK的应尽量减少引入第三方库。

这里的网络框架从一开始由游戏SDK自行封装的异步任务后改为封装 OKHttp,考虑的原因有以下几点:

  1. 自行封装的网络框架无法满足需求某个任务的需求,比如有业务场景,当用户取消登录时,需要立即取消登录请求,不再继续登录。
  2. 目前 OKHttp 非常普遍,Android 原生的HttpClient在 android5.0被废弃,6.0逐渐删除。HttpURLConnection 是一个轻量级的http客户端,api 较少,用得不是很方便。而OKHttp支持 2.3 及以上版本,支持 JDK 1.7 及以上版本。

网络框架设计的需求分析

我们写的代码就是为了满足需求的,如何设计一个框架也是从需求出发,设计一个最适合项目的网络框架。游戏SDK的网络请求需求分析:

  1. 正常的 post/get 请求;
  2. 多文件上传文件、下载文件;
  3. 统一的回调处理是否需要提示统一错误;
  4. 统一处理返回值。
  5. 请求失败时自动重试。

OKHttp 二次封装

这里只简单说一下整个流程,引出后面对 OKHttp 的理解分析。

以下只说使用过程,源码详解见:

1、初始化及发送请求

  1. 创建单例管理类,单例实例使用 volatile 关键词修饰,关于 volatile 的作用:https://blog.csdn.net/MarinaTsang/article/details/84306990
// 初始化 okhttp ,单例管理public class OkHttpManager {private static final String TAG = "[OkHttpManager.requestServerData]";private static int TIME_OUT = 10;private static int READ_TIME_OUT = 15;private static int MAX_RETRY = 3;// 设置最长下载时间 30sprivate static int MAX_DOWNLOAD_TIME = 30;private static volatile OkHttpManager mManager;private OkHttpClient mOkHttpClient;public OkHttpManager() {OkHttpClient.Builder builder = new OkHttpClient.Builder();builder.hostnameVerifier((hostname, session) -> true);
//        builder.sslSocketFactory(SSL.getSSLSocketFactory(), SSL.getTrustAllCert());// 尝试解决:okhttp3.internal.framed.StreamResetException: stream was reset: CANCEL// https://github.com/square/okhttp/issues/3955
//        builder.protocols(Collections.singletonList(Protocol.HTTP_1_1));// 这个默认只重试一次; 关闭默认重连机制builder.retryOnConnectionFailure(false);builder.connectTimeout(TIME_OUT, TimeUnit.SECONDS);builder.readTimeout(READ_TIME_OUT, TimeUnit.SECONDS);builder.writeTimeout(READ_TIME_OUT, TimeUnit.SECONDS);// 允许重定向builder.followRedirects(true);// 自动重试拦截器builder.addInterceptor(new RetryInterceptor(MAX_RETRY));// 解决 java.io.IOException: unexpected end of stream 问题。builder.addInterceptor(new NetInterceptor());mOkHttpClient = builder.build();// 设置最大请求并发数
//        mOkHttpClient.dispatcher().setMaxRequestsPerHost(15);}/*** 单例*/public static OkHttpManager getInstance() {if (mManager == null) {synchronized (OkHttpManager.class) {if (mManager == null) {mManager = new OkHttpManager();}}}return mManager;}// 之后的就是 get 请求 和 post 请求/*** 网络请求统一分发** @param method* @param url* @param params* @param tag*/public void requestServerData(String method, String url, Map params, Object tag, String urlType, DisposeDataHandler handler) {// 先检测是否有网if (isNetworkAvailable(handler)) {return;}// 拼接 url,不同的跟域名,使用一个 HttpConfig 来统一管理if (HttpConfig.URL_TYPE_SAFE_API.equals(urlType)) {url = PlatInfo.getSdkRootApi() + url;} else if (HttpConfig.URL_TYPE_DATA_COLLECT.equals(urlType)) {url = PlatInfo.getDataCollectorApi() + url;}RequestParams requestParams = new RequestParams(params);if (params != null) {LogUtil.d(TAG, "params===>", params.toString());}// CommonRequest 根据不同类型,构建不同的请求体Request request = null;if (UrlUtils.METHOD_GET.equals(method)) {request = CommonRequest.createGetRequest(url, tag, requestParams);} else if (UrlUtils.METHOD_POST.equals(method)) {request = CommonRequest.createPostRequest(url, tag, requestParams);} else if (UrlUtils.METHOD_POST_JSON.equals(method)) {request = CommonRequest.createPostJsonRequest(url, tag, requestParams);}if (request != null) {// CommonCallback 统一处理回调mOkHttpClient.newCall(request).enqueue(new CommonCallback(handler));}}// 上传文件和下载文件类似}

2、构建请求体

public class CommonRequest {private static final String TAG = "[CommonRequest]";public static final MediaType JSON= MediaType.parse("application/json; charset=utf-8");private static final MediaType FILE_TYPE = MediaType.parse("multipart/form-data; charset=utf-8");/*** 发送 get 请求** @param url* @param params* @return*/public static Request createGetRequest(@NotNull String url, Object tag, @Nullable RequestParams params) {StringBuilder urlBuilder = new StringBuilder(url).append("?");if (params != null) {for (Map.Entry entry : params.getParams().entrySet()) {urlBuilder.append(entry.getKey()).append("=").append(UrlUtils.URLEncode((String) entry.getValue())).append("&");}}return getBuilderAddTag(tag).get().url(urlBuilder.substring(0, urlBuilder.length() - 1)).build();}private static Request.Builder getBuilderAddTag(Object tag) {Request.Builder builder = new Request.Builder();if (tag != null) {builder.tag(tag);}return builder;}/*** 发送 post 表单请求** @param url* @param params* @return*/public static Request createPostRequest(@NotNull String url, Object tag, @NotNull RequestParams params) {FormBody.Builder builder = new FormBody.Builder();for (Map.Entry entry : params.getParams().entrySet()) {builder.add(entry.getKey(), (String) entry.getValue());}FormBody formBody = builder.build();return getBuilderAddTag(tag).url(url).post(formBody).build();}/*** 发送post JSON 请求** @param url* @param params* @return*/public static Request createPostJsonRequest(@NotNull String url, Object tag, @NotNull RequestParams params) {RequestBody body = RequestBody.create(JSON, JsonUtils.map2jsonObject(params.getParams()).toString());return getBuilderAddTag(tag).post(body).url(url).build();}/*** 创建文件上传请求类** @param url* @param tag* @param params* @param fileParams* @return*/public static Request createFilePost(@NotNull String url, Object tag, @Nullable Map params,@NotNull Map> fileParams) {MultipartBody.Builder builder = addFileRegulaParams(params);// 添加多文件Iterator>> iterator = fileParams.entrySet().iterator();while (iterator.hasNext()) {Map.Entry> next = iterator.next();String key = next.getKey();List fileList = next.getValue();if (fileList == null) {continue;}// 循环添加文件for (File file : fileList) {if (file != null) {builder.addFormDataPart(key, file.getName(), RequestBody.create(FILE_TYPE, file));}}}RequestBody build = builder.build();return getBuilderAddTag(tag).post(build).url(url).build();}/*** 创建文件上传请求类* 文件需要根据分类重命名** @param url* @param tag* @param params* @param fileParams 需要分类重命名的文件集合*                   Map>> 外层是的key 是请求key ,比如服务端规定:logFiles,*                   Map> 是文件列表集合,key 是分类,比如CP的日志文件对应一个 list file;*                   SDK 日志对应一个list file*                   需要根据不同的 file 类型,传文件时的名字需要拼接名字* @return*/public static Request createNeedRenameFilePost(@NotNull String url, Object tag, @Nullable Map params,@NotNull Map>> fileParams) {// 添加常规参数MultipartBody.Builder builder = addFileRegulaParams(params);// 添加多文件Iterator>>> iterator = fileParams.entrySet().iterator();while (iterator.hasNext()) {Map.Entry>> next = iterator.next();String key = next.getKey();Map> fileMap = next.getValue();if (fileMap == null) {continue;}Iterator>> iterator1 = fileMap.entrySet().iterator();while (iterator1.hasNext()) {Map.Entry> typeMap = iterator1.next();String typeMapKey = typeMap.getKey();List fileList = typeMap.getValue();if (fileList == null) {continue;}// 循环添加文件for (File file : fileList) {if (file != null) {// 获取扩展名及没有扩展名的文件名String extensionName = FileUtil.getExtensionName(file.getName());String fileNameNoEx = FileUtil.getFileNameNoEx(file.getName());builder.addFormDataPart(key, fileNameNoEx + "_" + typeMapKey + "." + extensionName,RequestBody.create(FILE_TYPE, file));}}}}RequestBody build = builder.build();return getBuilderAddTag(tag).post(build).url(url).build();}

3、创建代理,统一处理 OKHttp 的回调

public class DisposeDataHandler {private DisposeDataListener mListener;private Class mClass;public DisposeDataHandler(DisposeDataListener mListener) {this.mListener = mListener;}public DisposeDataHandler(DisposeDataListener mListener, Class mClass) {this.mListener = mListener;this.mClass = mClass;}public void onSuccess(String responseObj) {mListener.onSuccess(responseObj);}public void onFailure(OkHttpException exception) {mListener.onFailure(exception);}public Class getClassType() {return mClass;}
}

4、处理 OKHttp 的回调

public class CommonCallback implements Callback {private static final String TAG = "[CommonCallback]";private static String VALIDATION_FAILED = "Chain validation failed";protected DisposeDataHandler mDisposeHandler;// 切换回主线程protected Handler mHandler = new Handler(Looper.getMainLooper());public CommonCallback(DisposeDataHandler mDisposeHandler) {this.mDisposeHandler = mDisposeHandler;}/*** 网络请求失败, 开始监测网络** @param call* @param e*/@Overridepublic void onFailure(@NotNull Call call, @NotNull IOException e) {LogUtil.e(TAG, "onFailure connection error!", e.toString(), DeviceUtils.getRequestUuid());// 请求失败,网络错误mHandler.post(() -> {LogUtil.e(TAG, "onFailure connection error or time out! begin to callback");String message = e.getMessage();if (VALIDATION_FAILED.equals(message)) {mDisposeHandler.onFailure(new OkHttpException(PlatErrorCode.VALIDATION_ERROR, message));} else {mDisposeHandler.onFailure(new OkHttpException(PlatErrorCode.CONNECTION_ERROR, PlatErrorCode.MSG_CONNECTION_ERROR));}// 开始监测网络INetTool iNetTool = NTSDK.getInstance().getNetToolManager();if (iNetTool != null) {iNetTool.checkNetSdkAuto(ApkUtils.getApplication().getApplicationContext());}});}@Overridepublic void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {if (!response.isSuccessful()) {LogUtil.e(TAG, "onResponse NOT success.");mHandler.post(() -> {LogUtil.e(TAG, "onResponse NOT success begin to callback.");mDisposeHandler.onFailure(new OkHttpException(PlatErrorCode.CONNECTION_RESPONSE_ERROR, PlatErrorCode.MSG_CONNECTION_RESPONSE_ERROR));});} else {LogUtil.i(TAG, "onResponse success.");mHandler.post(() -> {// 上传网络报告,如果有INetTool iNetTool = NTSDK.getInstance().getNetToolManager();if (iNetTool != null) {iNetTool.uploadNextTime();}});try {LogUtil.i(TAG, "onResponse success start handle response.");handleResponse(response);} catch (IOException e) {LogUtil.e(TAG, "onResponse io exception.",e.getMessage());mDisposeHandler.onFailure(new OkHttpException(PlatErrorCode.RESPONSE_IO_ERROR, PlatErrorCode.MSG_RESPONSE_IO_ERROR));e.printStackTrace();} catch (Exception e) {LogUtil.e(TAG, "onResponse exception.",e.toString());mDisposeHandler.onFailure(new OkHttpException(PlatErrorCode.RESPONSE_IO_ERROR, PlatErrorCode.MSG_RESPONSE_IO_ERROR));e.printStackTrace();}}}protected void handleResponse(Response response) throws IOException {ResponseBody body = response.body();int code = response.code();if (body == null) {LogUtil.e(TAG, "handleResponse:" + code, " body is NULL!");mDisposeHandler.onFailure(new OkHttpException(code, PlatErrorCode.MSG_NO_DATA + code));return;}String result = body.string();if (TextUtils.isEmpty(result)) {LogUtil.e(TAG, "handleResponse:", " body is Empty!", result);mDisposeHandler.onFailure(new OkHttpException(PlatErrorCode.NO_DATA, PlatErrorCode.MSG_NO_DATA));return;}LogUtil.e(TAG, "handleResponse:", " onSuccess ", result);mDisposeHandler.onSuccess(result);}
}

5、根据和服务端的约定,对返回数据进行统一处理

private static class MyDisposeDataListener implements DisposeDataListener {private boolean isChange = false;private BaseCallback mBaseCallback;private String mUrl;private String mUrlType;public MyDisposeDataListener(BaseCallback mBaseCallback, String url, String urlType) {this.mBaseCallback = mBaseCallback;this.mUrl = url;this.mUrlType = urlType;}@Overridepublic void onSuccess(String response) {LogUtil.i(TAG, mUrl, "disposeDataHandler.onSuccess");LogUtil.i(TAG, mUrl, "onSuccess request uuid: ", DeviceUtils.getRequestUuid());try {// 统一解析parseNotPlatResponse(response);} catch (Exception e) {if (mBaseCallback != null) {mBaseCallback.onFailure(PlatErrorCode.DEFAULT_ERROR, PlatErrorCode.MSG_DEFAULT_ERROR);}e.printStackTrace();}}
}

6、将数据回调到业务层

public abstract class BaseCallback {/*** 部分接口不需要提示处理,例如启动时请求的接口,默认需要处理,如不需要处理单独传参数*/private boolean isShowToast = true;private Context mContext;public BaseCallback(Context context) {this.mContext = context;}public BaseCallback(Context context, boolean isShowToast) {this.mContext = context;this.isShowToast = isShowToast;}/*** 数据请求成功** @param data 请求到的数据 ---- 不必使用泛型*/protected abstract void onSuccess(JSONObject data);/*** 使用网络API接口请求方式时,虽然已经请求成功但是由* 于{@code msg}的原因无法正常返回数据。** @param code 服务端返回的错误码* @param msg  错误消息*/public void onFailure(int code, String msg) {LogUtil.e(" request onFailure(),code = " + code, " msg = " + msg);if (isShowToast && mContext != null) {// 统一提示if (code == PlatErrorCode.DEFAULT_ERROR) {ToastUtil.showNormalToast((Activity) mContext, RUtil.getString((Activity) mContext, "string_unknown_error_toast") + code);} else if (code == PlatErrorCode.NETWORK_ERROR) {ToastUtil.showNormalToast((Activity) mContext, RUtil.getString(mContext, "string_network_error_toast"));}}}
}

相关内容

热门资讯

哈尔滨工业大学北京研究院在石景... 转自:北京日报客户端12月15日,石景山区人民政府与哈尔滨工业大学签署合作协议,共建哈尔滨工业大学北...
秦来来:迟蓬,迟来的“蓬头” (来源:上观新闻)一个人因为某件事情突然火起来了、红起来了,上海人叫“起蓬头”;看看影视演员迟蓬在《...
东方网与上海电信达成战略合作,...   炒股就看金麒麟分析师研报,权威,专业,及时,全面,助您挖掘潜力主题机会! (来源:澎湃新闻)1...
焦点复盘沪指低开低走创2个月新... 转自:财联社财联社12月16日讯,今日42股涨停,19股炸板,封板率为69%,百大集团、法尔胜、华菱...
“城中心”尊享养老优选 莲花池...   天渐寒,情愈暖。老年人随着年龄增长,抵抗力与御寒能力逐渐减弱,寒冷天气易引发心血管不适,也容易加...