WebRTC C++开发实战:从环境搭建到点对点通信示例

1. 项目概述:为什么你需要一个WebRTC C++示例项目?

如果你正在搜索“WebRTC C++ 示例项目”,大概率是遇到了和我当初一样的困境:官方文档庞大而分散,网上教程要么是JavaScript的天下,要么就是一些零碎的代码片段,根本拼凑不出一个能跑起来的、完整的C++项目。WebRTC作为实时音视频通信的基石,其C++ API功能最强大、最底层,但学习曲线也最陡峭。你可能想开发一个高性能的桌面端音视频应用,或者想深入理解WebRTC的内部机制,甚至是想将WebRTC集成到现有的C++服务端架构中。无论你的目标是什么,一个清晰、完整、可编译运行的C++示例项目,都是你从理论走向实践最关键的那块敲门砖。

我花了相当长的时间,在官方源码、社区项目和无数踩坑经验中,整理并验证了一套真正能跑通的WebRTC C++示例项目指南。这个指南的核心价值在于“亲测免费”和“完整闭环”。它不依赖于任何付费服务或特定平台,所有工具链都是开源免费的;更重要的是,它不是一个孤立的代码文件,而是从环境搭建、项目配置、代码编写、编译运行到问题排查的完整流程。你将看到的不只是“Hello World”,而是一个能够建立点对点连接、传输音视频数据的微型但功能完备的通信demo。接下来,我会带你一步步拆解这个过程中的每一个核心环节,分享那些官方文档里不会写的配置细节和避坑技巧。

2. 环境准备与工具链搭建:避开第一个大坑

开始写代码之前,90%的失败都源于环境没配好。WebRTC C++开发的环境搭建是个系统工程,需要耐心和精准的操作。

2.1 核心依赖安装:不仅仅是Visual C++

在Windows平台上,最常见的错误就是“error MSB3428: 未能加载 Visual C++ 组件 ‘vcbuild.exe’”。这个错误通常发生在通过npm安装某些依赖(如node-sass)或编译一些老项目时,但它提醒我们一个核心事实:完整的C++构建工具链是基础。

1. 安装Visual Studio Build Tools 或 Visual Studio IDE:

  • 推荐方案:直接安装Visual Studio 2022 Community版(免费)。安装时,在“工作负载”中必须勾选“使用C++的桌面开发”。这个选项会自动安装MSVC编译器、Windows SDK以及最关键的基础库和构建工具。这是最一劳永逸的方法。
  • 轻量方案:如果你不想安装完整的IDE,可以下载Visual Studio Build Tools。同样,在安装器中选择“C++ 生成工具”工作负载。这能提供编译所需的最小环境。
  • 重要提示:确保安装的Windows SDK版本与你的系统兼容。通常选择最新稳定版即可。安装完成后,建议从“开始菜单”的“Visual Studio”文件夹中,打开“x64 Native Tools Command Prompt for VS 2022”或“Developer Command Prompt for VS 2022”来进行后续的编译操作,这个命令行环境已经配置好了所有必要的路径变量。

2. 安装Python和Git:WebRTC的源码管理和编译脚本严重依赖Python和Git。

  • Python:前往python.org下载3.8+版本(WebRTC官方推荐)。安装时务必勾选“Add Python to PATH”,将Python添加到系统环境变量。
  • Git:前往git-scm.com下载安装。这是拉取WebRTC庞大源码库的必备工具。

