前提概要
众所周知,http/https是当下开发应用程序时,网路部分不可或缺的部分,我们可以基于socket自己来实现,因为http/https本身是基于TCP实现的应用层协议(位于网络模型的第7层)。但随着行业的发展,https加密、业内非标准http协议的推广(CDN非标准协议)等这些部分,都需要耗费大量的开发成本,基于socket自己实现http/https的方案,成本上已经难以接受,选择开源的成熟方案是当下业内的共识。而curl是http/https最成熟的开源方案,其兼顾稳定性和易用性、跨平台性,是作为底层库的首选。当然其他一体化底层解决方案也是不错的选择,例如Mars(微信开源框架)、Qt等,这里我们仅在单一http/https方案这一选择中来做探讨。
curl虽然易于使用,但在各平台编译上,有不少晦涩难懂的地方,也是它对于使用者来说的障碍,这篇文章旨在消除这些障碍,拨开云雾,一站式解决各平台编译问题,从而将大家宝贵的精力从中抽出,用在更有价值的事情上。
参考资料
https://curl.haxx.se/docs/install.html
http://p-nand-q.com/programming/windows/building_openssl_with_visual_studio_2013.html
https://wiki.openssl.org/index.php/Main_Page
基本脉络
这里是我个人理解下来,需要大家提前搞懂的几个点,整理出脉络图,以便理解。和代码阅读类似,我们先观其行,然后再达其意,有利于各个击破,如果能接触到一两个有意思的技术历史,那也不失为过程中的风景,本篇文章也会按照各平台来逐一介绍和阐述。
整体工程
请务必下载下来如下链接中整体curl编译工程,然后再针对性阅读后续介绍
百度云盘:https://pan.baidu.com/s/1yXdqiUMBiHqeyVktlFUQ9A
腾讯微云:https://share.weiyun.com/5t9cdnJ
针对整体工程,我们分如下几部分做介绍:
1.mac/ios编译
2.windows编译
3.android编译
4.openssl多线程安全
工程目录结构如下:
一、Mac编译
主要参考curl官方文档:
https://curl.haxx.se/docs/install.html
iOS的编译,这里我稍作说明,由于个人精力的缘故,没有完整实践过,之前看官方文档的时候,大致看到方法应该是类似的,由于移动平台CPU多种多样,这里是否有编译上的差异,我还未做考证。总之,再麻烦总不会麻烦过Android(文章后半段大家会感觉到这一点),请大家阅读好官方文档,我们要做的大多数情况下只是保持好正确的坐姿,设置好编译参数,然后正确调用编译命令。
注意事项
需要更新到xcode9.4.1以上版本,curl-7.63.0版本在xcode9.2.1版本编译会报如下错误:
Undefined symbols for architecture x86_64:
“_SSLCopyALPNProtocols”, referenced from:
_darwinssl_connect_step2 in libcurl.a(libcurl_la-darwinssl.o)
“_SSLSetALPNProtocols”, referenced from:
_darwinssl_connect_common in libcurl.a(libcurl_la-darwinssl.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]:[curl] Error 1
make[1]:[install-recursive] Error 1
make: [install-recursive] Error 1
编译脚本build/libcurl/build_for_mac.sh执行的步骤:
1.解压源码:curl-7.63.0.tar.gz
2.编译libcurl
关键代码(限于篇幅只贴出部分脚本):其中current_path是当前脚本执行路径,是编译输出路径,也可以配置为自定义的输出路径
export MACOSX_DEPLOYMENT_TARGET="10.6"
# buid configure
./buildconf
./configure --prefix=$current_path/out \
--disable-shared \
--enable-static \
--with-darwinssl \
--enable-threaded-resolver \
--disable-ldap \
--disable-ldaps
# workaround still works though: make CFLAGS=-Wno-error for buid bug before v7.55.1
# the build error is:connectx' is only available on macOS 10.11 or newer
#make CFLAGS=-Wno-error
make
# install
make install
在win/linux/android下使用openssl,在mac/ios下用apple体系下ssl实现(Apple's SSL/TLS implementation)
通过指定编译参数来指明:--with-darwinssl, remark: it is not necessary to use the option --without-ssl
二、Windows编译
windows上libcurl的编译,可以参考libcurl源码下
winbuild/BUILD.WINDOWS.txt
准备环境:
1.安装ActivePerl:官网下载http://www.activestate.com/activeperl/downloads
2.安装nasm:官网下载http://www.nasm.us,附件中(build/build_depend_tools)也有安装包,并在系统环境变量中添加nasm安装路径(也可以使用附件中包含的批处理文件添加
3.安装python:确认系统环境变量中是否已自动添加python路径,若没有手动添加
1.编译入口
编译脚本build/build_for_win.bat执行步骤:
1.设置7zip环境变量,用于解压源码(整体工程压缩包中,带有7zip,curl/7-Zip路径下)
2.编译openssl
3.编译libcurl
4.删除openssl临时生成文件
5.删除libcurl临时文件
关键代码(限于篇幅只贴出部分脚本):
@echo off
@set workdir=%cd%
@set sevenzip=%workdir%\7-Zip\7z.exe
:: build openssl
@cd %workdir%\openssl
@call build_for_win.bat %sevenzip%
@cd %workdir%
:: build libcurl
@cd %workdir%\libcurl
@call build_for_win.bat %sevenzip%
@cd %workdir%
:: delete openssl temp files
@if exist %workdir%\openssl\openssl-1.0.2l (rd /s /q "%workdir%\openssl\openssl-1.0.2l") else echo can not find openssl-1.0.2l dir
@if exist %workdir%\openssl\output_lib (rd /s /q "%workdir%\openssl\output_lib") else echo can not find output_lib dir
:: delete libcurl temp files
@if exist %workdir%\libcurl\curl-7.63.0 (rd /s /q "%workdir%\libcurl\curl-7.63.0") else echo can not find curl-7.63 dir
@echo on
2.首先编译openssl
编译脚本build/openssl/build_for_win.bat执行步骤:
1.设置VC环境变量(这里使用的是VS2015,可以根据自身需要自定义修改)
2.解压源码:openssl-1.0.2l.tar.gz
3.设置输出目录
4.代码工程相关设置
5.将/MD设置为/MT模式
这一步根据自己工程的需要来做,如果应用程序其他模块都是/MD模式,则不需要执行这一步
另外,由于openssl中没有提供脚本选项来自动生成/MT工程,所以只能替换生成的.mak中对应选项
6.编译
7.同步生成文件到目标路径,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径
8.同步生成的.pdb文件目标路径,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径
由于openssl安装脚本中没有提供pdb文件安装选项,所以这里需要额外从openssl临时生成路径下拷贝出来
关键代码(限于篇幅只贴出部分脚本):
:: 3)generate VC project config
@perl configure VC-WIN32 --prefix=%outputlib%
@call ms\do_ms.bat
@call ms\do_nasm.bat
:: 4)replace "/MD" to "/MT" in ms/ntdll.mak
@setlocal enabledelayedexpansion
@set ntdll_mak_file=%currentPath%\openssl-1.0.2l\ms\ntdll.mak
@set ntdll_mak_file_temp=%currentPath%\openssl-1.0.2l\ms\ntdll_temp.mak if exist %ntdll_mak_file_temp% (del %ntdll_mak_file_temp%) else echo create temp file ntdll_temp.mak"
for /f "delims=" %%i in (%ntdll_mak_file%) do (
set str=%%i
set str=!str:/MD=/MT!
echo !str!>>%ntdll_mak_file_temp% )
@move /y "%ntdll_mak_file_temp%" "%ntdll_mak_file%"
@endlocal enabledelayedexpansion
:: 5)build
@nmake -f ms\ntdll.mak
@nmake -f ms\ntdll.mak install @cd %currentPath%
3.然后设置openssl依赖,编译curl
脚本build/libcurl/build_for_win.bat执行步骤:
1.设置VC环境变量(这里使用的是VS2015,可以根据自身需要自定义修改)
2.解压源码:curl-7.63.0.tar.gz
3.支持Windows XP版本
VS2010以后,XP系统需要单独设置才能支持,若不需要,可以在curl/build/build_for_win.bat中去掉“@call build_for_win.bat %sevenzip% enable_xp”中enable_xp参数即可
VS2015,推荐使用,兼容性好,工程中curl/build/build_for_win.bat编译脚本中默认开启了XP支持,这种情况下编译脚本自动化不会出错
VS2013,不推荐,curl源码中对自动化编译支持有兼容性问题,打开了XP支持参数后,VS2013需要手动编译才能编译通过,VS2013工程在curl/build/libcurl/curl-7.63.0/project/windows/VC12路径下
4.编译这里使用的是/MT模式,如果需要使用/MD模式,择修改“RTLIBCFG=static” 为 “RTLIBCFG=dll” RTLIBCFG=static,表示libcurl是/MT RTLIBCFG=dll,表示libcurl是/MD
5.同步生成文件到目标路径下,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径
关键代码(限于篇幅只贴出部分脚本):
:: 2) support Windows XP (add build command into “winBuild/MakefileBuild.vc”)
if "%supportXP%"=="enable_xp” (
echo modify "winbuild/MakefileBuild.vc" to support windows xp
python %currentPath%\build_for_win_support_xp.py %currentPath%\curl-7.63.0\winbuild\MakefileBuild.vc
)
:: 3)build
@cd %currentPath%\curl-7.63.0\winbuild
@nmake /f Makefile.vc WITH_DEVEL=../../../openssl/output_lib mode=dll VC=14 RTLIBCFG=static WITH_SSL=dll GEN_PDB=yes DEBUG=no MACHINE=x86
@cd %currentPath%
:: 4)sync build result to kernel
@set output=%currentPath%\curl-7.63.0\builds\libcurl-vc14-x86-release-dll-ssl-dll-ipv6-sspi
@copy /y "%output%\bin\libcurl.dll" "%currentPath%\..\..\..\..\output\bin"
@copy /y "%output%\lib\libcurl.lib" "%currentPath%\..\..\..\..\output\lib"
@copy /y "%output%\lib\libcurl.pdb" "%currentPath%\..\..\..\..\output\bin"
三、Android编译
https://wiki.openssl.org/index.php/Android
https://wiki.openssl.org/index.php/FIPS_Library_and_Android
google中搜索 “openssl” “android” “wiki”关键字,从而找到权威文档https://wiki.openssl.org/index.php/Android
关于NDK相关环境变量设置,参考权威文档中Setenv-android.sh
1.编译入口
脚本build/build_for_android.sh执行步骤:
1.声明环境变量:NDK、NDK根目录、NDK版本、CPU指令架构(arm/x86/…)
2.抽取NDK对应的CPU指令架构工具集(编译时需要用到)
3.编译openssl
4.编译libcurl
5.删除抽取出来的NDK指令架构工具集
6.同步生成文件到目标路径下,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径
NDK工具集抽取:
这里首先需要有一些技术上的概念理解,才能做到真正的掌握,技术上无惑于心太重要,毕竟我个人的目的也不仅仅是让大家去使用我提供的编译脚本,否则我没有必要赘述这篇文章了
主机编译:一般来说,大多数可执行程序的编译都是主机编译,例如,windows上VS编译windows程序,mac上xcode编译mac程序,都是主机编译。
交叉编译:与主机编译相对应,在其他系统上编译出目标系统上的可执行程序,例如,目前在Android机器上没有完备的编译开发环境,从而导致只能在其他系统上编译Android应用,这种就是典型的交叉编译。
在NDK之中,根据Android系统的CPU指令架构的不同,包含了能够实现Android应用程序交叉编译的各种工具集,以16b版本的NDK为例,其工具集目录是这样:
android-ndk-r16b
| - toolchains
| - arm-linux-androideabi-4.9
| - aarch64-linux-android-4.9
| - mipsel-linux-android-4.9
| - mips64el-linux-android-4.9
| - renderscript
| - x86-4.9
| - x86_64-4.9
| - llvm
一般来说,跨平台项目的编译,编译脚本中兼容性做得好的话,使用者是不需要关心当前应该使用哪一种交叉编译工具集的,但很可惜,openssl这里需要我们关心这一点,这也是当时我在处理这块时,一开始遇到障碍与困惑的地方,也花了不少时间,直到搞清楚了这些基本技术概念的来龙去脉后,才找到正确的方法。NDK这块,有太多的东西,这里我们需要做到怎样的程度呢,对于这种类似的问题,我个人一般秉持一个原则:“代码的编写,通透到底为好,而工具的使用,则是技术盲区的知识补充到刚好够用即可”,毕竟人的精力有限,兴趣之上就看个人了。最后,言归正传,NDK本身提供了比较完善的工具集抽取命令,我们这里的编译脚本中也是简单调用,而后抽取出来的工具集路径在openssl编译的脚本中正确设置给环境变量即可。
关键代码(限于篇幅只贴出部分脚本):
echo 抽取NDK指令集目录
ndk_toolchain_dir="$work_dir/ndk_toolchain"
rm -rf $ndk_toolchain_dir
$ndk_dir/build/tools/make_standalone_toolchain.py --arch $target_cpu --api $ndk_ver --stl gnustl --install-dir=$ndk_toolchain_dir --force
echo 编译openssl
bash $work_dir/openssl/build_for_android.sh $ndk_root $ndk_toolchain_dir $target_cpu
echo 编译libcurl
bash $work_dir/libcurl/build_for_android.sh $ndk_toolchain_dir $work_dir/openssl $target_cpu
echo 删除NDK临时目录$ndk_toolchain_dir
rm -rf $ndk_toolchain_dir
echo 同步libcurl和openssl头文件
cp $work_dir/libcurl/out/$target_cpu/include/curl/*.h $work_dir/../
cp $work_dir/openssl/out/$target_cpu/include/openssl/*.h $work_dir/../openssl
2.首先编译openssl
脚本build/openssl/build_for_android.sh执行步骤:
1.设置NDK相关环境变量,内部编译时会用到,
这部分主要参考https://wiki.openssl.org/index.php/Android中的Setenv-android.sh
2.编译
关键代码(限于篇幅只贴出部分脚本):
arch_target=arch-x86
if [ $target_cpu == "x86" ]; then
arch_target=android-x86
fi
if [ $target_cpu == "arm64" ]; then
arch_target=android-armv7
fi
if [ $target_cpu == "arm" ]; then
arch_target=android-armv7
fi
./Configure $arch_target no-shared no-comp no-hw no-engine --prefix=$ssl_path --openssldir=$ssl_path --sysroot=$CROSS_SYSROOT -D__ANDROID_API__=18 -isystem$ANDROID_SYSTEM
if [ $? != 0 ]; then
exit 1
fi
make depend
if [ $? != 0 ]; then
exit 1
fi
make all
if [ $? != 0 ]; then
exit 1
fi
make install_sw
if [ $? != 0 ]; then
exit 1
fi
3.然后设置openssl依赖后,编译curl
脚本build/libcurl/build_for_android.sh执行步骤:
1.解压源码:curl-7.63.0.tar.gz
1.设置NDK工具集目录(入口build/build_for_android.sh编译脚本中抽取的NDK对应的CPU指令架构工具集)
2.设置openssl输出目录(依赖openssl)
3.设置目标机器指令集
4.编译
关键代码(限于篇幅只贴出部分脚本):
# 自己的android-toolchain(NDK针对特定配置抽取出来的独立目录)
export ANDROID_HOME=$ndk_toolchain_dir
# openssl的输出目录
export CFLAGS="-isystem$openssl_dir/out/$target_cpu/include"
export LDFLAGS="-L$openssl_dir/out/$target_cpu/lib"
export TOOLCHAIN=$ANDROID_HOME/bin
# 设置目标机器指令集
arch_flags="-march=i686 -msse3 -mstackrealign -mfpmath=sse"
arch=arch-x86
tool_target=i686-linux-android
host_os=i686-linux-android
if [ $target_cpu == "x86" ]; then
arch_flags="-march=i686 -msse3 -mstackrealign -mfpmath=sse"
arch=arch-x86
tool_target=i686-linux-android
host_os=i686-linux-android
fi
if [ $target_cpu == "arm" ]; then
arch_flags="-march=armv7-a -msse3 -mstackrealign -mfpmath=sse"
arch=arch-arm
tool_target=arm-linux-androideabi
host_os=arm-androideabi-linux
fi
if [ $target_cpu == "arm64" ]; then
arch_flags="-march=armv8 -msse3 -mstackrealign -mfpmath=sse"
arch=arch-arm
tool_target=arm-linux-androideabi
host_os=arm-androideabi-linux
fi
echo 当前CPU指令集匹配arch为"$arch",arch_flags为$arch_flags
export TOOL=$tool_target
export ARCH_FLAGS=$arch_flags
export ARCH=$arch
export CC=$TOOLCHAIN/$TOOL-gcc
export CXX=$TOOLCHAIN/${TOOL}-g++
export LINK=${CXX}
export LD=$TOOLCHAIN/${TOOL}-ld
export AR=$TOOLCHAIN/${TOOL}-ar
export RANLIB=$TOOLCHAIN/${TOOL}-ranlib
export STRIP=$TOOLCHAIN/${TOOL}-strip
export CPPFLAGS="-DANDROID -D__ANDROID_API__=18"
export LIBS="-lssl -lcrypto"
export CROSS_SYSROOT=$TOOLCHAIN/sysroot
cd $source_dir
./configure --prefix=$current_path/out/$target_cpu \
--exec-prefix=$current_path/out/$target_cpu \
--bindir=$TOOLCHAIN \
--sbindir=$TOOLCHAIN \
--libexecdir=$TOOLCHAIN \
--with-sysroot=$CROSS_SYSROOT \
--host=$host_os \
--enable-ipv6 \
--enable-threaded-resolver \
--disable-dict \
--disable-gopher \
--disable-ldap \
--disable-ldaps \
--disable-manual \
--disable-pop3 \
--disable-smtp \
--disable-imap \
--disable-rtsp \
--disable-smb \
--disable-telnet \
--disable-verbose
make install
四、openssl多线程安全
openssl在多线程这块,有些历史因素,导致不同版本应用层需要做的事情不一样:
参考文档:https://www.openssl.org/blog/blog/2017/02/21/threads/
1.v1.0.2和之前的版本,多线程安全需要应用层自己实现,在openssl/crypto.h中有预留实现接口位置
文档中示例程序th-lock.c有做说明
2.v1.1.0版本之后,多线程安全加锁的实现,从运行时转到了编译期间
编译时启用多线程安全参数,则openssl会将各平台加锁实现打包进来
我们这里使用的版本是v1.0.2l版本,所以多线程安全部分需要应用层自己实现,虽然如此,openssl其实已经做了很多,我们只需要实现特定的几个位置的代码即可,具体代码如下:
加锁实现部分:
#include "openssl/crypto.h"
#include "openssl/err.h"
#if defined(WIN32)
#define MUTEX_TYPE HANDLE
#define MUTEX_SETUP(x) (x) = CreateMutex(NULL, FALSE, NULL)
#define MUTEX_CLEANUP(x) CloseHandle(x)
#define MUTEX_LOCK(x) WaitForSingleObject((x), INFINITE)
#define MUTEX_UNLOCK(x) ReleaseMutex(x)
#define THREAD_ID GetCurrentThreadId()
#else
#include
#define MUTEX_TYPE pthread_mutex_t
#define MUTEX_SETUP(x) pthread_mutex_init(&(x), NULL)
#define MUTEX_CLEANUP(x) pthread_mutex_destroy(&(x))
#define MUTEX_LOCK(x) pthread_mutex_lock(&(x))
#define MUTEX_UNLOCK(x) pthread_mutex_unlock(&(x))
#define THREAD_ID pthread_self()
#endif
static MUTEX_TYPE *mutexArray = NULL;
static int32_t nNumLocks = 0;
static void locking_function(int mode, int n, const char * file, int line)
{
if (n >= nNumLocks) {
return;
}
if (mode & CRYPTO_LOCK) {
MUTEX_LOCK(mutexArray[n]);
} else {
MUTEX_UNLOCK(mutexArray[n]);
}
}
static unsigned long threadId_function(void)
{
return ((unsigned long)THREAD_ID);
}
namespace OpenSSLThreadLock
{
void OpenSSLLock_Setup(void)
{
nNumLocks = CRYPTO_num_locks();
#ifdef _MSC_VER
mutexArray = (MUTEX_TYPE*)OPENSSL_malloc(nNumLocks * sizeof(MUTEX_TYPE));
#else
mutexArray = (MUTEX_TYPE*)malloc(nNumLocks * sizeof(MUTEX_TYPE));
#endif
if (!mutexArray) {
return;
}
for (int32_t i = 0; i < nNumLocks; ++i) {
MUTEX_SETUP(mutexArray[i]);
}
CRYPTO_set_id_callback(threadId_function);
CRYPTO_set_locking_callback(locking_function);
}
void OpenSSLLock_Cleanup(void)
{
if (!mutexArray) {
return;
}
CRYPTO_set_id_callback(NULL);
CRYPTO_set_locking_callback(NULL);
for (int32_t i = 0; i < CRYPTO_num_locks(); ++i) {
MUTEX_CLEANUP(mutexArray[i]);
}
#ifdef _MSC_VER
OPENSSL_free(mutexArray);
#else
free(mutexArray);
#endif
mutexArray = NULL;
}
}
调用部分则比较简单,只需要在libcurl模块整体初始化和退出调用对应接口即可
1.http模块初始化时
OpenSSLThreadLock::OpenSSLLock_Setup();
2.http模块退出时
// curl_global_cleanup();之前调用
OpenSSLThreadLock::OpenSSLLock_Cleanup();