基于libcurl、openssl的跨平台网络库的封装--Android篇

前言

    这篇文章中会介绍如何在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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容