写点什么

Android C++ 系列:JNI 中发送 Http 网络请求

作者:轻口味
  • 2022 年 4 月 07 日
  • 本文字数:6878 字

    阅读完需:约 23 分钟

Android C++系列:JNI中发送Http网络请求

1. 背景

之前 Linux 网络编程的文章下有小伙帮咨询 jni 中发送 http 请求的示例,本文基于 libcurl 库实现 http 网络请求发送功能。


2. libcurl 库介绍

libcurl 是一个免费和易于使用的客户端 URL 传输库,支持 DICT, FILE, FTP, FTPS, GOPHER, gopers, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTMP, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET 和 TFTP。libcurl 支持 SSL 证书,HTTP POST, HTTP PUT, FTP 上传,HTTP 表单上传,代理,HTTP/2, HTTP/3, cookie,用户+密码认证(基本,摘要,NTLM,协商,Kerberos),文件传输恢复,HTTP 代理隧道等等!


libcurl 是高度可移植的,它构建和工作在许多平台上,包括 Solaris, NetBSD, FreeBSD, OpenBSD,达尔文,HPUX, IRIX, AIX, Tru64, Linux, UnixWare, HURD, Windows, Amiga, OS/2, BeOs, Mac OS X, Ultrix, QNX, OpenVMS, RISC OS, Novell NetWare, DOS 等等。


libcurl 是免费的,线程安全的,IPv6 兼容的,特性丰富,有着良好,快速,充分的文档,已经被许多知名的,很多大厂都在使用。


官方文档:https://curl.se/libcurl/

3. libcurl 库编译

3.1 编译 openssl

libcurl 支持 SSL 证书,我们需要支持 HTTPS 的话需要依赖 openssl 库,我们先把 openssl 库编译出来。


我们从 https://github.com/openssl/openssl/archive/OpenSSL_1_1_0h.tar.gz 下载 1.1.0h 版本的 openssl 库,解压后执行 Configure 配置脚本:


Configure" \"${OPENSSL_TARGET}" \-DARCH="${OPENSSL_ARCH}" \-DCROSS_COMPILE="${OPENSSL_CROSS_COMPILE}" \-DMACHINE="${OPENSSL_MACHINE}" \-DRELEASE="${OPENSSL_RELEASE}" \-DSYSTEM="${OPENSSL_SYSTEM}" \no-asm \no-comp \no-dso \no-dtls \no-engine \no-hw \no-idea \no-nextprotoneg \no-psk \no-srp \no-ssl3 \no-weak-ssl-ciphers \--prefix="${INSTALL_TARGET}" \--openssldir="${INSTALL_TARGET}/ssl" \-D_FORTIFY_SOURCE="2" -fstack-protector-strong
复制代码


由于我们是使用 ndk 交叉编译,需要配置架构 ARCH 和跨平台编译器 CROSS_COMPILE。


再执行 make 进行编译。

3.2 编译 nghttp2

如果需要支持 HTTP2 协议,需要依赖 nghttp2 库,这里我们下载 1.32.0 版本:https://github.com/nghttp2/nghttp2/releases/download/v1.32.0/nghttp2-1.32.0.tar.gz,解压后运行 configure 脚本:


configure" \${DISABLE_RPATH} \--prefix="${INSTALL_TARGET}" \--host="${TOOLCHAIN_HOST}" \--build="${TOOLCHAIN_BUILD}" \--enable-static="YES" \--enable-shared="YES" \CPPFLAGS="-fPIE -D_FORTIFY_SOURCE=2 -fstack-protector-strong" \LDFLAGS="-fPIE -pie" \PKG_CONFIG_LIBDIR="${INSTALL_TARGET_LIB}/pkgconfig"
复制代码


执行 make 编译。

3.3 编译 curl

下载 7.61.0 版本 curl 源码https://github.com/curl/curl/releases/download/curl-7_61_0/curl-7.61.0.tar.gz 后解压,进入源码目录执行:


autoreconf -iautomakeautoconf
复制代码


配置编译选项:


CFLAGS="-fstack-protector-strong" \CPPFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -I\"${INSTALL_TARGET_INCLUDE}\"" \LDFLAGS="-L${INSTALL_TARGET_LIB} -Wl,-rpath=${INSTALL_TARGET_LIB}" 
configure" \ ${DISABLE_RPATH} \ --prefix="${INSTALL_TARGET}" \ --with-sysroot="${SYSROOT}" \ --host="${TOOLCHAIN_HOST}" \ --build="${TOOLCHAIN_BUILD}" \ --enable-optimize \ --enable-hidden-symbols \ --disable-largefile \ --disable-static \ --disable-ftp \ --disable-file \ --disable-ldap \ --disable-rtsp \ --disable-proxy \ --disable-dict \ --disable-telnet \ --disable-tftp \ --disable-pop3 \ --disable-imap \ --disable-smb \ --disable-smtp \ --disable-gopher \ --disable-manual \ --disable-verbose \ --disable-sspi \ --disable-crypto-auth \ --disable-tls-srp \ --disable-unix-sockets \ --enable-cookies \ --without-zlib \ --with-ssl="${INSTALL_TARGET}" \ --with-ca-bundle="${CURL_CA_BUNDLE}" \ --with-nghttp2="${INSTALL_TARGET}"
复制代码


