前提概要
眾所周知,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
基本脈絡
這里是我個人理解下來,需要大家提前搞懂的幾個點,整理出脈絡圖,以便理解。和代碼閱讀類似,我們先觀其行,然后再達其意,有利于各個擊破,如果能接觸到一兩個有意思的技術歷史,那也不失為過程中的風景,本篇文章也會按照各平臺來逐一介紹和闡述。
整體工程
請務必下載下來如下鏈接中整體curl編譯工程,然后再針對性閱讀后續介紹
針對整體工程,我們分如下幾部分做介紹:
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 <pthread.h>
#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();