“ 一起练习Second“——《开源网络模拟器NS-3架构与实践(周之迪)》学习之旅(3)

磨刀霍霍

经历了first的锻炼,我们已经熟悉了NS3脚本的基础规则和逻辑,教材中直接对third脚本开始讲解,为了连贯性和完整性,让我们自己动手学习加练习一下second脚本吧~

      10.1.1.0
n0 -----------------n1         n2        n3        n4    
  | point-to-point  |            |           |            |
                           ===================
                                      LAN 10.1.2.0

首先观察一下这个网络拓扑,0号点和1号点依旧通过p2p通信,而1、2、3、4四个结点则在一个有线局域网LAN10.1.2.0。我们发现这个网络拓扑出现两个不同的网段,事情变得复杂了一些哈。

刀磨好了

废话不多说,让我们开始写脚本吧~

头文件与命名空间

#include "ns3/core-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/csma-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/applications-module.h"
#include "ns3/netanim-module.h"
#include "ns3/ipv4-global-routing-helper.h"

相信前面的文件大家都比较熟悉,唯独最后一条:#include "ns3/ipv4-global-routing-helper.h"是用来做什么的呢?按照first脚本的套路,分配地址使用network模块就够了,何必再多来一个全局路由助手。这时我们需要回头观察一下拓扑,1号结点即属于p2p的10.1.1.0网段,又属于LAN10.1.2.0网段,这也意味着1号结点将具备两个网络设备,要实现不同网段之间的通信,路由表还是不可缺少的。

using namespace ns3;
using namespace std;

NS_LOG_COMPONENT_DEFINE("My_new_Second");

命名空间就不多说了,依旧是两位老朋友。那么顺便把日志组件也定义一下吧~

准备阶段

    uint32_t nCsma = 3;
    bool verbose = true;
    CommandLine cmd(__FILE__);
    cmd.AddValue("nCsma","Number of \"extra\" CSMA nodfes/devices",nCsma);
    cmd.AddValue("verbose","Tell echo applications to log if True",verbose);
    cmd.Parse(argc,argv);

 定义nCsma相信比较好理解,就是右侧LAN的结点数量,你也许会问,明明是4个啊,嗯,别忘了p2p网段需要两个结点,让我们暂时把1号节点让给p2p

verbose是官方脚本中出现的一个用法,用过下面cmd变量对verbose的描述我们可以知道,这个变量是用来控制是否回显日志信息的,也许你对这个概念不甚理解,请暂时把他认为是一把锁。

cmd.AddValue(),让我们回想一下为什么要定义一个CommandLine类型的变量cmd呢,我们之前说过(也许说过),是为了通过在终端控制一些变量的更改,马春光先生对这部分使用了丰富的实例进行解释,这节的最后我们也会小小体现一下。在这里我们添加了两个可以通过命令行控制的变量,分别是csma节点数量和verbose锁。

    if(verbose)
    {
        LogComponentEnable("UdpEchoClientApplication",LOG_LEVEL_INFO);
        LogComponentEnable("UdpEchoServerApplication",LOG_LEVEL_INFO);
    }

这个if逻辑是否让你对verbose的理解更加清晰了?verbose我们设置了默认为真,我们可以在文末观察一下他为假的现象。

拓扑部署

在编程时老师常说,要保持良好的编程习惯,这个习惯可能体现在格式上、风格上、可靠性上等等,这里我们要再次提到的习惯就是,把可能出现的错误尽可能回避。

处在CSMA上的结点数量通过nCsma定义,并且可以通过命令行修改,如果不小心忘记了网络拓扑设置成0怎么办?

    nCsma = nCsma == 0 ? 1 : nCsma;

三目运算就不多介绍了,nCsma等于0的话就给他赋个1,这样一来右侧至少有两个节点。

    NodeContainer p2pNodes;
    p2pNodes.Create(2);
    NodeContainer csmaNodes;
    csmaNodes.Create(nCsma);

定义两个结点容器,分别创建2个p2p结点和nCsma个CSMA结点。是不是忘了什么?难道这两部分结点的缘分就到这里了吗?不管他们的前世又如何机缘,1号结点早已投入LAN的怀抱,这时我们要将1号结点加入csmaNodes中:

    csmaNodes.Add(p2pNodes.Get(1));