3. 安装Depot Tools:这是Google用于管理Chromium及其相关项目(包括WebRTC)源码的一套工具脚本,是获取WebRTC源码的唯一官方指定方式

  • 在你的工作目录(例如D:\webrtc)打开命令行。
  • 克隆depot_tools仓库:
    git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  • depot_tools文件夹的路径(例如D:\webrtc\depot_tools添加到系统环境变量PATH的最前面。这是关键步骤,必须确保它在其他可能冲突的工具(如旧版Python)之前。

实操心得:环境变量配置后,务必关闭所有命令行窗口并重新打开,让新的PATH生效。很多“命令找不到”的问题都是因为没重启终端。

2.2 获取WebRTC源码:一场耐心与网速的较量

WebRTC的源码仓库巨大(超过20GB),获取过程需要稳定的网络和较长的时间。

  1. 在你准备存放源码的目录(如D:\webrtc)下,打开配置好环境变量的命令行。
  2. 设置一个环境变量,避免在同步过程中使用过大的缓存(可选但推荐):
    set DEPOT_TOOLS_WIN_TOOLCHAIN=0
  3. 创建并进入一个专门存放源码的文件夹,然后开始获取代码:
    mkdir src cd src fetch --nohooks webrtc
    这个fetch命令会初始化仓库并开始同步代码。这个过程会下载数十GB的数据,请保持网络通畅,可能需要数小时。
  4. 同步完成后,进入源码目录,并切换到某个稳定的发布分支(比如M分支,相对稳定):
    cd src git checkout branch-heads/m122 # 示例:切换到M122分支,你可以查看当前最新分支 gclient sync
    gclient sync命令会同步所有依赖的第三方库,这也是一个漫长的过程。

避坑指南:同步过程极易因网络问题中断。如果中断,重新执行gclient sync即可,它会断点续传。建议在夜间或网络稳定的时段进行此操作。另外,确保磁盘有充足的剩余空间(建议至少40GB)。

3. 项目结构与编译配置解析

获取到源码后,我们先不急着写自己的代码,而是理解官方示例的结构和编译系统。WebRTC使用GN (Generate Ninja)作为元构建系统,用Ninja作为实际的构建工具。这套工具链效率极高,但配置方式与CMake、Makefile不同。

3.1 源码目录关键结构

src目录下,你需要关注以下几个关键路径:

  • src/:根目录,包含顶层的.gn配置文件。
  • src/examples/宝藏目录!这里存放着官方的示例程序,包括我们最关心的peerconnection示例。
    • src/examples/peerconnection/:点对点连接的核心示例,包含客户端 (client) 和服务器端 (server) 的代码。这是我们学习和改造的基础。
  • src/out/:编译输出目录。我们后续会在这里创建自己的构建配置。
  • src/third_party/:所有第三方依赖库,如abseil-cpp, libyuv, opus, ffmpeg等。WebRTC已经帮你管理好了。

3.2 创建并配置你自己的构建目录

我们不直接修改官方示例的编译配置,而是创建自己的构建目标,这样更清晰,也便于管理。

  1. 生成默认编译配置:src目录下,运行以下命令生成一个针对当前平台的默认Release构建配置。

    gn gen out/Default --args='is_debug=false target_cpu="x64"'
    • out/Default:指定输出目录。
    • --args:传递构建参数。
      • is_debug=false:生成Release版本,优化性能,减小体积。
      • target_cpu="x64":目标CPU架构为64位。如果你的系统是ARM或需要32位,请相应修改。
  2. 编译官方示例进行验证:使用Ninja编译peerconnection示例中的客户端。

    ninja -C out/Default peerconnection_client

    如果一切顺利,编译完成后,你会在out/Default目录下找到peerconnection_client.exe。运行它,你会看到一个简单的GUI客户端。不过,它需要配套的信号服务器 (peerconnection_server) 才能工作。这个步骤主要是验证你的编译环境完全正确。

注意事项:第一次编译会非常耗时(可能超过1小时),因为需要编译WebRTC核心库及所有依赖。请耐心等待。编译成功后,后续增量编译会快很多。

4. 从零构建一个极简的C++ WebRTC示例

现在,我们抛开复杂的GUI客户端,创建一个最纯粹的命令行示例,专注于理解WebRTC C++ API的核心调用流程。我们将创建一个仅建立对等连接、交换SDP(会话描述协议)和ICE(交互式连接建立)候选者的程序。为了简化,我们假设两个对等端在同一进程内通过内存交换信令(实际应用中,信令需要通过服务器中转)。

4.1 创建项目文件与BUILD.gn配置

  1. 创建示例目录和文件:src目录下,创建一个属于自己的示例目录,例如src/mysimplepeer

    cd src mkdir mysimplepeer cd mysimplepeer
  2. 创建主CPP文件 (mysimplepeer.cc):

    // mysimplepeer.cc #include <iostream> #include <memory> #include <thread> #include <atomic> #include "api/peer_connection_interface.h" #include "api/create_peerconnection_factory.h" #include "rtc_base/ssl_adapter.h" #include "rtc_base/thread.h" #include "rtc_base/logging.h" class SimplePeerConnectionObserver : public webrtc::PeerConnectionObserver { public: // 当ICE候选者收集到时触发 void OnIceCandidate(const webrtc::IceCandidateInterface* candidate) override { std::string sdp; if (candidate->ToString(&sdp)) { std::cout << "[本地候选者] " << sdp << std::endl; // 在实际应用中,这里需要通过信令服务器发送给远端 // 本例中,我们简单打印出来,假设手动交换 } } // 当连接状态改变时触发 void OnConnectionChange(webrtc::PeerConnectionInterface::PeerConnectionState new_state) override { const char* state_str = ""; switch (new_state) { case webrtc::PeerConnectionInterface::PeerConnectionState::kNew: state_str = "New"; break; case webrtc::PeerConnectionInterface::PeerConnectionState::kConnecting: state_str = "Connecting"; break; case webrtc::PeerConnectionInterface::PeerConnectionState::kConnected: state_str = "Connected"; break; case webrtc::PeerConnectionInterface::PeerConnectionState::kDisconnected: state_str = "Disconnected"; break; case webrtc::PeerConnectionInterface::PeerConnectionState::kFailed: state_str = "Failed"; break; case webrtc::PeerConnectionInterface::PeerConnectionState::kClosed: state_str = "Closed"; break; } std::cout << "[连接状态] " << state_str << std::endl; } // 其他虚函数保持空实现,以满足接口要求 void OnSignalingChange(webrtc::PeerConnectionInterface::SignalingState) override {} void OnAddStream(rtc::scoped_refptr<webrtc::MediaStreamInterface>) override {} void OnRemoveStream(rtc::scoped_refptr<webrtc::MediaStreamInterface>) override {} void OnDataChannel(rtc::scoped_refptr<webrtc::DataChannelInterface>) override {} void OnRenegotiationNeeded() override {} void OnIceConnectionChange(webrtc::PeerConnectionInterface::IceConnectionState) override {} void OnIceGatheringChange(webrtc::PeerConnectionInterface::IceGatheringState) override {} }; class SimpleSdpObserver : public webrtc::SetSessionDescriptionObserver { public: static rtc::scoped_refptr<SimpleSdpObserver> Create() { return new rtc::RefCountedObject<SimpleSdpObserver>(); } void OnSuccess() override { std::cout << "[SDP操作成功]" << std::endl; } void OnFailure(webrtc::RTCError error) override { std::cout << "[SDP操作失败] " << error.message() << std::endl; } protected: SimpleSdpObserver() = default; ~SimpleSdpObserver() override = default; }; int main() { // 1. 初始化SSL和日志 rtc::InitializeSSL(); rtc::LogMessage::LogToDebug(rtc::LS_WARNING); // 设置日志级别 // 2. 创建网络线程和工作线程(WebRTC内部需要) auto network_thread = rtc::Thread::CreateWithSocketServer(); auto worker_thread = rtc::Thread::Create(); auto signaling_thread = rtc::Thread::Create(); network_thread->Start(); worker_thread->Start(); signaling_thread->Start(); // 3. 创建PeerConnectionFactory rtc::scoped_refptr<webrtc::PeerConnectionFactoryInterface> peer_connection_factory = webrtc::CreatePeerConnectionFactory( network_thread.get(), worker_thread.get(), signaling_thread.get(), nullptr, // 默认音频设备管理 webrtc::CreateBuiltinAudioEncoderFactory(), webrtc::CreateBuiltinAudioDecoderFactory(), webrtc::CreateBuiltinVideoEncoderFactory(), webrtc::CreateBuiltinVideoDecoderFactory(), nullptr, // 音频混合器 nullptr // 音频处理 ); if (!peer_connection_factory) { std::cerr << "创建PeerConnectionFactory失败!" << std::endl; return -1; } // 4. 创建PeerConnection配置 webrtc::PeerConnectionInterface::RTCConfiguration config; config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan; // 使用Unified Plan,现代标准 webrtc::PeerConnectionInterface::IceServer ice_server; ice_server.urls.push_back("stun:stun.l.google.com:19302"); // 使用Google公共STUN服务器 config.servers.push_back(ice_server); // 5. 创建PeerConnection auto observer = std::make_unique<SimplePeerConnectionObserver>(); webrtc::PeerConnectionDependencies dependencies(observer.get()); rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection = peer_connection_factory->CreatePeerConnection(config, std::move(dependencies)); if (!peer_connection) { std::cerr << "创建PeerConnection失败!" << std::endl; return -1; } std::cout << "PeerConnection创建成功。按回车键创建Offer并退出..." << std::endl; // 6. 创建一个仅用于演示的音频流(无实际设备) // 在实际应用中,这里应该调用 `peer_connection_factory->CreateAudioTrack` 并添加流 // 本例省略,仅展示连接建立流程 // 7. 创建Offer(发起方) peer_connection->CreateOffer( webrtc::CreateSessionDescriptionObserver::Create( [peer_connection](webrtc::SessionDescriptionInterface* desc) { // 设置本地描述 peer_connection->SetLocalDescription(SimpleSdpObserver::Create(), desc); // 获取SDP字符串 std::string sdp_offer; desc->ToString(&sdp_offer); std::cout << "\n=== 生成的SDP Offer ===\n" << sdp_offer << "\n=== End ===\n" << std::endl; // 在实际应用中,将此sdp_offer通过信令发送给远端 }, [](webrtc::RTCError error) { std::cout << "创建Offer失败: " << error.message() << std::endl; } ), webrtc::PeerConnectionInterface::RTCOfferAnswerOptions() ); std::cin.get(); // 等待按键,让程序有足够时间收集ICE候选者 // 8. 清理 peer_connection->Close(); peer_connection_factory = nullptr; network_thread->Stop(); worker_thread->Stop(); signaling_thread->Stop(); rtc::CleanupSSL(); return 0; }

    这个示例创建了一个WebRTC对等连接,配置了STUN服务器,并生成了一个SDP Offer。它演示了工厂创建、连接配置、观察者回调等核心流程。

  3. 创建BUILD.gn文件:这是GN构建系统的核心配置文件,告诉GN如何编译你的目标。

    # src/mysimplepeer/BUILD.gn import("//webrtc.gni") # 导入WebRTC的构建规则 # 声明一个可执行文件目标 executable("mysimplepeer") { output_name = "mysimplepeer" # 输出文件名 sources = [ "mysimplepeer.cc", ] # 定义依赖项 deps = [ "//api:create_peerconnection_factory", "//api:peer_connection_interface", "//api:rtc_error", "//api:set_remote_description_observer_interface", "//media:rtc_media_base", "//pc:peer_connection", "//pc:rtc_pc", "//rtc_base:rtc_base", "//rtc_base:ssl_adapter", "//system_wrappers:system_wrappers", ] # 配置编译参数 configs += [ "//build/config/compiler:default_optimization", "//build/config/win:default_crt", ] if (is_win) { configs += [ "//build/config/win:console" ] # 设置为控制台程序 } }

4.2 编译并运行你的第一个示例

  1. 更新构建配置:回到src目录,编辑或重新生成你的构建目录,确保它包含你的新目标。

    cd src # 如果out/Default已存在,只需同步项目文件 gn gen out/Default --args='is_debug=false target_cpu="x64"' # 这个命令会重新解析BUILD.gn文件,将mysimplepeer目标加入构建列表
  2. 编译你的项目:

    ninja -C out/Default mysimplepeer

    编译成功后,在out/Default目录下会生成mysimplepeer.exe

  3. 运行程序:

    cd out/Default mysimplepeer.exe

    程序运行后,你会看到控制台输出连接状态,打印出收集到的本地ICE候选者信息,最后输出一大段SDP Offer字符串。这证明你的WebRTC C++环境已经成功搭建,并且能够创建基本的对等连接了。

核心原理解读:这个简单的程序实际上走完了WebRTC连接建立的前半部分:信令交换的本地生成阶段CreateOffer生成了包含媒体信息和本地网络能力的SDP。OnIceCandidate回调则提供了NAT穿透所需的网络路径信息(ICE候选者)。在实际应用中,你需要通过自定义的信令服务器(如WebSocket)将这个SDP Offer和ICE候选者发送给另一个客户端(Answerer),并处理对方传回的SDP Answer和远端ICE候选者,才能完成连接的建立。

5. 进阶:实现一个简单的信令服务器与双端通信

单机演示只是第一步。真正的WebRTC应用需要信令服务器来协调两个或多个客户端。下面我们扩展示例,使用一个简单的本地TCP Socket服务器模拟信令交换,实现两个进程间的连接。

5.1 设计一个微型信令服务器

为了保持项目纯粹,我们用C++写一个极简的同步TCP服务器。在实际生产环境,你可能会选择Boost.Asio、libevent或直接使用WebSocket库(如libwebsockets)。

信令服务器 (signaling_server.cc) 核心逻辑:

  1. 监听一个端口,等待两个客户端连接。
  2. 为每个客户端分配一个ID(如0和1)。
  3. 当一个客户端发送消息(SDP或ICE候选者)时,服务器将其转发给另一个客户端。
  4. 实现简单的消息格式,例如type:sdp, data:...type:candidate, data:...

由于篇幅限制,这里不展开完整的服务器代码,但关键点在于:信令服务器不处理WebRTC的任何业务逻辑,它只负责转发文本消息。消息的格式和含义由客户端双方约定。

5.2 改造客户端以使用信令

你需要修改mysimplepeer.cc,使其包含网络通信逻辑来连接信令服务器,并实现完整的信令状态机:

  1. 连接信令服务器:使用rtc::AsyncSocket或标准C++ Socket库连接到localhost的服务器端口。
  2. 发送信令:OnIceCandidate和创建SDP成功的回调中,将候选者字符串或SDP字符串封装成约定好的格式,通过Socket发送给服务器。
  3. 接收信令:在另一个线程或异步回调中监听Socket,接收服务器转发的消息。解析消息类型,如果是远端SDP Offer,则调用SetRemoteDescription并创建Answer;如果是远端ICE候选者,则调用AddIceCandidate

这个改造过程会将一个孤立的对等端,变成一个可以通过网络协调的真正客户端。你会深刻理解“信令交换”“ICE协商”这两个WebRTC核心概念是如何在代码层面协作的。

实操心得:在实现信令时,消息的序列化与反序列化建议使用简单的JSON格式(如使用nlohmann/json库)。确保在发送SDP前,正确设置本地描述 (SetLocalDescription),这是一个常见的顺序错误。另外,处理ICE候选者时,注意候选者可能在设置远程描述之前或之后到达,需要用一个队列缓存那些过早到达的候选者。

6. 常见编译与运行问题深度排查

即使按照步骤操作,你也可能会遇到各种问题。下面是我在多次实践中总结的典型问题及解决方案。

6.1 编译期问题

问题现象可能原因解决方案
gn genninja命令未找到Depot Tools路径未正确添加到PATH环境变量,或终端未重启。1. 检查PATH,确保depot_tools目录在最前面。
2. 关闭所有命令行,重新打开一个新的命令提示符(最好是VS开发人员命令提示符)。
编译错误:找不到头文件“api/...”BUILD.gn文件中的依赖 (deps) 声明不全。根据缺失的头文件所属模块,在BUILD.gndeps中添加对应的目标。使用gn ls out/Default查看所有可用目标,或参考src/examples/peerconnection/BUILD.gn来补充依赖。
链接错误:大量未解析的外部符号(LNK2001)1. 依赖缺失(同头文件问题)。
2. 链接了错误的库配置(Debug vs Release)。
1. 仔细检查并补全deps
2. 确保你的构建参数is_debug与所有依赖的编译配置一致。清理out/Default目录,用正确的参数重新gn gen
error: use of undeclared identifier ‘CreatePeerConnectionFactory’函数签名或命名空间错误。WebRTC API可能随版本更新。确认你包含的头文件正确,并且使用了正确的命名空间(通常是webrtc::)。最可靠的方法是直接参考src/examples/peerconnection/client/conductor.cc中的用法。
编译时间极长,或内存不足WebRTC编译非常消耗资源。1. 增加系统虚拟内存。
2. 在gn genargs中添加enable_nacl=false以禁用不需要的组件。
3. 使用ninja -C out/Default -j 4限制并行编译任务数(根据你的CPU核心数调整)。

6.2 运行期问题

问题现象可能原因解决方案
程序崩溃在rtc::InitializeSSL()SSL库初始化失败,可能缺少DLL或环境冲突。1. 确保程序运行时,其所在目录(out/Default)或系统PATH包含必要的OpenSSL DLL(通常由depot_tools或VS环境提供)。
2. 尝试以管理员身份运行。
无法收集到ICE候选者,或状态一直停留在kNew/kConnecting1. 网络权限被防火墙阻止。
2. STUN服务器不可达或配置错误。
3. 没有添加任何媒体流(音频/视频/数据通道),连接不会被激活。
1. 检查防火墙设置,允许你的程序进行网络访问。
2. 尝试更换其他公共STUN服务器,如stun:stun1.l.google.com:19302
3.关键点:在创建Offer前,必须调用AddTrack添加至少一个媒体轨道到PeerConnection,否则连接不会开始ICE收集。我们的极简示例为了聚焦流程省略了这一步,但实际可运行的demo必须添加。
SDP Offer/Answer交换后,连接状态无法变为kConnected1. ICE候选者交换不完整或格式错误。
2. NAT穿透失败,需要配置TURN服务器。
3. 双方SDP中的媒体编解码器不支持。
1. 仔细核对双方交换的SDP和ICE候选者字符串是否完整、正确。
2. 在复杂网络下(对称型NAT),仅STUN可能不够。需要在RTCConfiguration中添加TURN服务器配置。这是实现高连通率的关键。
3. 检查SDP中的m=audiom=video行,确保双方有共同的编解码器(如opus, VP8)。
内存泄漏报告没有正确释放WebRTC的引用计数对象。WebRTC大量使用rtc::scoped_refptr进行引用计数管理。确保你的观察者类(如SimpleSdpObserver)继承自rtc::RefCountedObject或正确实现引用计数。在程序退出前,按顺序释放PeerConnectionPeerConnectionFactory并停止所有线程。

6.3 调试与日志技巧

  • 启用详细日志:main函数开头,设置rtc::LogMessage::LogToDebug(rtc::LS_VERBOSE);可以输出最详细的日志,对排查网络、ICE问题极有帮助。
  • 使用调试器:在Visual Studio中打开生成的.sln文件(位于out/Default下的all.sln),可以方便地设置断点、单步调试你的代码和WebRTC内部逻辑。这是理解流程最有效的方式。
  • 检查网络流量:使用Wireshark过滤STUNDTLSRTP流量,可以直观看到连接是否建立、媒体是否在传输。

7. 项目扩展与下一步学习方向

当你成功运行起基础的示例后,可以沿着以下几个方向深化学习和项目扩展:

  1. 添加真实媒体流:

    • 音频:使用peer_connection_factory->CreateAudioTrackpeer_connection_factory->GetAudioDeviceModule()获取本地麦克风输入。
    • 视频:使用peer_connection_factory->CreateVideoTrack。视频采集更复杂,需要用到webrtc::VideoCaptureModule或更新的webrtc::VideoTrackSource接口。可以参考src/examples/peerconnection/client/conductor.cc中的CreateVideoCapturer方法。
    • 创建webrtc::MediaStreamInterface,将轨道添加到流中,再通过peer_connection->AddTrack将流添加到连接。
  2. 实现一个图形化界面:

    • 结合Qt、Win32 API或任何你熟悉的GUI框架,将视频渲染到窗口。WebRTC提供了webrtc::VideoSinkInterface,你可以实现它来接收视频帧,并转换为GUI库能显示的格式(如RGB数据)。
  3. 集成编解码与处理:

    • 探索webrtc::AudioProcessingModule进行回声消除、降噪等音频处理。
    • 研究webrtc::VideoEncoderwebrtc::VideoDecoder接口,了解如何定制或替换编解码器。
  4. 深入架构:

    • 阅读src/pc/目录下的源码,理解PeerConnection的生命周期管理、ICE状态机、媒体引擎的交互。
    • 学习src/media/目录,了解音频流水线和视频流水线是如何工作的。

这个“亲测免费”的指南,旨在为你打通从零到一的第一公里。WebRTC C++的世界很深,但一旦你掌握了环境搭建、项目编译和核心API的调用模式,剩下的就是在这个坚实的基础上,根据你的具体需求去添砖加瓦。记住,多读官方示例代码 (src/examples/),多利用调试工具,遇到问题优先在Chromium/WebRTC的官方issue tracker或社区论坛搜索,大多数坑都已经有人踩过并提供了解决方案。