Android C++ series: Send Http network requests in JNI

Get into the habit of writing together! This is the second day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

1. Background

In the previous article on Linux network programming, there is an example of sending http requests in jni. This article implements the function of sending http network requests based on the libcurl library.

image-20220211111024046.png

2. Introduction to libcurl library

libcurl is a free and easy-to-use client-side URL transfer library supporting 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 and TFTP. libcurl supports SSL certificates, HTTP POST, HTTP PUT, FTP upload, HTTP form upload, proxy, HTTP/2, HTTP/3, cookies, user+password authentication (basic, digest, NTLM, negotiation, Kerberos), file transfer recovery, HTTP proxy tunneling and more!

libcurl is highly portable, it builds and works on many platforms including Solaris, NetBSD, FreeBSD, OpenBSD, Darwin, HPUX, IRIX, AIX, Tru64, Linux, UnixWare, HURD, Windows, Amiga, OS/2, BeOs , Mac OS X, Ultrix, QNX, OpenVMS, RISC OS, Novell NetWare, DOS, etc.

libcurl is free, thread-safe, IPv6-compatible, feature-rich, well-documented, fast, and well-documented. It has been used by many well-known and big companies.

Official documentation: curl.se/libcurl/

3. libcurl library compilation

3.1 Compile 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请求示例。

Guess you like

Origin juejin.im/post/7082376728756092936