这里面最后配置了 ssl 和 nghttp2 库的路径。


执行 make 编译。

4. libcurl 库 API 介绍

编译出最终的库后可以开始使用了,使用前我们先了解 libcurl 库主要 API。


官方文档:https://curl.se/libcurl/c/

4.1 全局初始化

应用程序在使用 libcurl 之前,必须先初始化 libcurl。libcurl 只需初始化一次。可以使用以下语句进行初始化:


curl_global_init();
复制代码


curl_global_init()接收一个参数,告诉 libcurl 如何初始化。参数 CURL_GLOBAL_ALL 会使 libcurl 初始化所有的子模块和一些默认的选项,我们通常使用这个默认值即可。还有两个可选值:

CURL_GLOBAL_WIN32

只能应用于 Windows 平台。它告诉 libcurl 初始化 winsock 库。如果 winsock 库没有正确地初始化,应用程序就不能使用 socket。在应用程序中,只要初始化一次即可。

CURL_GLOBAL_SSL

如果 libcurl 在编译时被设定支持 SSL,那么该参数用于初始化相应的 SSL 库。同样,在应用程序中,只要初始化一次即可。


libcurl 有默认的保护机制,如果在调用 curl_easy_perform 时它检测到还没有通过 curl_global_init 进行初始化,libcurl 会根据当前的运行时环境,自动调用全局初始化函数。但是,安全起见,我们还是自己来全局初始化一波。当应用程序不再使用 libcurl 的时候,应该调用 curl_global_cleanup 来释放相关的资源。


注意:使用过程中应当避免多次调用 curl_global_init 和 curl_global_cleanup,最好是进程启动和进程结束时各调用一次。

4.2 版本信息

在运行时根据 libcurl 支持的特性来进行开发,通常比编译时更好。可以通过调用 curl_version_info 函数返回的结构体来获取运行时的具体信息,从而确定当前环境下 libcurl 支持的一些特性。比如我们查看是否支持 HTTP2:


if (!(curl_version_info(CURLVERSION_NOW)->features & CURL_VERSION_HTTP2)) {  LOGI("curl not support http2");  }
复制代码


curl_version_info_data 包含以下内容:


  1. age:age of the returned struct

  2. version:LIBCURL_VERSION

  3. version_num:LIBCURL_VERSION_NUM

  4. host:OS/host/cpu/machine when configured

  5. features:bitmask

  6. ssl_version:human readable string

  7. ssl_version_num:not used anymore, always 0

4.3 easy interface

libcurl 提供了两种接口:easy interface 与 multi interface。


  • easy interface 是同步的,高效的,快速上手的,许多应用程序都是使用这种方法构建的。

  • multi interface 是异步的,它还提供了使用单线程或多线程的多路传输。


easy interface 的 api 函数都是有相同的前缀:curl_easy。

4.3.1 创建 easy handle

要使用 easy interface,首先必须创建一个 easy handle,easy handle 用于执行每次操作。下面的函数用于获取一个 easy handle :


CURL *easy_handle = curl_easy_init();
复制代码


每个线程都应该有自己的 easy handle 用于网络请求。千万不要在多线程之间共享同一个 easy handle。

4.3.2 设置属性

在 easy handle 上可以设置属性和操作(action)。easy handle 就像一个逻辑连接,用于接下来要进行的数据传输。


使用curl_easy_setopt函数可以设置 easy handle 的属性和操作,这些属性和操作控制 libcurl 如何与远程主机进行数据通信。一旦在 easy handle 中设置了相应的属性和操作,它们将一直作用与该 easy handle。也就是说,重复使用 easy hanle 向远程主机发出请求,先前设置的属性仍然生效。


easy handle 的许多属性使用字符串(以/0 结尾的字节数组)来设置。通过 curl_easy_setopt 函数设置字符串属性时,libcurl 内部会自动拷贝这些字符串,所以在设置完相关属性之后,字符串可以直接被释放掉。


easy handle 最基本、最常用的属性是 URL。你应当通过 CURLOPT_URL 属性提供适当的 URL:


