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

本文涉及的产品
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
数据传输服务 DTS,数据同步 1个月
简介: 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)

image.png


1. 背景


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


image.png


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兼容的,特性丰富,有着良好,快速,充分的文档,已经被许多知名的,很多大厂都在使用。


官方文档curl.se/libcurl/

3. libcurl库编译


3.1 编译openssl


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


我们从 github.com/openssl/ope… 下载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版本:github.com/nghttp2/ngh…


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源码github.com/curl/curl/r… 后解压,进入源码目录执行:


autoreconf -i
automake
autoconf


配置编译选项:


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。


官方文档: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, "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请求示例。

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
Sqoop 企业级大数据迁移方案实战
Sqoop是一个用于在Hadoop和关系数据库服务器之间传输数据的工具。它用于从关系数据库(如MySQL,Oracle)导入数据到Hadoop HDFS,并从Hadoop文件系统导出到关系数据库。 本课程主要讲解了Sqoop的设计思想及原理、部署安装及配置、详细具体的使用方法技巧与实操案例、企业级任务管理等。结合日常工作实践,培养解决实际问题的能力。本课程由黑马程序员提供。
目录
相关文章
|
10天前
|
缓存 网络协议 安全
49. 【Android教程】HTTP 使用详解
49. 【Android教程】HTTP 使用详解
15 1
|
28天前
|
监控 Unix 应用服务中间件
Android-音视频学习系列-(八)基于-Nginx-搭建(rtmp、http)直播服务器
Android-音视频学习系列-(八)基于-Nginx-搭建(rtmp、http)直播服务器
|
28天前
|
缓存 安全 前端开发
探索HTTP协议:网络通信的基石
探索HTTP协议:网络通信的基石
|
28天前
|
安全 Android开发
Android之OKHttp基本使用和OKHttp发送https请求安全认证
Android之OKHttp基本使用和OKHttp发送https请求安全认证
55 0
|
9天前
|
网络协议 前端开发 Java
网络原理 - HTTP / HTTPS(4)——构造http请求
网络原理 - HTTP / HTTPS(4)——构造http请求
13 1
|
9天前
|
JSON 缓存 前端开发
网络原理 - HTTP / HTTPS(3)——http响应
网络原理 - HTTP / HTTPS(3)——http响应
10 0
|
9天前
|
存储 JSON 安全
网络原理 - HTTP / HTTPS(2)——http请求
网络原理 - HTTP / HTTPS(2)——http请求
12 1
|
9天前
|
前端开发 网络协议 JavaScript
网络原理 - HTTP / HTTPS(1)——http请求
网络原理 - HTTP / HTTPS(1)——http请求
8 0
|
15天前
|
JSON API 定位技术
.NET集成DeveloperSharp实现http网络请求&与其它工具的比较
该内容介绍了一个支持.NET Core 2.0及以上和.NET Framework 4.0及以上的HTTP请求调用方法,主要讨论了POST和GET两种形式。POST请求较为常见,涉及调用地址、发送参数、HTTP请求头和编码格式设置。文中提供了一个使用DeveloperSharp库发送POST请求的C#代码示例,用于发送短信,其中`IU.HttpPost`方法用于执行POST请求。此外,还提到了`HttpPost`方法的参数和返回值说明。最后简要提及了GET请求,通常用于URL带有查询参数的情况,并给出一个简单的GET请求示例。
|
20天前
|
Android开发 数据安全/隐私保护 iOS开发
ios和安卓测试包发布网站http://fir.im的注册与常用功能
ios和安卓测试包发布网站http://fir.im的注册与常用功能
17 0
ios和安卓测试包发布网站http://fir.im的注册与常用功能