面对一个复杂问题,我们应该把他们逐步分解,一步步解决。这种两个网段的问题,就让我们拆成两个网段分别解决吧。

    PointToPointHelper pointToPoint;
    pointToPoint.SetChannelAttribute("Delay",StringValue("2ms"));
    pointToPoint.SetDeviceAttribute("DataRate",StringValue("5Mbps"));
    NetDeviceContainer p2pDevices;
    p2pDevices = pointToPoint.Install(p2pNodes);

是不是找回来熟悉的感觉?定义一个助手,设置链路和物理层的属性,然后安装到节点上,用一个网络设备去接收,行云流水,令人拍案。

    CsmaHelper csma;
    csma.SetChannelAttribute("DataRate",StringValue("100Mbps"));
    csma.SetChannelAttribute("Delay",TimeValue(NanoSeconds(6560)));

    NetDeviceContainer csmaDevices;
    csmaDevices = csma.Install(csmaNodes);

再用这熟悉的感觉去定义CSMA吧!欸,也许就是这熟悉的感觉,会让你在排错时费尽脑汁,眼冒金星。仔细看,这里csma助手设置的属性,都是”Channel Attribute“,为什么不设置Device的数据传输速率呢?曾记得远古时候(两个月前)自己找遍了关于这个问题的解释,在记不清的地方找到了答案:CSMA不允许统一信道上有多个不同数据传输速度的设备。关于这个回答的正确性欢迎大家指正,只是在NS3脚本里,我还是比较接收这个回答(起码它可以解决我的报错)。

部署网络

    InternetStackHelper stack;

    stack.Install(p2pNodes.Get(0));
  
    stack.Install(csmaNodes);

这里会发现p2p只有0号节点配置了网络,为什么,因为1号点已经投入了csma的怀抱(好凄惨的故事)

    Ipv4AddressHelper address;
    address.SetBase("10.1.1.0","255.255.255.0");

    Ipv4InterfaceContainer p2pInterface;
    p2pInterface = address.Assign(p2pDevices);

    address.SetBase("10.1.2.0","255.255.255.0");
    Ipv4InterfaceContainer csmaInterface;
    csmaInterface = address.Assign(csmaDevices);

因为NS3是一个离散事件编程,且我们的脚本采用顺序执行,这里address变量相当于被反复使用,但配置好的地址都被安装到了指定的接口上。到现在我们的网络层也配置好啦。

设置应用

似乎一开头忘记说谁是服务器谁是客户端了,既然布置了这么”复杂“的网络,那必然不能辜负我们的辛苦。选择第nCsma个结点作为服务器,选择0号结点作为客户端,让他们跨过遥远的网络进行通信吧。

    UdpEchoServerHelper echoServer(9);

    ApplicationContainer serverApps = echoServer.Install(csmaNodes.Get(nCsma));
    serverApps.Start(Seconds(1.0));
    serverApps.Stop(Seconds(10.0));

    UdpEchoClientHelper echoClient(csmaInterface.GetAddress(nCsma),9);
    echoClient.SetAttribute("MaxPackets", UintegerValue(1));
    echoClient.SetAttribute("Interval",TimeValue(Seconds(1.0)));
    echoClient.SetAttribute("MaxPackets", UintegerValue(1024));

    ApplicationContainer clientApps = echoClient.Install(p2pNodes.Get(0));
    clientApps.Start(Seconds(2.0));
    clientApps.Stop(Seconds(10.0));

我故意不在代码里放注释,也是为了大家能够积极回想,我也一起回想。这里需要注意的还是服务器助手和客户端助手的在定义的时候逻辑差异。服务器高高在上,只需要告诉他需要侦听哪个接口就好;而客户端略显”卑微“需要主动知道服务器的地址和端口,还得主动设置各种属性。

别着急

我晓得,我晓得该运行了,因为现在网络很完备了,但是我们写脚本写程序的意义在哪呢,我要分析输出。

 Ipv4GlobalRoutingHelper::PopulateRoutingTables();