curl_easy_setopt(easy_handle, CURLOPT_URL, "http://baidu.com ");

4.3.3 设置回调函数

我们发起请求后需要获取请求响应,这个时候需要通过 curl_easy_setopt 来设置回调函数,回调函数的原型如下:


size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp);
复制代码


使用下面的语句来注册回调函数,回调函数将会在接收到数据的时候被调用:


curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_data);
复制代码


可以给回调函数提供一个自定义参数(libcurl 不处理该参数,只是简单的传递):


curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, &internal_struct);
复制代码


如果你没有通过 CURLOPT_WRITEFUNCTION 属性给 easy handle 设置回调函数,libcurl 会提供一个默认的回调函数,它只是简单的将接收到的数据打印到标准输出。我们可以通过 CURLOPT_WRITEDATA 属性给默认回调函数传递一个已经打开的文件指针,用于将数据输出到文件里。

4.3.4 执行网络请求

调用 curl_easy_perform 函数,将执行真正的数据通信:


success = curl_easy_perform(easy_handle);
复制代码


curl_easy_perfrom 将连接到远程主机,执行必要的命令,并接收数据。当接收到数据时,先前设置的回调函数将被调用。libcurl 可能一次只接收到 1 字节的数据,也可能接收到好几 K 的数据,libcurl 会尽可能多、及时的将数据传递给回调函数。回调函数返回接收的数据长度。如果回调函数返回的数据长度与传递给它的长度不一致(即返回长度 != size * nmemb),libcurl 将会终止操作,并返回一个错误代码。


当数据传递结束的时候,curl_easy_perform 将返回一个代码表示操作成功或失败。如果需要获取更多有关通信细节的信息,你可以设置 CURLOPT_ERRORBUFFER 属性,让 libcurl 缓存许多可读的错误信息。


easy handle 在完成一次数据通信之后可以被重用,libcurl 推荐重用一个已经存在的 easy handle。如果在完成数据传输之后,你创建另一个 easy handle 来执行其他的数据通信,libcurl 在内部会尝试着重用上一次创建的连接。

4.3.5 释放 easy handle

可以通过curl_easy_cleanup释放 easy handle。

4.4 multi interface

上面介绍的 easy interface 以同步的方式进行数据传输,curl_easy_perform 会一直阻塞到数据传输完毕后返回,且一次操作只能发送一次请求,如果要同时发送多个请求,必须使用多线程。 而 multi interface 以一种简单的、非阻塞的方式进行传输,它允许在一个线程中,同时提交多个相同类型的请求。 multi interface 是建立在 easy interface 基础之上的,它只是简单的将多个 easy handler 添加到一个 multi stack,而后同时传输而已。使用 multi interface 很简单,首先使用 curl_multi_init()函数创建一个 multi handler,然后使用 curl_easy_init()创建一个或多个 easy handler,并按照上面介绍的接口正常的设置相关的属性,然后通过 curl_multi_add_handler 将这些 easy handler 添加到 multi handler,最后调用 curl_multi_perform 进行数据传输。


curl_multi_perform 是异步的、非阻塞的函数。如果它返回 CURLM_CALL_MULTI_PERFORM,表示数据通信正在进行。


每个 easy handler 在低层就是一个 socket,通过 select()来管理这些 socket,在有数据可读/可写/异常的时候,通知应用程序,所以通过 select()来操作 multi interface 将会使工作变得简单。在调用 select()函数之前,应该使用 curl_multi_fdset 来初始化 fd_set 变量。


select()函数返回时,说明受管理的低层 socket 可以操作相应的操作(接收数据或发送数据,或者连接已经断开),此时应该马上调用 curl_multi_perform,libcurl 将会执行相应操作。使用 select()时,应该设置一个较短的超时时间。在调用 select()之前,不要忘记通过 curl_multi_fdset 来初始化 fd_set,因为每次操作,fd_set 中的文件描述符可能都不一样。


如果想中止 multi stack 中某一个 easy handle 的数据通信,可以调用 curl_multi_remove_handle 函数将其从 multi stack 中取出。同事不要忘记释放掉 easy handle(通过 curl_easy_cleanup()函数)。


当 multi stack 中的一个 eash handle 完成数据传输的时候,同时运行的传输任务数量就会减少一个。当数量降到 0 的时候,说明所有的数据传输已经完成。


curl_multi_info_read 用于获取当前已经完成的传输任务信息,它返回每一个 easy handle 的 CURLcode 状态码。可以根据这个状态码来判断每个 easy handle 传输是否成功。

5. 发送网络请求示例

5.1 使用 easy interface 发送 http 请求

我们简单在回调结果中打印响应内容:


