说实话,标题里“不依赖boost的asio安装”这个需求,我太理解了。C++写网络程序,Asio几乎是绕不开的名字。但一到安装环节,很多人就被boost劝退:下载boos全套源码、跑bootstrap、跑b2、再等它编译完,少说半小时,多则一小时。关键是项目里往往只用到Asio那一小块,其他boost组件根本用不上,纯粹是拖着个大包袱。其实Asio从很早开始就提供了独立的standalone版本,完全可以不装boost,把asio当作一个普通的头文件库直接用。这篇就专门围绕这个场景,把下载、引入、编译、接入CMake的完整流程走一遍,同时把我在实际部署中踩过的坑、排查的思路一起写出来。内容面向刚接触Asio的C++开发者,也适合交叉编译、嵌入式这类讲究依赖干净的环境。
整个安装流程并不复杂,核心就三件事:下载独立源码、把include目录交给编译器、定义ASIO_STANDALONE宏。但这里面的版本选型、OpenSSL依赖、线程库链接,还有和boost旧环境打架的问题,如果不提前搞清楚,很容易进坑。我会按照一个实际项目的推进顺序来讲,从背景拆解一直写到问题排查。
1. 为什么asio可以不依赖boost
1.1 boost.asio与standalone版本的关系
很多人以为Asio必须是boost的一部分,这是一个历史误会。Asio的作者Christopher Kohlhoff在开发初期确实依托于boost库去开发,但很快就把核心实现抽离出来了,形成了两个发布渠道:一个是boost.asio,跟随boost的发布节奏;另一个就是独立的standalone asio,单独打包成tar.gz或zip,不依赖boost,直接给出头文件。
这两个版本在API设计上是同一套,代码风格、命名空间、文档结构几乎一致,所以平时教怎么用Asio的教程、代码示例,在standalone版本上基本能直接跑通。区别主要在底层实现:standalone版本大量使用C++11及以后的标准库组件,替代了boost里那些基础设施,比如用std::error_code替代boost::system::error_code,std::shared_ptr替代boost::shared_ptr,std::thread替代boost::thread。对于现代C++项目来说,这反而是个巨大的优点,标准库自己就可以支撑Asio运行。
一个常见的疑惑是:既然有两个版本,那我直接用boost.asio不就行了?从功能上讲没问题,但抛开编译成本不看,boost发行版里的asio版本更新非常迟缓。拿实际经历来说,boost 1.74里的asio还停留在某种旧版状态,而standalone版本早就迭代了好几轮,C++20协程支持、各种新API、较新的bug修复都集中在standalone仓库里。追求新特性和少依赖的话,独立版本明显更合适。
从我个人的迁移经验看,从boost.asio切到standalone asio,代码改动量其实很小,主要就是去掉对boost命名空间的依赖,把某些boost::的引用换成std::或直接使用asio自带的类型别名。比如boost::asio::ip::tcp在standalone里还是asio::ip::tcp这种形式,名字清晰很多。
1.2 选择独立asio的好处与适用场景
为什么一定要强调“不依赖boost安装”?单独一个asio头文件库明明可以用包管理器顺手解决,但现实里的痛点很具体。
首先是依赖树膨胀的问题。boost不是一个库,而是几十个库的集合。引入boost.asio时,即便只想要异步网络,实际依赖也可能牵扯出一串组件。项目编译时间上升、二进制体积变大、团队其他成员需要额外学习boost的使用习惯和管理方式。对于追求干净工程的人来说,这种隐形成本非常高。
其次是交叉编译和嵌入式环境。嵌入式设备的交叉编译工具链通常没有现成的boost,即使有,版本匹配也是个麻烦事。很多ARM Linux平台的SDK会自带老版本boost,但Asio要求的boost组件又可能不完整,最后只能自己交叉编译boost,过程相当痛苦。standalone asio是纯头文件,不涉及动态库、静态库的链接,只要把include目录拷贝到目标根文件系统里,交叉编译器带上-I参数就能用,大大简化了嵌入式项目的依赖管理。
还有一类常见场景是CI流水线。自动化构建环境里安装完整boost,对网络和磁盘都是压力。用standalone asio则只需要在CI脚本里下载源码包、解压、指定include路径,几秒钟就能完成,构建速度明显提升。
所以我的结论是:除非你的项目本来就大量使用boost,否则新项目完全没必要为了asio去引入boost。独立版本无论是上手门槛、构建速度、还是长期维护成本,都更贴合现代C++工程的诉求。
2. 安装前的准备:版本、环境与关键依赖
2.1 下载前先确认版本号与来源
下载asio standalone源码包时,有两个渠道是官方认可的:一个是作者维护的官网think-async.com,那里能看到Asio用户指南、常见问题,也提供各版本源码包下载;另一个是GitHub上的chriskohlhoff/asio仓库,除了源码本身还有issue跟踪和提交历史。从GitHub直接git clone也可以,但如果只是给项目引入,下载某个release快照的tar.gz会更干净,仓库里那些测试代码、文档、历史提交不会一起塞进vendor目录。
版本选择上我建议以官网列出的最新稳定版为准,不要为了稳妥去下载两三年前的版本。以我写这篇时的常见版本来看,1.30.x、1.28.x这类版本号都很常见,具体数字以你实际看到的为准。越新的版本,对C++20协程、国家对一些新标准库特性的利用就越充分,bug修复也更完整。要注意的是,部分老版本的standalone asio对编译器要求偏高,如果你用的是比较旧的gcc,新版本可能会因为语法特性不能编译。这时候再去下载一个次新版本,不要死磕最新的。
有一个常见误解是:asio没用到就别下载装它。实际上standalone asio只有头文件,不需要编译生成任何库文件,所以它在系统里的形态就是一个目录。你完全可以把它放进项目的third_party/asio目录,随项目一起走,这种vendor方式在团队协作中特别方便,也避免了每个人环境里的版本漂移问题。
2.2 确认Core之外的功能依赖:OpenSSL选装
standalone asio的核心网络功能是header-only的,不需要任何动态链接库。但一旦你用到SSL/TLS相关的接口,比如asio::ssl::stream,就会依赖OpenSSL。这不是Asio自身的问题,而是SSL功能本来就是封装OpenSSL来实现的。
换句话说,如果你的项目只需要TCP、UDP、串口、定时器等基础IO能力,那根本不用安装任何额外的库,下载源码包后直接引入头文件即可。如果你后续要用到SSL,那需要提前把OpenSSL的开发包装上。Linux下常见的命令是sudo apt install libssl-dev,这也是我为数不多推荐提前装的依赖,避免代码写到一半才发现缺了头文件。
不需要SSL功能的时候,还可以通过定义ASIO_NO_SSL来禁用所有SSL相关的头文件包含,这样编译器不会因为找不到openssl/ssl.h之类的东西而报错,对编译环境的依赖更小。这个宏在嵌入式等精简环境下非常有用,我后面排查章节会专门讲。
2.3 环境检查:编译器与CMake
因为standalone asio依赖C++11及以上特性,编译器版本不能太老。实际开发现象是,gcc 7或clang 5基本上就能编译C++11的代码,但如果想体验C++20协程支持,编译器必须支持Coroutines,这需要gcc 10以上或者clang 12以上,而且还需要一些移植性配置。
在Linux上,验证编译器很简单:
g++ --version cmake --version如果g++低于某个能接受的水平,建议先用包管理器升级编译器。Windows上我用得比较多的是MSVC,Visual Studio 2019及以后版本都能很好地编译standalone asio,要点是项目属性里设置C++语言标准为C++17或C++20。macOS上的clang也一直维护得不错,基本开箱即用。
CMake版本建议至少3.10以上,因为现在很多项目采用CMake的target-based方式管理依赖,用find_package(Threads)这类模块需要较新的CMake支持。如果你的工程还在用Makefile,也没问题,编译命令里手动加-I参数和-D宏就可以,具体命令我在下一步讲。
3. 实操安装:从源码包到能跑的工程
3.1 解压结构与include目录说明
假设你已经把asio源码包下载到了~/tools/asio-1.28.0.tar.gz,解压后看一下目录结构,会发现里面核心部分是include目录:
tar -zxvf asio-1.28.0.tar.gz -C ~/tools cd ~/tools/asio-1.28.0 ls include/正常情况下include/asio.hpp和include/asio目录就在那里。asio.hpp是整个库的主入口,所有公开API几乎都能从这个头文件引入。也就是说,你的编译器include路径只需要指向这个include目录,源码本身不需要任何编译阶段。
这里要说清楚一个细节:standalone asio没有传统意义上的“链接库”概念。Linux下不需要libasio.a或libasio.so,Windows下也不存在asio.lib。它就是纯header-only的库,存在形式等同于nlohmann/json这类现代头文件库。所以安装动作本质就是“把源码目录放到一个合适的位置 + 让编译器能看到”,术语可以叫“vendor”或“include-only”。
我习惯把独立版asio放进项目的third_party/asio目录,这样别人clone代码之后自带依赖,不用每台机器都跑一遍下载步骤。如果公司内部有镜像或制品库,也可以把tar.gz上传上去,安装时统一从镜像拉取,避免团队访问外网不畅导致构建失败。
3.2 写一段最小测试代码并编译
安装是否成功,最简单的方式是写一个能编译、能运行的测试程序。我一般会写一个最基础的TCP daytime客户端,逻辑简单但覆盖了核心头文件、核心API和线程库依赖。
#include <asio.hpp> #include <iostream> int main() { asio::io_context io; asio::ip::tcp::resolver resolver(io); asio::ip::tcp::socket socket(io); try { auto endpoints = resolver.resolve("time.nist.gov", "13"); asio::connect(socket, endpoints); for (;;) { char data[128]; std::error_code ec; size_t len = socket.read_some(asio::buffer(data), ec); if (ec == asio::error::eof) { break; } if (ec) { throw std::system_error(ec); } std::cout.write(data, len); } } catch (std::exception& e) { std::cerr << "error: " << e.what() << std::endl; } return 0; }这段代码依赖asio::io_context、resolver、socket、buffer、error_code这些基本类型,正好能测试standalone版本的完整可用性。编译命令如下(Linux + g++):
g++ -std=c++17 -DASIO_STANDALONE -I~/tools/asio-1.28.0/include test.cpp -lpthread -o test最关键的两个参数:
- -DASIO_STANDALONE:显式告诉asio进入standalone模式,不要试图找boost.asio头文件。
- -lpthread:Asio内部用到线程、互斥量等,在Linux上必须链接pthread库,否则编译能过但链接会报undefined reference。
编译成功后运行./test,正常情况下会连上时间服务器并打印一串时间报文。如果网络环境访问不了time.nist.gov,可以换成一个局域网内已知TCP服务,或者干脆只写一个定时器来验证Asio基本功能,我认为这一步能跑通就说明环境没问题了。
Windows下的编译命令稍有不同,MSVC的命令行或者Visual Studio工程里,只需要设置预处理宏ASIO_STANDALONE和include路径,不用特意链接pthread。具体步骤我写在后面问题排查表格里。
3.3 用CMake集成到项目
实际工程里很少手动敲g++命令,CMake集成是更普遍的方式。把standalone asio接入CMake有两种常见做法:直接include路径配合宏定义,或者用FetchContent自动下载。
直接include路径的方式适合团队已经规整了vendor目录的情况,CMakeLists.txt写起来非常简单:
cmake_minimum_required(VERSION 3.10) project(asio_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 路径按你的vendor目录调整 set(ASIO_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/asio/include) add_executable(test_app main.cpp) target_include_directories(test_app PRIVATE ${ASIO_INCLUDE_DIR}) target_compile_definitions(test_app PRIVATE ASIO_STANDALONE) # Linux上需要Threads find_package(Threads REQUIRED) target_link_libraries(test_app PRIVATE Threads::Threads)找Threads这个CMake模块再链接Threads::Threads,是Linux环境的标准做法,比直接写-lpthread更不容易踩平台切换的坑。因为Threads::Threads这个target在Linux上解析为pthread,在Windows上则是一个no-op,可以很好地跨平台。
如果想要全自动,可以用FetchContent从GitHub拉取指定版本的asio源码:
include(FetchContent) FetchContent_Declare( asio URL https://github.com/chriskohlhoff/asio/archive/refs/tags/asio-1-28-0.tar.gz ) FetchContent_MakeAvailable(asio) # 之后 target_include_directories 指向 asio_SOURCE_DIR/include但FetchContent方式有一个和网络相关的风险:一旦下载源不可达,整个configure阶段就会失败。对CI环境来说,我喜欢把tar.gz先缓存到制品库,再通过file(URL)方式引用内网地址,或者干脆走vendor目录的方案,综合来看更稳。这里没有绝对正确答案,按团队基础设施情况来选就行。
3.4 C++20协程快速体验(可选)
如果你是新项目且编译器支持C++20,那独立asio的coroutine支持值得体验一下。传统异步回调式的代码容易陷入回调地狱,用了协程后,异步代码看起来几乎像同步代码。
下面是一个简单的异步定时器,使用asio::co_spawn和awaitable:
#include <asio.hpp> #include <asio/experimental/awaitable_operators.hpp> #include <iostream> using asio::awaitable; using asio::co_spawn; using asio::detached; using asio::use_awaitable; awaitable<void> timer_demo(asio::io_context& io) { asio::steady_timer timer(io, std::chrono::seconds(1)); co_await timer.async_wait(use_awaitable); std::cout << "timer fired after 1s" << std::endl; } int main() { asio::io_context io; co_spawn(io, timer_demo(io), detached); io.run(); return 0; }编译时需要开启C++20,并且通常要定义ASIO_HAS_CO_AWAIT或明确指定ASIO_HAS_CO_AWAIT宏,具体看版本文档。这个体验例子只是为了证明独立asio在最新标准下体验不错,不是安装环节的必需项,但对老代码迁移到协程的评估很有意义。
4. 常见问题与排查实录
4.1 编译阶段:找不到asio.hpp或误入boost.asio
第一个最典型的报错是:
fatal error: asio.hpp: No such file or directory这种情况几乎都是include路径问题。检查编译命令或CMake里target_include_directories的路径是否真的指向了include目录,尤其注意不要把路径写成asio的上级目录。比如你解压在/home/user/asio-1.28.0,正确的include参数是-I/home/user/asio-1.28.0/include,而不是-I/home/user/asio-1.28.0。
另一种隐蔽的报错是:明明没有安装boost,但编译时出现boost相关头文件找不到的提示,比如:
fatal error: boost/system/error_code.hpp: No such file or directory出现这个的原因多半是你没有定义ASIO_STANDALONE。在这种情况下,standalone asio默认会回退到boost模式,尝试找boost.asio的头文件。解决办法很简单:把宏加上,优先在CMake里target_compile_definitions加,或者在源文件第一行写#define ASIO_STANDALONE,但考虑到多个源文件都会包含asio.hpp,宏分散容易遗漏,我还是推荐编译选项统一加。
还有一类情况更隐蔽:系统里同时存在boost.asio和standalone asio头文件。编译器寻找头文件时有搜索路径顺序,如果把boost的include目录和asio的include目录都交给了编译器,某些旧版本头文件可能会抢占路径,导致明明写了standalone版本却被封装成boost版本编译。这种情况下优先保证asm的include目录在boost include目录之前,并且显式定义ASIO_STANDALONE。
4.2 链接阶段:pthread相关undefined reference
编译通过但链接报错的场景非常经典,错误信息类似:
undefined reference to `pthread_create' undefined reference to `pthread_join'这是因为Asio内部启动线程、创建互斥量时,需要链接到pthread库。如果你使用g++手动编译,在命令末尾增加-lpthread即可。注意这个参数要放在源文件之后,链接器的符号解析顺序有时候会把参数顺序搞出问题,虽然现代gcc版本大多自动处理,但手动命令还是养成库参数放后面的习惯。
如果用了CMake,最佳实践是:
find_package(Threads REQUIRED) target_link_libraries(your_target PRIVATE Threads::Threads)而不是手写-lpthread,因为Threads模块在Windows上会直接变成空操作,Windows不需要额外线程链接。
还有一个高级问题:如果代码里使用了asio::experimental或者协程相关特性,某些老版本可能需要额外链接libstdc++fs之类的东西。我在gcc 9环境下遇到过一次,原因是std::filesystem的依赖不完整,但大部分asio项目不会踩这个点,只有在同时使用协程和文件系统时才会触发。遇到这个报错时,考虑升级编译器是最有效的做法。
4.3 Windows环境:MSVC与MinGW的差异
Windows上使用standalone asio,MSVC用户通常不需要特意链接winsock库,因为现代SDK已经把Ws2_32等系统库默认包含在工程设置里了。至少Visual Studio 2019的默认Windows SDK配置就能跑通。
但如果你使用的是MinGW-w64配合CMake,有时候需要显式链接ws2_32和mswsock:
target_link_libraries(your_target PRIVATE ws2_32 mswsock)否则可能出现类似:
undefined reference to `WSAStartup@8'这种链接错误其实是winsock基础函数没有找到,原因就是MinGW环境不会自动排入系统库。
Windows上还容易出现另一个问题:没有设置编译标准。Visual Studio默认语言标准可能停留在C++14,但新版asio可能要求C++17。建议项目属性里把C++语言标准明确设置为ISO C++17或更高,再进行编译。
4.4 交叉编译与嵌入式环境的坑
交叉编译场景下,standalone asio的header-only特性完胜boost,但不代表完全没有坑。
最常见的坑是pthread库的交叉链接。如果你的工具链是arm-linux-gnueabihf-g++,通常需要在CMake里用对应的toolchain file,并确保find_package(Threads)能找到目标平台的pthread库,而不是宿主机器的。一个写死在根文件系统里的libpthread.so路径容易造成链接错平台库的乌龙。我的建议是始终通过工具链配置文件指定CMAKE_FIND_ROOT_PATH,让CMake仅搜索目标系统根目录里的库。
另一个坑是OpenSSL的交叉编译。很多交叉编译环境没有目标平台的libssl.a和头文件,一旦你的代码引入了asio/ssl.hpp,整个编译马上报错。这时要么自己交叉编译OpenSSL,要么像前面说的,明确不需要SSL就定义ASIO_NO_SSL,从源码层面把SSL依赖屏蔽掉。嵌入式项目里我碰到过多次因为引用了一个SSL头文件导致整条构建链挂掉的,我现在都会在非SSL需求的工程里提前加上这个宏。
4.5 常见问题速查表
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| fatal error: asio.hpp: No such file | include路径配置错误 | 检查-I或CMake的include目录,确保指向源码包下的include文件夹 |
| boost相关头文件找不到 | 忘了定义ASIO_STANDALONE宏 | 编译选项或CMake定义中加上-DASIO_STANDALONE |
| undefined reference to pthread_* | Linux下没有链接线程库 | 手动编译加-lpthread;CMake用Threads::Threads |
| 找不到openssl/ssl.h | 使用了SSL功能但没有安装OpenSSL开发包 | 安装libssl-dev,或定义ASIO_NO_SSL禁用SSL头文件 |
| 交叉编译链接到宿主pthread | CMake搜索路径指向了宿主机 | 配置CMAKE_FIND_ROOT_PATH指向目标平台根文件系统 |
| WSAStartup等winsock链接错误 | MinGW环境未链接WinSock库 | 添加ws2_32、mswsock到链接列表 |
| 协程功能编译不通过 | 编译器不支持C++20协程或未开启标准 | 使用gcc 10+并设置CMAKE_CXX_STANDARD 20 |
| 旧boost.asio头文件抢占路径 | 编译器同时搜索了boost和asio头文件 | 调整include搜索顺序,确保asio在前,并加ASIO_STANDALONE |
四级排查这些问题的思路,其实核心就是一条:先确认宏,再看include路径,最后查链接库。顺序对的话,绝大多数安装问题能很快定位。
4.6 尽量少用包管理器安装asio
我见过很多朋友习惯用Homebrew、apt或vcpkg一把梭安装asio,虽然快速,但我不太推荐把这个作为主要安装方式。原因有两个:一是包管理器里的asio版本往往落后于官方发布,有些发行版仓库里的asio还被打包成boost依赖形态,装了之后反而跟你项目里已有的boost版本产生混乱;二是包管理器安装的头文件位置分散在系统目录,移植到别的机器时,依赖环境不透明,反而不如把stdio源码vendor进项目里来得可控。
如果实在要用包管理器,比如Deiban/Ubuntu下apt install libasio-dev,建议装完后检查一下头文件版本,再写个测试编译确认它是否真正独立了boost。很多环境下你会发现它确实独立了,毕竟这么多年社区推动下来,主流发行版已经默认打包standalone形态,但版本号依然可能比较旧。大概率够用,只是别追求最新特性。
个人使用心得与收尾
我自己从boost.asio迁移到standalone asio,算起来也有几年时间了。最大的感受是,asio这个库的设计本来就不应该和boost绑定得那么死。它真正需要的不过是标准库、一个线程库和一个异步事件循环,这些在现代C++里都已经非常成熟。只要你把ASIO_STANDALONE这个宏记住,把include目录指对,整个安装过程就会变得异常清爽。
如果后续要继续扩展,我建议你维护一个vendor/asio目录,把它作为项目的一部分提交到代码库。这样新同事、CI、自动化测试拉取代码后不需要任何额外安装步骤,直接configure和build。对这个目录的更新不要随意,每次锁定一个版本号,用release tag命名目录,方便回滚。顺便可以说一句,要是哪天项目真要上协程,独立asio对C++20的支持会让你觉得当初这个选择特别值。