这里我们借助全局路由助手维护这个网络的路由表,面对有多个节点,多个网段的网络,如果没有路由维护,那么我们只能知道”发送的状态“,却无从知道”发送的结果“。越说越抽象了,让我们看看没有全局理由的结果。

 我们会发现只能知道客户端发了,而服务器收到了吗。点开生成的流量文件,我们发现都是空白,这样的网络并没有完成通信。

现在我们为脚本加入这个全局路由,再次运行观察结果。

是不是像点样子,再点开流量文件,可以看到客户端和服务器的来回通信

 下面让我们研究下输出文件的代码。EnablePcap函数可以帮助我们生成.pcap文件,使用wireshark进行分析。NS3提供了多种生成方式,其中EnablePcapAll生成由指定助手下的所有结点的流量文件;EnablePcap则是针对指定设备的,仔细观察下面csma三种不同的EnablePcap:

  • 第一种指定了csma容器里的1号设备,是第几个呢,0是p2p的,1号结点是之后才加入csma的,2号结点是csma容器里的0,那3号结点就是csma容器里的1.很绕是吧,要想明白呐~
  • 第二种是生成csma结点容器里所有节点的流量文件,一共几个呢,没错,4个;
  • 第三种是获得csma容器中,指定编号为1(也就是3号节点)的流量文件,但这时我们发现,在使用结点容器Get(1)后,有使用指针方法去Get了Id,这就是这个函数的用法有关了,大家可以研究源码,去探索这背后的原因。
    pointToPoint.EnablePcapAll("new_Second_All");
    csma.EnablePcap("new_Second_Devices",csmaDevices.Get(1),false);
    csma.EnablePcap("new_Second_NodesContainer",csmaNodes,false);
    csma.EnablePcap("new_Second_GetID",csmaNodes.Get(1)->GetId(),false);

让我们看一下生成文件

 肥肠正确,是吧。

剩下的就是可视化文件和吞吐量文件了,我们可以使用吞吐量文件去测试刚才有无路由表情况下结点的工作状态。

    AnimationInterface anim("p2p_csma.xml");

    AsciiTraceHelper ascii;
    pointToPoint.EnableAscii(ascii.CreateFileStream("client.tr"),p2pDevices.Get(0));

很好,我们现在知道了文件的输出操作,下面让我们看看通过终端控制一些好玩的事情。

cmd万岁

 ./ns3 run scratch/new_second.cc --cwd=scratch/new_Tomorrow/Second_output

这行指令是什么意思呢,根据上一节我们知道,是运行scratch目录下的脚本,把输出文件放入 scratch/new_Tomorrow/Second_output目录中,这里不再赘述。

现在我们突发奇想,那个verbose设置成false会发生什么?可是每次都点开脚本找verbose好麻烦,而且我只是想临时看下效果而已。这是我们可以通过终端去临时改变这个变量。

./ns3 run 'scratch/new_second.cc --verbose=false'

没错,用单引号括起来就好,这里能修改verbose是因为我们在程序的开始使用了cmd.AddValue。运行一下:

 嗯,输出文件一切正常,只是客户端和服务器之间的沟通变成了悄悄话。(这里的输出是因为我们输出了可视化文件,但是没有对结点的位置进行部署,NS提醒我们这些结点都采用固定分布)verbose决定是否显示这些消息,我们再次执行指令

./ns3 run scratch/new_second.cc

 你会发现他们的通信过程又变得可见了,正如刚才说的,这个修改是临时的。

很好,现在我们要把右侧的csma结点变成9个,怎么实现呢?

 ./ns3 run 'scratch/new_second.cc --nCsma=9'