size_t process_data(void *buffer, size_t size, size_t nmemb, void *user_p) {  FILE *fp = (FILE *)user_p;  size_t return_size = fwrite(buffer, size, nmemb, fp);  LOGI("process_data = %s", buffer);
return return_size;}
复制代码


发送请求:


static jint_httprequest(JNIEnv *env, jclass cls) {  CURL *easy_handle = curl_easy_init();  curl_easy_setopt(easy_handle, CURLOPT_URL, "http://baidu.com");  curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, &process_data);  curl_easy_perform(easy_handle);  curl_easy_cleanup(easy_handle);  return 0;}
复制代码


打印结果:


process_data = <html>    <meta http-equiv="refresh" content="0;url=http://www.baidu.com/">    </html>
复制代码

5.2 使用 multi interface 发送 http 请求

我们创建两个 easy handle 用来分别向新浪和搜狐网站发送请求并打印响应结果:


size_t save_sina_page(void *buffer, size_t size, size_t count, void *user_p){  LOGI("save_sina_page = %s", buffer);
return size;}size_t save_sohu_page(void *buffer, size_t size, size_t count, void *user_p){ LOGI("save_sohu_page = %s", buffer);
return size;}static jint_httprequest2(JNIEnv *env, jclass cls) { CURLM *multi_handle = NULL; CURL *easy_handle1 = NULL; CURL *easy_handle2 = NULL;
multi_handle = curl_multi_init();
// 设置easy handle easy_handle1 = curl_easy_init(); curl_easy_setopt(easy_handle1, CURLOPT_URL, "http://www.sina.com.cn"); curl_easy_setopt(easy_handle1, CURLOPT_WRITEFUNCTION, &save_sina_page);
easy_handle2 = curl_easy_init(); curl_easy_setopt(easy_handle2, CURLOPT_URL, "http://www.sohu.com"); curl_easy_setopt(easy_handle2, CURLOPT_WRITEFUNCTION, &save_sohu_page);
// 添加到multi stack curl_multi_add_handle(multi_handle, easy_handle1); curl_multi_add_handle(multi_handle, easy_handle2);
// int running_handle_count; while (CURLM_CALL_MULTI_PERFORM == curl_multi_perform(multi_handle, &running_handle_count)) { LOGI("running_handle_count = %d", running_handle_count); }
while (running_handle_count) { timeval tv; tv.tv_sec = 1; tv.tv_usec = 0;
int max_fd; fd_set fd_read; fd_set fd_write; fd_set fd_except;
FD_ZERO(&fd_read); FD_ZERO(&fd_write); FD_ZERO(&fd_except);
curl_multi_fdset(multi_handle, &fd_read, &fd_write, &fd_except, &max_fd); int return_code = select(max_fd + 1, &fd_read, &fd_write, &fd_except, &tv); if (-1 == return_code) { LOGI("select error."); break; } else { while (CURLM_CALL_MULTI_PERFORM == curl_multi_perform(multi_handle, &running_handle_count)) { LOGI("running_handle_count = %d", running_handle_count); } } }
// 释放资源 curl_easy_cleanup(easy_handle1); curl_easy_cleanup(easy_handle2); curl_multi_cleanup(multi_handle); curl_global_cleanup(); return 0;}
复制代码


执行结果:


2022-02-11 16:08:58.213 21853-23488/com.qingkouwei.chttp2 I/JNI_HTTP: [save_sina_page():60]save_sina_page = <html>    <head><title>302 Found</title></head>    <body>    <center><h1>302 Found</h1></center>    <hr><center>nginx</center>    </body>    </html>2022-02-11 16:08:58.220 21853-23488/com.qingkouwei.chttp2 I/JNI_HTTP: [save_sohu_page():65]save_sohu_page = <html>    <head><title>307 Temporary Redirect</title></head>    <body bgcolor="white">    <center><h1>307 Temporary Redirect</h1></center>    <hr><center>nginx</center>    </body>    </html>
复制代码

6. 总结

本文介绍了 Android 在 jni 中使用 libcurl 发送 http 网络请求,libcurl 是一个传统的功能强大的客户端网络库,优点是成熟稳定,确定是功能强大带来的臃肿,编译出来的动态库有 400 多 k。稳重介绍了 libcurl 的跨平台交叉编译方法以及 libcurl 的 API,并提供了基于 easy interface 与 multi interface 的 http 请求示例。

发布于: 18 小时前阅读数: 41
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android、音视频、AI相关领域从业者。 欢迎加我微信wodekouwei拉您进InfoQ音视频沟通群 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
Android C++系列:JNI中发送Http网络请求_c++_轻口味_InfoQ写作平台