基于libcurl、openssl的跨平台网络库的封装--And
前言
这篇文章中会介绍如何在Android平台用curl+openssl作为网络库进行native封装和java封装成为一套简单易用的http网络库。之所以可以称为“跨平台”,是因为curl、openssl以及c++的功能封装和线程池等基础功能都是完全跨平台的代码,只要编译成对应平台的库即可实现底层功能的跨平台。上层调用层则需要再封装,而本篇文章所用的平台是Android平台,所以上层封装调用是java代码,这一部分是无法跨平台的,需要在各个平台分别实现。
结构总览
直接看一下各个功能的总览图。
androidcurl.png
接下来将介绍从基础库的交叉编译到最终demo完成的各个环节。
交叉编译基础库
首先从最基本的功能开始,要使用curl就需要将curl代码编译成为目标平台的库,同时可能我们编写native功能的时候可能需要在pc平台开发调试,这样的效率会比较高,所以至少需要编译一个pc库(mac或windows)和android需要的库。
首先看一下编译mac使用的库的编译过程(使用mac开发c++封装部分)。第一步需要下载curl的源代码https://github.com/curl/curl,如果需要历史稳定版可以这里找到https://curl.haxx.se/download/。第二步,如果之前没有编译过可能mac会缺少一些自动生成工具--m4、libtool、automake、autoconf。你可以通过下面的信息下载安装它们:
1)安装m4
http://ftp.gnu.org/gnu/m4/
tar -xzvf m4-1.4.17.tar.gz
cd m4-1.4.17
./configure --prefix=/usr/local
make
sudo make install
2)安装autoconf
http://ftp.gnu.org/gnu/autoconf/
tar -xzvf autoconf-2.69.tar.gz
cd autoconf-2.69
./configure --prefix=/usr/local
make
sudo make install
3)安装automake
http://ftp.gnu.org/gnu/automake/
tar xzvf automake-1.15.tar.gz
cd automake-1.15
./configure --prefix=/usr/local
make
sudo make install
4)安装libtool
http://mirror.hust.edu.cn/gnu/libtool/
tar xzvf libtool-2.4.6.tar.gz
cd libtool-2.4.6
./configure --prefix=/usr/local
make
sudo make install
第三步,编译curl--在curl源码路径下./buildconf
./configure --prefix=./libcurl --with-darwinssl
make
make install
运行之后会在libcurl目录下找到编译产物,我们可以用这些库在mac上进行功能开发和调试。
接下来看一下编译android上用的库(上面的4个基础库下载安装是同样需要的),这里就要提到openssl了,为什么之前mac版本不需要编译openssl呢?因为mac的framework中有自己的ssl实现(darwinssl)。在android中虽然也有系统层的ssl实现(目前google用的是openssl的分支boringssl),但是我们仍然需要将openssl编译到curl中以提供TLS的能力。交叉编译android需要的库是一件很繁琐的事情,主要是ndk的不同版本会导致编译失败,所以这里没必要浪费时间,直接用我编译好的库或者用我写好的跑通的编译脚本吧:https://github.com/yutianzuo/build_script,在这里有直接可以用的编译产物,也有编译脚本,和编译的源码版本,用的ndk在脚本中有标注。至此基础工作准备完成,下面看代码。
C++封装部分代码解析
依旧从整体结构看一下,在上到下三张类图可以代码c++功能的全部。
curl_request.png curl_manager.png curl_httpmanager.png
以上三张图,从上到下以此是被封装和封装的关系。
HttpRequest是最基础的封装类,它封装了curl的c API实现了最最基本的http功能,包括设置hosturl,设置header,设置代理地址,设置证书路径等等,这些都是每一个http请求需要的基础功能。由此派生出各种业务可能需要的http请求类--get、postform,postfile、download、put等等,每一个子类实现特定的http请求类型,在子类中处理更上一层的功能。
RequestManager是对所有类型的http请求的封装,它可以作为一类业务功能的集合,比如都要访问hostA的请求都在此类中处理,它管理这所有类型的请求。它同时管理着线程池,目前的实现是多个RequestManager示例共用一个线程池。
HttpManager作为一个单例是最上层封装,它管理多个RequestManager实例,并且管理着一个全局锁以供业务方使用。
那么C++部分的功能代码就是这样的,它的调用看起来是这个样子
{
HttpManager::init(5);
RequestManager::STRING_MAP headers;
headers["1"] = "2";
headers["3"] = "4";
RequestManager::STRING_MAP params;
params["5"] = "6";
params["7"] = "8";
RequestManager *manager_download = HttpManager::get_instance()->get_request_manager("hashcode");
manager_download->set_proxy_path("http://localhost:8888");//for debug
manager_download->set_host(
"path/somefile.file");
manager_download->download("", headers, params, "/Users/yourname/Downloads/test.file",
[](int
result,
const std::string &str_respones,
float persent,
size_t call_back_seq,
int internal_code,
void *extra)
{}, 12
);
//sleep for callback...
HttpManager::uninit();
}
Android上层调用封装
C++部分代码直接移植到android studio上就可以编译通过,只不过我们需要配置一下编译脚本,这里用到AS3.1版本,编译脚本为CMake方式。具体的工程配置和脚本配置可见源码。这里需要提示一下,底层库只编译了v7a版本,所以aar层制定了abi类型只接受v7a,这是在实践中总结的比较稳妥的方式,一来减少包体积,二来减少了粗心少提供不同cpu架构库的崩溃几率。
首先看一下jni层的封装
public class JniCurl {
static {
System.loadLibrary("native_net");
}
public static native void init(int threadPoolSize, Object callBack);
public static native void unInit();
public static native void addBasicHeader(String strHash, String strKey, String strValue);
public static native void addBasicURLParam(String strHash, String strKey, String strValue);
public static native void setHost(String strHash, String strHost);
public static native void setCertPath(String strHash, String strCertPath);
public static native void setProxy(String strHash, String proxy);
public static native void get(String strHash, int requestSeq, String strPath, List<String> headers_keys,
List<String> headers_values, List<String> params_keys, List<String> params_values);
public static native void postFromData(String strHash, int requestSeq, String strPath, List<String> headers_keys,
List<String> headers_values, List<String> params_keys, List<String> params_values);
public static native void postJson(String strHash, int requestSeq, String strPath, List<String> headers_keys,
List<String> headers_values, String strJson);
public static native void putJson(String strHash, int requestSeq, String strPath, List<String> headers_keys,
List<String> headers_values, String strJson);
public static native void postFile(String strHash, int requestSeq, String strPath, List<String> headers_keys,
List<String> headers_values, String strFormName, List<String> params_keys, List<String> params_values,
String strJsonName, String strJson, String strFileKeyName, String strFilePath, String strFileName);
public static native void download(String strHash, int requestSeq, String strPath, List<String> headers_keys,
List<String> headers_values, List<String> params_keys, List<String> params_values, String strFilePath);
}
这里基本就是对C++层的RequestManager能力的一个封装,我们再通过aar层android库的封装起到对这些基本功能的一个管理目的。aar层的封装基本和C++功能层的封装是对应的,同样是RequestManager和HttpManager两层来进行功能封装,这里不赘述了。不同的是java的HttpManager承担了native回调分发的工作,在这里来沟通上下的数据传递。还有一点值得注意的是java和C++的RequestManager之间的对应关系是通过上层的一个hash值来对应起来的,也就是说java层去调用具体功能都是用自己保有的一个hash值去操作底层的对象,在上层看来这个hash值就是底层的native对象。具体代码可以参见工程代码。
具体java功能也具备了,那么接下来就是上层调用功能了,比如回调回传的数据还是一个utf-8字符串,反序列化解析的一些功能就需要业务层来实现了,另外还有就是调用的便捷性也需要业务层来实现了,这里直接贴一下我简单封装的业务层代码。具体工程可以依照这个思路封装。
public enum BizNetWrapper {
INSTANCE;
RequestManager mRequest1;
RequestManager mRequest2;
RequestManager mRequest3;
public void init(Context context) {
HttpManager.INSTANCE.Uninit();
HttpManager.INSTANCE.Init(5, context);
}
public void uninit() {
HttpManager.INSTANCE.Uninit();
mRequest1 = null;
mRequest2 = null;
mRequest3 = null;
}
public RequestManager getBizRequestManager(Context context) {
if (mRequest1 == null) {
mRequest1 = HttpManager.INSTANCE.getRequest();
mRequest1.setHost("http://news.163.com/");
mRequest1.addBasicHeader("MyCookie", "123456789");
mRequest1.addBasicHeader("MyCookie2", "123456789123456");
mRequest1.addBasicUrlParams("param1", "value");
mRequest1.addBasicUrlParams("param2", "value");
mRequest1.setCertPath(Misc.getAppDir(context) + Misc.CERT_NAME);
// mRequest1.setProxy("http://172.18.100.56:8888"); //for debug
}
return mRequest1;
}
public RequestManager getBizRequestManager2(Context context) {
if (mRequest2 == null) {
mRequest2 = HttpManager.INSTANCE.getRequest();
mRequest2.setHost("http://example.com/");
mRequest2.setCertPath(Misc.getAppDir(context) + Misc.CERT_NAME);
// mRequest2.setProxy("http://172.18.100.56:8888"); //for debug
}
return mRequest2;
}
public RequestManager getBizRequestManager3(Context context) {
if (mRequest3 == null) {
mRequest3 = HttpManager.INSTANCE.getRequest();
mRequest3.setHost("https://somehost");
mRequest3.setCertPath(Misc.getAppDir(context) + Misc.CERT_NAME);
// mRequest3.setProxy("http://172.18.100.56:8888"); //for debug
}
return mRequest3;
}
public interface HttpCallbackBiz {
void success(BeanTest data);
void fail(int errcode);
void progress(float persent);
}
static public class UrlBuilder {
private RequestManager request;
private String mPath = ""; //important
private String mJson;
private Map<String, String> headers = new HashMap<>();
private Map<String, String> urlParams = new HashMap<>();
private Map<String, String> formDataParams = new HashMap<>();
private String mJsonName;
private String mFormName;
private String mFileKeyName;
private String mFileName;
private String mFilePath;
private String mDownFilePath;
public UrlBuilder with(RequestManager request) {
this.request = request;
return this;
}
public UrlBuilder addHeader(String key, String value) {
headers.put(key, value);
return this;
}
public UrlBuilder addUrlParam(String key, String value) {
urlParams.put(key, value);
return this;
}
public UrlBuilder setPath(String strPath) {
mPath = strPath;
return this;
}
public UrlBuilder addFormData(String key, String value) {
formDataParams.put(key, value);
return this;
}
public UrlBuilder setJson(String strJson) {
this.mJson = strJson;
return this;
}
public UrlBuilder setFormName(String formName) {
mFormName = formName;
return this;
}
public UrlBuilder setJsonName(String jsonName) {
mJsonName = jsonName;
return this;
}
public UrlBuilder setFileKeyName(String fileKeyName) {
mFileKeyName = fileKeyName;
return this;
}
public UrlBuilder setFileName(String fileName) {
mFileName = fileName;
return this;
}
public UrlBuilder setFilePath(String filePath) {
mFilePath = filePath;
return this;
}
public UrlBuilder setDownloadFilePath(String filePath) {
mDownFilePath = filePath;
return this;
}
public void get(final HttpCallbackBiz callback) {
request.get(mPath, headers, urlParams, new HttpCallback() {
@Override
public void success(String respones) {
//gson thing...
BeanTest bean = new BeanTest();
bean.rep = respones;
callback.success(bean);
}
@Override
public void progress(float persent) {
callback.progress(persent);
}
@Override
public void fail(int errcode) {
callback.fail(errcode);
}
});
}
public void postFormdata(final HttpCallbackBiz callbackBiz) {
request.postForm(mPath, headers, formDataParams, new HttpCallback() {
@Override
public void success(String respones) {
//gson thing...
BeanTest bean = new BeanTest();
bean.rep = respones;
callbackBiz.success(bean);
}
@Override
public void progress(float persent) {
callbackBiz.progress(persent);
}
@Override
public void fail(int errcode) {
callbackBiz.fail(errcode);
}
});
}
public void postJson(final HttpCallbackBiz callbackBiz) {
request.postJson(mPath, headers, mJson, new HttpCallback() {
@Override
public void success(String respones) {
//gson thing...
BeanTest bean = new BeanTest();
bean.rep = respones;
callbackBiz.success(bean);
}
@Override
public void progress(float persent) {
callbackBiz.progress(persent);
}
@Override
public void fail(int errcode) {
callbackBiz.fail(errcode);
}
});
}
public void putJson(final HttpCallbackBiz callbackBiz) {
request.putJson(mPath, headers, mJson, new HttpCallback() {
@Override
public void success(String respones) {
//gson thing...
BeanTest bean = new BeanTest();
bean.rep = respones;
callbackBiz.success(bean);
}
@Override
public void progress(float persent) {
callbackBiz.progress(persent);
}
@Override
public void fail(int errcode) {
callbackBiz.fail(errcode);
}
});
}
public void postFile(final HttpCallbackBiz callbackBiz) {
request.postFile(mPath, headers, mFormName, formDataParams, mJsonName, mJson, mFileKeyName, mFilePath,
mFileName, new
HttpCallback() {
@Override
public void success(String respones) {
//gson thing...
BeanTest bean = new BeanTest();
bean.rep = respones;
callbackBiz.success(bean);
}
@Override
public void progress(float persent) {
callbackBiz.progress(persent);
}
@Override
public void fail(int errcode) {
callbackBiz.fail(errcode);
}
});
}
public void downloadFile(final HttpCallbackBiz callbackBiz) {
request.downloadFile(mPath, headers, urlParams, mDownFilePath,
new
HttpCallback() {
@Override
public void success(String respones) {
//gson thing...
BeanTest bean = new BeanTest();
bean.rep = respones;
callbackBiz.success(bean);
}
@Override
public void progress(float persent) {
callbackBiz.progress(persent);
}
@Override
public void fail(int errcode) {
callbackBiz.fail(errcode);
}
});
}
}
}
调用:
new BizNetWrapper.UrlBuilder().
with(BizNetWrapper.INSTANCE.getBizRequestManager(MainActivity.this)).
addHeader("customheader1", "value").
addHeader("customheader2", "value").
addUrlParam("customparam1", "value").
addUrlParam("customparam2", "value").
setPath("").
get(new HttpCallbackBiz() {
@Override
public void success(BeanTest data) {
mTextView.setText("Get TestCase OK");
Log.e("JAVA_TAG", data.rep);
}
@Override
public void fail(int errcode) {
mTextView.setText("Get TestCase Failed:" + errcode);
Log.e("JAVA_TAG", "fail:" + errcode);
}
@Override
public void progress(float persent) {
Log.e("JAVA_TAG", "progress");
}
});
最后
上面简要的大体介绍了一下功能和封装等结构,更多的细节请见源码:https://github.com/yutianzuo/android-curl