运行一下,你会发现会输出好多好多文件(光是csma结点就是9个鸭,算上后加入的1号点就是11个噢。

收刀

第二个脚本也结束啦,下一节让我们在此基础上加入大家熟悉又陌生的——WiFi。

最后附上带有注释的所有代码~

// Default Network Topology
//
//       10.1.1.0
// n0 -------------- n1   n2   n3   n4
//    point-to-point  |    |    |    |
//                    ================
//                      LAN 10.1.2.0

//让我做一个练习,尝试根据这个拓扑写下他的脚本

#include "ns3/core-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/csma-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/applications-module.h"
#include "ns3/ipv4-global-routing-helper.h"
//按照first脚本来看,分配ipv4/6地址network就够了
/*该头文件定义了 Ipv4GlobalRoutingHelper 类,
该类是用于管理 IPv4 全局路由的助手类。
它提供了创建和管理全局路由协议的功能,
Create(Ptr<Node> node) 函数:创建指定节点上的 IPv4 全局路由协议的实例。
以及填充和重新计算路由表的静态函数。
PopulateRoutingTables():用于填充路由表的静态函数。
RecomputeRoutingTables():用于重新计算路由表的静态函数。
*/
#include "ns3/netanim-module.h"
using namespace ns3;
using namespace std;

NS_LOG_COMPONENT_DEFINE("My_new_Second");

int
main(int argc, char *argv[])
{
/******************准备阶段**********************/
    uint32_t nCsma = 3;
    //定义拓扑右方的有线局域网的节点个数,接入点是PPP
    bool verbose = true;
    //这个变量相当于一把锁,来控制是否将回显应用程序的日志记录。
    CommandLine cmd(__FILE__);
    //定义一个命令行类型的变量cmd
    cmd.AddValue("nCsma","Number of \"extra\" CSMA nodfes/devices",nCsma);
    cmd.AddValue("verbose","Tell echo applications to log if True",verbose);

    cmd.Parse(argc,argv);
    // 这里定义了一个命令行变量,其作用是可以通过终端去临时修改一些变量,方便调试

    if(verbose)
    {
        LogComponentEnable("UdpEchoClientApplication",LOG_LEVEL_INFO);
        LogComponentEnable("UdpEchoServerApplication",LOG_LEVEL_INFO);
    }
    //这样一看,这把锁的作用就很清晰明了了
    
/******************准备结束**********************/

/******************拓扑部署**********************/

    nCsma = nCsma == 0 ? 1 : nCsma;
    //三目运算,nCsma等于0的话就给他赋个1,这样一来右侧至少有两个节点

    NodeContainer p2pNodes;
    //定义点对点节点容器
    p2pNodes.Create(2);
    //在容器里创建两个节点

    NodeContainer csmaNodes;
    csmaNodes.Create(nCsma);
    //定义csma容器并创建节点
    csmaNodes.Add(p2pNodes.Get(1));
    //把p2p的右侧节点加入到csma里,作为接入点

    PointToPointHelper pointToPoint;
    pointToPoint.SetChannelAttribute("Delay",StringValue("2ms"));
    pointToPoint.SetDeviceAttribute("DataRate",StringValue("5Mbps"));
    //设置点对点设备和信道的属性

    NetDeviceContainer p2pDevices;
    p2pDevices = pointToPoint.Install(p2pNodes);
    //定义设备容器,利用助手把设备安装到节点里
    //使用定义好的设备接收助手安装后的返回值

    CsmaHelper csma;
    csma.SetChannelAttribute("DataRate",StringValue("100Mbps"));
    csma.SetChannelAttribute("Delay",TimeValue(NanoSeconds(6560)));
    //利用助手设置csma信道属性
    //因为CSMA信道上不允许有数据传输速率不一致的情况发生,所以不考虑设备属性

    NetDeviceContainer csmaDevices;
    csmaDevices = csma.Install(csmaNodes);
    //定义设备容器并使用助手进行安装

    //到这里可以发现,右侧第一个节点作为p2p和csma的接入点
    //其拥有两个网络设备

/******************拓扑部署完毕**********************/

/***********至此物理层和链路层部署完毕**********************/

/********************网络部署**********************/
    InternetStackHelper stack;
    //定义一个网络协议栈助手
    stack.Install(p2pNodes.Get(0));
    //为点对点左侧节点安装一个栈
    stack.Install(csmaNodes);
    //为csma所有节点安装栈
    //这样做的原因也很明显,左右侧属于两个不一样的网段,
    //p2p1号节点论范围还是csma的网段里

    Ipv4AddressHelper address;
    address.SetBase("10.1.1.0","255.255.255.0");
    //定义一个网络的起始地址和掩码
    Ipv4InterfaceContainer p2pInterface;
    p2pInterface = address.Assign(p2pDevices);
    //把设置的网络部署到点对点网络接口上

    address.SetBase("10.1.2.0","255.255.255.0");
    Ipv4InterfaceContainer csmaInterface;
    csmaInterface = address.Assign(csmaDevices);
    //把设置的网络部署到csma网络接口上

/******************网络部署结束**********************/

/***********至此可以完成基于TCP/IP的网络通信**********/

/******************安装应用程序**********************/
    UdpEchoServerHelper echoServer(9);
    //利用助手定义服务端应用程序,定义端口为9

    ApplicationContainer serverApps = echoServer.Install(csmaNodes.Get(nCsma));
    //定义应用容器,接收echoServer.Install后返回的容器类型
    //把csma网段中最后一个节点作为服务器
    serverApps.Start(Seconds(1.0));
    serverApps.Stop(Seconds(10.0));
    //设置服务器程序的起止时间

    UdpEchoClientHelper echoClient(csmaInterface.GetAddress(nCsma),9);
    //利用助手定义客户端程序,告知客户端程序服务器的地址和端口  
    echoClient.SetAttribute("MaxPackets", UintegerValue(1));
    echoClient.SetAttribute("Interval",TimeValue(Seconds(1.0)));
    echoClient.SetAttribute("MaxPackets", UintegerValue(1024));
    //设置客户端程序属性

    ApplicationContainer clientApps = echoClient.Install(p2pNodes.Get(0));
    //把设置好的客户端应用安装到p2p网段0号节点
    clientApps.Start(Seconds(2.0));
    clientApps.Stop(Seconds(10.0));
    //设置客户端开始和结束时间
/******************应用安装完成**********************/

/**************至此网络功能搭建完成**********************/

/******************生成分析文件**********************/

    Ipv4GlobalRoutingHelper::PopulateRoutingTables();
    // //利用全局路由助手为每个节点建立路由表

    pointToPoint.EnablePcapAll("new_Second_All");
    csma.EnablePcap("new_Second_Devices",csmaDevices.Get(1),false);
    //这里可以看到EnablePcapAll和EnablePcap的区别
    //显然All不需要考虑很多参数,而指定Pcap需要设置一些
    csma.EnablePcap("new_Second_NodesContainer",csmaNodes,false);
    csma.EnablePcap("new_Second_GetID",csmaNodes.Get(1)->GetId(),false);
    //通过学习源码,发现Pcap文件的生成形式有很多
    AnimationInterface anim("p2p_csma.xml");
    //引入netanim模块,通过接口生成xml文件,可视化

    AsciiTraceHelper ascii;
    pointToPoint.EnableAscii(ascii.CreateFileStream("client.tr"),p2pDevices.Get(0));
    //使用吞吐量跟踪助手,生成客户端的吞吐文件

/********至此生成了流量文件、可视化文件、吞吐量文件**********/
    
    Simulator::Run();
    Simulator::Destroy();
    
    return 0;
}

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个基于ns-3.27的NS-3 WIFI性能仿真代码,用于模拟802.11n网络的性能。 ``` #include "ns3/core-module.h" #include "ns3/mobility-module.h" #include "ns3/wifi-module.h" #include "ns3/internet-module.h" #include "ns3/network-module.h" #include "ns3/applications-module.h" #include "ns3/flow-monitor-helper.h" #include "ns3/flow-monitor-module.h" using namespace ns3; NS_LOG_COMPONENT_DEFINE ("WifiN"); int main (int argc, char *argv[]) { uint32_t nWifi = 3; bool verbose = false; CommandLine cmd; cmd.AddValue ("nWifi", "Number of wifi STA devices", nWifi); cmd.AddValue ("verbose", "Turn on all WifiNetDevice log components", verbose); cmd.Parse (argc,argv); if (verbose) { LogComponentEnableAll (LOG_LEVEL_INFO); LogComponentEnable ("WifiNetDevice", LOG_LEVEL_ALL); } NodeContainer wifiStaNodes; wifiStaNodes.Create (nWifi); NodeContainer wifiApNode; wifiApNode.Create (1); YansWifiChannelHelper channel = YansWifiChannelHelper::Default (); YansWifiPhyHelper phy = YansWifiPhyHelper::Default (); phy.SetChannel (channel.Create ()); WifiHelper wifi = WifiHelper::Default (); wifi.SetRemoteStationManager ("ns3::AarfWifiManager"); NqosWifiMacHelper mac = NqosWifiMacHelper::Default (); Ssid ssid = Ssid ("ns-3-ssid"); mac.SetType ("ns3::StaWifiMac", "Ssid", SsidValue (ssid), "ActiveProbing", BooleanValue (false)); NetDeviceContainer staDevices; staDevices = wifi.Install (phy, mac, wifiStaNodes); mac.SetType ("ns3::ApWifiMac", "Ssid", SsidValue (ssid)); NetDeviceContainer apDevice; apDevice = wifi.Install (phy, mac, wifiApNode); MobilityHelper mobility; mobility.SetPositionAllocator ("ns3::GridPositionAllocator", "MinX", DoubleValue (0.0), "MinY", DoubleValue (0.0), "DeltaX", DoubleValue (5.0), "DeltaY", DoubleValue (10.0), "GridWidth", UintegerValue (3), "LayoutType", StringValue ("RowFirst")); mobility.SetMobilityModel ("ns3::RandomWalk2dMobilityModel", "Bounds", RectangleValue (Rectangle (-50, 50, -50, 50))); mobility.Install (wifiStaNodes); mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel"); mobility.Install (wifiApNode); InternetStackHelper stack; stack.Install (wifiApNode); stack.Install (wifiStaNodes); Ipv4AddressHelper address; address.SetBase ("10.1.1.0", "255.255.255.0"); Ipv4InterfaceContainer staNodeInterface; staNodeInterface = address.Assign (staDevices); address.SetBase ("10.1.2.0", "255.255.255.0"); Ipv4InterfaceContainer apNodeInterface; apNodeInterface = address.Assign (apDevice); UdpEchoServerHelper echoServer (9); ApplicationContainer serverApps = echoServer.Install (wifiApNode.Get (0)); serverApps.Start (Seconds (1.0)); serverApps.Stop (Seconds (10.0)); UdpEchoClientHelper echoClient (apNodeInterface.GetAddress (0), 9); echoClient.SetAttribute ("MaxPackets", UintegerValue (1)); echoClient.SetAttribute ("Interval", TimeValue (Seconds (1.0))); echoClient.SetAttribute ("PacketSize", UintegerValue (1024)); ApplicationContainer clientApps = echoClient.Install (wifiStaNodes.Get (0)); clientApps.Start (Seconds (2.0)); clientApps.Stop (Seconds (10.0)); FlowMonitorHelper flowmon; Ptr<FlowMonitor> monitor = flowmon.InstallAll (); Simulator::Stop (Seconds (10.0)); Simulator::Run (); monitor->CheckForLostPackets (); Ptr<Ipv4FlowClassifier> classifier = DynamicCast<Ipv4FlowClassifier> (flowmon.GetClassifier ()); std::map<FlowId, FlowMonitor::FlowStats> stats = monitor->GetFlowStats (); for (std::map<FlowId, FlowMonitor::FlowStats>::const_iterator i = stats.begin (); i != stats.end (); ++i) { Ipv4FlowClassifier::FiveTuple t = classifier->FindFlow (i->first); std::cout << "Flow " << i->first << " (" << t.sourceAddress << " -> " << t.destinationAddress << ")\n"; std::cout << " Tx Bytes: " << i->second.txBytes << "\n"; std::cout << " Rx Bytes: " << i->second.rxBytes << "\n"; std::cout << " Throughput: " << i->second.rxBytes * 8.0 / 9.0 / 1000 / 1000 << " Mbps\n"; } Simulator::Destroy (); return 0; } ``` 这个代码创建了一个包含一个AP和三个STA的802.11n网络,使用UDP Echo协议进行通信,并使用Flow Monitor模块监测网络流量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值