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

本文涉及的产品
数据传输服务 DTS,数据同步 small 3个月
推荐场景:
数据库上云
数据传输服务 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请求示例。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
Sqoop 企业级大数据迁移方案实战
Sqoop是一个用于在Hadoop和关系数据库服务器之间传输数据的工具。它用于从关系数据库(如MySQL,Oracle)导入数据到Hadoop HDFS,并从Hadoop文件系统导出到关系数据库。 本课程主要讲解了Sqoop的设计思想及原理、部署安装及配置、详细具体的使用方法技巧与实操案例、企业级任务管理等。结合日常工作实践,培养解决实际问题的能力。本课程由黑马程序员提供。
目录
相关文章
|
29天前
|
NoSQL 网络协议 Linux
Redis的实现一:c、c++的网络通信编程技术,先实现server和client的通信
本文介绍了使用C/C++进行网络通信编程的基础知识,包括创建socket、设置套接字选项、绑定地址、监听连接以及循环接受和处理客户端请求的基本步骤。
44 6
|
1月前
|
域名解析 存储 安全
HTTP【网络】
HTTP协议格式、HTTP的方法 、HTTP的状态码、HTTP常见的Header
228 6
HTTP【网络】
|
1天前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
20 13
|
1天前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
2月前
|
数据采集 JSON API
🎓Python网络请求新手指南:requests库带你轻松玩转HTTP协议
本文介绍Python网络编程中不可或缺的HTTP协议基础,并以requests库为例,详细讲解如何执行GET与POST请求、处理响应及自定义请求头等操作。通过简洁易懂的代码示例,帮助初学者快速掌握网络爬虫与API开发所需的关键技能。无论是安装配置还是会话管理,requests库均提供了强大而直观的接口,助力读者轻松应对各类网络编程任务。
112 3
|
2月前
|
机器学习/深度学习 JSON API
HTTP协议实战演练场:Python requests库助你成为网络数据抓取大师
在数据驱动的时代,网络数据抓取对于数据分析、机器学习等至关重要。HTTP协议作为互联网通信的基石,其重要性不言而喻。Python的`requests`库凭借简洁的API和强大的功能,成为网络数据抓取的利器。本文将通过实战演练展示如何使用`requests`库进行数据抓取,包括发送GET/POST请求、处理JSON响应及添加自定义请求头等。首先,请确保已安装`requests`库,可通过`pip install requests`进行安装。接下来,我们将逐一介绍如何利用`requests`库探索网络世界,助你成为数据抓取大师。在实践过程中,务必遵守相关法律法规和网站使用条款,做到技术与道德并重。
46 2
|
2月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
106 1
|
27天前
|
Linux 开发工具 C语言
【c++】c++发送http请求
【c++】c++发送http请求
|
29天前
|
存储 监控 NoSQL
Redis的实现二: c、c++的网络通信编程技术,让服务器处理多个client
本文讨论了在C/C++中实现服务器处理多个客户端的技术,重点介绍了事件循环和非阻塞IO的概念,以及如何在Linux上使用epoll来高效地监控和管理多个文件描述符。
23 0
|
30天前
|
JSON API 开发者
深入解析Python网络编程与Web开发:urllib、requests和http模块的功能、用法及在构建现代网络应用中的关键作用
深入解析Python网络编程与Web开发:urllib、requests和http模块的功能、用法及在构建现代网络应用中的关键作用
14 0