跨平台:libcurl+openssl编译(Mac、Android、Windows、MD/MT模式、XP)

前提概要

众所周知,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

基本脉络

这里是我个人理解下来,需要大家提前搞懂的几个点,整理出脉络图,以便理解。和代码阅读类似,我们先观其行,然后再达其意,有利于各个击破,如果能接触到一两个有意思的技术历史,那也不失为过程中的风景,本篇文章也会按照各平台来逐一介绍和阐述。

跨平台:libcurl+openssl编译(Mac、Android、Windows、MD/MT模式、XP)_第1张图片

整体工程

请务必下载下来如下链接中整体curl编译工程,然后再针对性阅读后续介绍

百度云盘:https://pan.baidu.com/s/1yXdqiUMBiHqeyVktlFUQ9A

腾讯微云:https://share.weiyun.com/5t9cdnJ

针对整体工程,我们分如下几部分做介绍:

1.mac/ios编译

2.windows编译

3.android编译

4.openssl多线程安全


工程目录结构如下:

跨平台:libcurl+openssl编译(Mac、Android、Windows、MD/MT模式、XP)_第2张图片


一、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();

你可能感兴趣的:(跨平台:libcurl+openssl编译(Mac、Android、Windows、MD/MT模式、XP))