## NS-3 安裝說明
### 設定 Ubuntu 環境
* [How to Install Ubuntu 22.04 LTS on VirtualBox in Windows 11](https://www.youtube.com/watch?v=v1JVqd8M3Yc)
* [How to Install Ubuntu on Windows 11 (WSL)](https://www.youtube.com/watch?v=wjbbl0TTMeo&t=19s)
* [在 Windows 10 中安裝 Ubuntu (WSL)](https://www.kwchang0831.dev/dev-env/wsl/ubuntu)
這裡使用 "Ubuntu 24.04.2 LTS" 做示範,並安裝在 windows 11 中的 WSL
### 安裝 ns-3 依賴
以下是安裝 ns-3 前所需安裝的依賴,可以按照順序一行行輸入,抑或打包成 bash 文件
```bash
#!/bin/bash
# 更新系統
sudo apt update && sudo apt upgrade -y
# 基本開發工具
sudo apt install -y g++ python3 python3-dev cmake ninja-build git ccache pkg-config sqlite3
# Python 相關工具
python3 -m pip install --user cppyy cxxfilt
sudo apt install -y python3-setuptools python3-pip python3-sphinx ipython3
# GUI & 圖形相關依賴
sudo apt install -y qt5-qmake qtbase5-dev qtbase5-dev-tools \
gir1.2-goocanvas-2.0 python3-gi python3-gi-cairo python3-pygraphviz gir1.2-gtk-3.0
# 科學計算與數學函式庫
sudo apt install -y gsl-bin libgsl-dev libgslcblas0
# 網絡相關工具
sudo apt install -y tcpdump lvm lxc uml-utilities
# 文檔生成與格式化工具
sudo apt install -y doxygen graphviz imagemagick \
texlive texlive-extra-utils texlive-latex-extra texlive-font-utils dvipng latexmk
# 調試工具
sudo apt install -y gdb valgrind clang-format
# 開發環境相關依賴
sudo apt install -y openmpi-bin openmpi-common openmpi-doc libopenmpi-dev \
mercurial unzip automake
# 低層庫與 XML 相關依賴
sudo apt install -y libxml2 libxml2-dev libboost-all-dev libgtk-3-dev \
libsqlite3-dev libc6-dev libc6-dev-i386 libclang-dev llvm-dev
```
下載時的注意事項
```bash
$ python3 -m pip install --user cppyy
$ python3 -m pip install --user cxxfilt
```
這兩段命令不可以使用 sudo 管理權限執行,此外,安裝 cppyy 時需要將 `~/.local/bin` 添加到環境變數,具體 Shell 腳本如下:
```bash
$ echo "export PATH=/home/$(whoami)/.local/bin:\$PATH" >> ~/.bashrc
$ source ~/.bashrc
```
### 下載 ns-3
```shell
$ git clone https://github.com/nsnam/ns-3-dev-git
```
### 編譯與安裝 ns-3
```shell
$ cd ns-3-dev-git
$ ./ns3 clean
$ ./ns3 configure --enable-examples --enable-tests --enable-python-bindings --build-profile=debug
$ ./ns3 build
```
其中 configure 的前兩個參數是編譯時包含範例腳本&測試腳本,`--prefix=` 可以指定安裝路徑。完整參數可以參考 `./ns3 configure --help`
### Testing ns-3
驗證安裝結果
```shell
$ ./test.py
```
### Running ns-3
在安裝完 ns-3 後,只要向下面一樣下 command 即可啟動 ns-3
```shell
$ ./ns3 run simple-global-routing
```
`simple-global-routing` 可替換成其他檔案名
如 :
```shell
$ ./ns3 run hello-simulator
```
🚀 注意要在安裝後的 `ns-3-dev-git` 路徑下執行,並注意自己的檔案夾名稱
## 範例操作
### NS3中的常用概念
> * Node(節點):
> ns3 用 Node 類來抽象網絡中的計算設備,可以理解為路由器、感測器等設備,通過向節點添加應用程式、協議棧等內容,使其能夠模擬某些設備的運行。
>
> * Application(應用):
> ns3 的 Node 類並沒有真實的硬體設備和對應的驅動程式,因此若要模擬某些設備,還需要對其功能進行模擬,Application 類的作用就是對其功能的模擬,例如收發數據包,以完成對某些設備的模擬。
>
> * Channel(信道):
> 現實中的網絡有有線信道和無線信道,在 ns3 中,一個 Node 將連接 Channel 對象上來實現通訊,通過對 Channel 類的設置,可以模擬延遲或丟包等信道屬性,使其更像真實的通信信道。
>
> * NetDevice(網卡):
> ns3 中 Node 內的抽象包括軟件驅動程式和模擬硬體。一個網卡被“安裝”在一個節點上,以使該節點能夠通過信道與模擬的其他節點通信。
> 這就像在真實計算機中一樣,一個節點可以通過多個 NetDevices 連接到多條信道,即 NetDevice 類是用來管理 Node 和 Channel 物件的連接方式。
> 例如 CsmaNetDevice 被設計為與 CsmaChannel 一起工作,而 WifiNetDevice 則被設計為與 WifiChannel 一起工作。
> * Helper(助手):
> 在使用實體硬體時,通常需要一定的驅動程式和軟件來完成與模組之間的操作,而在 ns3 中,則提供了 Helper 類來輔助完成模組與模組之間的交互。
> 例如:在 GitHub 的 AquaSimNG 專案中,會有 AquaSimHelper、AquaSimChannelHelper 來輔助對模組參數的設置及模擬環境的配置。
### 創建自己的腳本
在 ns-3 可以用 python 或 C 撰寫腳本,在此先準備一個範例函式庫及腳本
:::spoiler tutorial-app.h
```clike
/*
* SPDX-License-Identifier: GPL-2.0-only
*/
#ifndef TUTORIAL_APP_H
#define TUTORIAL_APP_H
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/applications-module.h"
namespace ns3
{
class Application;
class TutorialApp : public Application
{
public:
TutorialApp();
~TutorialApp() override;
static TypeId GetTypeId();
void Setup(Ptr<Socket> socket,
Address address,
uint32_t packetSize,
uint32_t nPackets,
DataRate dataRate);
private:
void StartApplication() override;
void StopApplication() override;
void SendPacket();
void ScheduleTx();
Ptr<Socket> m_socket;
Address m_peer;
uint32_t m_packetSize;
uint32_t m_nPackets;
DataRate m_dataRate;
EventId m_sendEvent;
bool m_running;
uint32_t m_packetsSent;
};
TutorialApp::TutorialApp()
: m_socket(nullptr), m_peer(), m_packetSize(0), m_nPackets(0), m_dataRate(0), m_sendEvent(), m_running(false), m_packetsSent(0)
{
}
TutorialApp::~TutorialApp()
{
m_socket = nullptr;
}
TypeId TutorialApp::GetTypeId()
{
static TypeId tid = TypeId("TutorialApp")
.SetParent<Application>()
.SetGroupName("Tutorial")
.AddConstructor<TutorialApp>();
return tid;
}
void TutorialApp::Setup(Ptr<Socket> socket, Address address, uint32_t packetSize, uint32_t nPackets, DataRate dataRate)
{
m_socket = socket;
m_peer = address;
m_packetSize = packetSize;
m_nPackets = nPackets;
m_dataRate = dataRate;
}
void TutorialApp::StartApplication()
{
m_running = true;
m_packetsSent = 0;
m_socket->Bind();
m_socket->Connect(m_peer);
SendPacket();
}
void TutorialApp::StopApplication()
{
m_running = false;
if (m_sendEvent.IsPending())
{
Simulator::Cancel(m_sendEvent);
}
if (m_socket)
{
m_socket->Close();
}
}
void TutorialApp::SendPacket()
{
Ptr<Packet> packet = Create<Packet>(m_packetSize);
m_socket->Send(packet);
if (++m_packetsSent < m_nPackets)
{
ScheduleTx();
}
}
void TutorialApp::ScheduleTx()
{
if (m_running)
{
Time tNext(Seconds(m_packetSize * 8 / static_cast<double>(m_dataRate.GetBitRate())));
m_sendEvent = Simulator::Schedule(tNext, &TutorialApp::SendPacket, this);
}
}
} // namespace ns3
#endif /* TUTORIAL_APP_H */
```
:::
將以上程式碼的檔案名改為 `tutorial-app.h`
:::spoiler FirstCode
```clike
/* SPDX-License-Identifier: GPL-2.0-only */
#include "tutorial-app.h"
#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
// 搭配 netanim 使用
// #include "ns3/netanim-module.h"
// #include "ns3/mobility-module.h"
#include <fstream>
using namespace ns3;
NS_LOG_COMPONENT_DEFINE("ScriptExample");
// ===========================================================================
//
// leftNode Router0 Router1 rightNode
// +----------------+ +----------------+ +----------------+ +----------------+
// | ns-3 TCP | | ns-3 TCP | | ns-3 TCP | | ns-3 TCP |
// +----------------+ +----------------+ +----------------+ +----------------+
// | 10.1.1.1 | | 10.1.1.2 | | 10.1.2.1 | | 10.1.3.1 |
// +----------------+ +----------------+ +----------------+ +----------------+
// | point-to-point | | point-to-point | | point-to-point | | point-to-point |
// +----------------+ +----------------+ +----------------+ +----------------+
// | | | |
// +---------------------+--------------------+---------------------+
// 10 Mbps 1 Mbps 10 Mbps
// 1 ms 10 ms 1 ms
//
// Packet Flow: leftNode -> Router0 -> Router1 -> rightNode
//
// Network Configuration:
// - leftNode to Router0: 10.1.1.0/30 (10 Mbps, 1 ms)
// - Router0 to Router1: 10.1.2.0/30 (1 Mbps, 10 ms)
// - Router1 to rightNode: 10.1.3.0/30 (10 Mbps, 1 ms)
// ===========================================================================
//
/**
* Congestion window change callback
*
* @param oldCwnd Old congestion window.
* @param newCwnd New congestion window.
*/
static void
CwndChange(uint32_t oldCwnd, uint32_t newCwnd)
{
NS_LOG_UNCOND(Simulator::Now().GetSeconds() << "\t" << newCwnd);
}
/**
* Rx drop callback
*
* @param p The dropped packet.
*/
static void
RxDrop(Ptr<const Packet> p)
{
NS_LOG_UNCOND("RxDrop at " << Simulator::Now().GetSeconds());
}
void QueueSizeLogger(uint32_t oldVal, uint32_t newVal)
{
static std::ofstream queueLog("queue-size.csv", std::ios_base::app); // Open in append mode
queueLog << Simulator::Now().GetSeconds() << "," << newVal << std::endl;
}
int main(int argc, char* argv[])
{
CommandLine cmd(__FILE__);
cmd.Parse(argc, argv);
LogComponentEnable("ScriptExample", LOG_LEVEL_ALL);
// In the following three lines, TCP NewReno is used as the congestion
// control algorithm, the initial congestion window of a TCP connection is
// set to 1 packet, and the classic fast recovery algorithm is used. Note
// that this configuration is used only to demonstrate how TCP parameters
// can be configured in ns-3. Otherwise, it is recommended to use the default
// settings of TCP in ns-3.
Config::SetDefault("ns3::TcpL4Protocol::SocketType", StringValue("ns3::TcpNewReno"));
Config::SetDefault("ns3::TcpSocket::InitialCwnd", UintegerValue(1));
Config::SetDefault("ns3::TcpL4Protocol::RecoveryType",
TypeIdValue(TypeId::LookupByName("ns3::TcpClassicRecovery")));
// Create nodes
NodeContainer leftNodes, rightNodes, routers;
routers.Create(2);
leftNodes.Create(1);
rightNodes.Create(1);
///////////////////////////////////////////////////////////////////////////////////
//// 搭配 netanim 使用
/* // Adding Mobility Model
MobilityHelper mobility;
// 設定節點為靜止 (stationary)
mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
// 左邊節點
mobility.Install(leftNodes);
Ptr<MobilityModel> mobLeft = leftNodes.Get(0)->GetObject<MobilityModel>();
mobLeft->SetPosition(Vector(0.0, 0.0, 0.0));
// 右邊節點
mobility.Install(rightNodes);
Ptr<MobilityModel> mobRight = rightNodes.Get(0)->GetObject<MobilityModel>();
mobRight->SetPosition(Vector(100.0, 0.0, 0.0));
// 路由器 0
mobility.Install(routers.Get(0));
Ptr<MobilityModel> mobRouter0 = routers.Get(0)->GetObject<MobilityModel>();
mobRouter0->SetPosition(Vector(30.0, 0.0, 0.0));
// 路由器 1
mobility.Install(routers.Get(1));
Ptr<MobilityModel> mobRouter1 = routers.Get(1)->GetObject<MobilityModel>();
mobRouter1->SetPosition(Vector(70.0, 0.0, 0.0));*/
///////////////////////////////////////////////////////////////////////////////////
// std::vector<NetDeviceContainer> leftToRouter;
// std::vector<NetDeviceContainer> routerToRight;
// Create the point-to-point link helpers and connect two router nodes
PointToPointHelper pointToPointRouter;
pointToPointRouter.SetDeviceAttribute("DataRate", StringValue("1Mbps"));
pointToPointRouter.SetChannelAttribute("Delay", StringValue("10ms"));
// NetDeviceContainer r1r2ND = pointToPointRouter.Install(routers.Get(0), routers.Get(1));
// Create the point-to-point link helpers and connect leaf nodes to router
PointToPointHelper pointToPointLeaf;
pointToPointLeaf.SetDeviceAttribute("DataRate", StringValue("10Mbps"));
pointToPointLeaf.SetChannelAttribute("Delay", StringValue("1ms"));
// leftToRouter.push_back(pointToPointLeaf.Install(leftNodes.Get(0), routers.Get(0)));
// routerToRight.push_back(pointToPointLeaf.Install(routers.Get(1), rightNodes.Get(0)));
// 節點 0 <-> 路由器 0
NetDeviceContainer devices1 = pointToPointLeaf.Install(leftNodes.Get(0), routers.Get(0));
// 路由器 0 <-> 路由器 1
NetDeviceContainer devices2 = pointToPointRouter.Install(routers.Get(0), routers.Get(1));
// 路由器 1 <-> 節點 1
NetDeviceContainer devices3 = pointToPointLeaf.Install(routers.Get(1), rightNodes.Get(0));
// Ptr<RateErrorModel> em = CreateObject<RateErrorModel>();
// em->SetAttribute("ErrorRate", DoubleValue(0.00001));
// devices2.Get(1)->SetAttribute("ReceiveErrorModel", PointerValue(em));
// // 將協定堆疊安裝到所有節點和路由器上
InternetStackHelper stack;
stack.Install(leftNodes);
stack.Install(rightNodes);
stack.Install(routers);
// // Configure IP addresses for each network segment
Ipv4AddressHelper address("10.0.0.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces1 = address.Assign(devices1);
address.NewNetwork();
Ipv4InterfaceContainer interfaces2 = address.Assign(devices2);
address.NewNetwork();
Ipv4InterfaceContainer interfaces3 = address.Assign(devices3);
address.NewNetwork();
Ipv4GlobalRoutingHelper::PopulateRoutingTables();
// TCP 伺服器應用設定 (接收端) (PacketSink)
uint16_t sinkPort = 8080;
Address sinkAddress(InetSocketAddress(interfaces3.GetAddress(1), sinkPort));
PacketSinkHelper packetSinkHelper("ns3::TcpSocketFactory",
InetSocketAddress(Ipv4Address::GetAny(), sinkPort));
ApplicationContainer sinkApps = packetSinkHelper.Install(rightNodes.Get(0));
sinkApps.Start(Seconds(0.)); // Server 啟動
sinkApps.Stop(Seconds(20.));
// TCP 用戶端應用設定 (發送端) (TutorialApp)
Ptr<Socket> ns3TcpSocket = Socket::CreateSocket(leftNodes.Get(0), TcpSocketFactory::GetTypeId());
ns3TcpSocket->TraceConnectWithoutContext("CongestionWindow", MakeCallback(&CwndChange));
Ptr<TutorialApp> app = CreateObject<TutorialApp>();
app->Setup(ns3TcpSocket, sinkAddress, 1040, 1000, DataRate("1Mbps"));
leftNodes.Get(0)->AddApplication(app);
app->SetStartTime(Seconds(1.)); // Client 啟動
app->SetStopTime(Seconds(20.));
// ===================== Queue Size Trace 設定 =====================
// 取得 Router0 出口 (指向 Router1) 的 NetDevice
Ptr<PointToPointNetDevice> router0Dev = DynamicCast<PointToPointNetDevice>(devices2.Get(0));
Ptr<Queue<Packet>> queue = router0Dev->GetQueue();
// 開啟輸出檔案
static std::ofstream queueLog("queue-size.csv");
queueLog << "Time(s),QueueSize(packets)" << std::endl;
// Trace callback:每次 queue size 改變時寫入檔案
// 註冊 callback
queue->TraceConnectWithoutContext("PacketsInQueue", MakeCallback(&QueueSizeLogger));
// Trace
//EnablePcap(std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);
pointToPointLeaf.EnablePcap("mytrace-left", devices1.Get(0), true);
pointToPointLeaf.EnablePcap("mytrace-right", devices3.Get(0), true);
pointToPointRouter.EnablePcap("mytrace-router", devices2.Get(0), true);
devices1.Get(0)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback(&RxDrop));
devices2.Get(0)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback(&RxDrop));
devices3.Get(0)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback(&RxDrop));
// 搭配 netanim 使用
// AnimationInterface anim("anim.xml");
Simulator::Stop(Seconds(20));
Simulator::Run();
Simulator::Destroy();
// 關閉輸出檔案
queueLog.close();
return 0;
}
```
:::
這裡取名 `FirstCode.cc` ,接著將兩份程式碼放置於 scratch 目錄底下,然後於主目錄底下編譯
```shell
$ ./ns3 build
```
執行
```shell
$ ./ns3 run FirstCode
```
:::info
在舊版 ns-3 使用 `./war` configure & build 文件,在新版已使用 `./ns3` 代替,另外注意 `./war --run yourCode` `./ns3 run yourCode` 的不同
:::
## 數據分析
### 輸出 & 分析數據
在執行程式期間,可以將程式的結果統一輸出到 `cwnd.dat` 方便後續進行資料分析
```shell
$ ./ns3 run FirstCode > cwnd.dat 2>&1
```
在資料收集完成後,接著使用 `gnuplot` 將資料彙整成圖片
```shell
$ gnuplot
# 進入 gnuplot
gnuplot> set terminal png size 640,480
# 設定圖片大小
gnuplot> set output "cwnd.png"
# 設定圖片名稱
gnuplot> set format y "%.1t*10^{%T}"
# 設定縱坐標為科學記號
gnuplot> set xlabel "Elapsed Time"
gnuplot> set ylabel "Congestion Window"
# 設定橫縱軸的名稱
gnuplot> plot "cwnd.dat" using 1:2 title 'Congestion Window' with linespoints
# 告知 gnuplot 使用 cwnd.dat 為資料來源做繪圖
gnuplot> exit
```
最後會輸出如下圖 :
<!--
-->

* X 軸: 時間 (Time)。
* Y 軸: 擁塞窗口大小 (Congestion Window),單位是 Bytes
程式碼會產生 ~~`tcp-newreno.tr` 文字檔~~ 與 二進制的`mytrace-0-0.pcap` 日誌檔,兩者可以分別藉由下面的 command line 進行查看
在產生出來的 packet capture 檔案中,檔案名稱會以 `<prefix>-<node>-<device>` 這個邏輯下去命名
<!--
```shell
$ cat tcp-newreno.tr | less
```
-->
### 搭配 wireshark 進行分析
如下匯入 `mytrace-left-2-0.pcap ` 檔案

於工具列選擇 statistics -> TCP Stream Graphs -> Window Scaling

並點選右下角 `Switch Direction` 按鍵,最後輸出

* X 軸: 時間 (Time)。
* Y 軸: 窗口大小 (Window Size),單位是 Bytes (位元組)。
## 同場加映
[NS-3 搭配 NetAnim 可視化網路拓譜](https://hackmd.io/@IsPlpVINQVGLm_dcip1iMg/Hy-85eWRyx)
## 參考
[在 Windows 10 中安裝和使用 Ubuntu 子系統](https://hackmd.io/@CynthiaChuang/WSL-Install-and-Use-the-Ubuntu-subsystem-in-Win10)
[[Github] The Network Simulator, Version 3](https://github.com/nsnam/ns-3-dev-git)
[[Github] Tracing through NS-3](https://github.com/nsnam/ns-3-dev-git/blob/master/doc/tutorial/source/tracing.rst)
[[CSDN] NS3下载与安装](https://blog.csdn.net/qq_43268767/article/details/134276308)
[ns-3 入门 1:介绍与安装](https://cyp0633.icu/post/ns3-intro-installation/)
[[官方文件] TCP models in ns-3](https://www.nsnam.org/docs/release/3.44/models/html/tcp.html#writing-tcp-tests)
[gnuplot 語法解說和示範](https://hackmd.io/@sysprog/Skwp-alOg)