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兼容的,特性丰富,有着良好,快速,充分的文档,已经被许多知名的,很多大厂都在使用。
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 -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。
官方文档: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包含以下内容:
- age:age of the returned struct
- version:LIBCURL_VERSION
- version_num:LIBCURL_VERSION_NUM
- host:OS/host/cpu/machine when configured
- features:bitmask
- ssl_version:human readable string
- 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请求示例。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。