/ NETWORK

NS-3 기본 사용법

ns-3 코드를 작성하기 전에 몇 가지 중심 개념과 시스템의 abstraction들에 대해 알아본다.

NS-3의 Concepts

Node 및 Application

ns-3는 리얼 월드가 아닌 시뮬레이팅된 세계에서 노드들 간의 통신을 시뮬레이션 한다. 그리고 이 노드들은 어떠한 activity를 수행하는 유저 프로그램의 abstraction인 application 을 실행시킨다.

이 abstraction은 C++에서 Application 클래스로 표현된다.

Application 클래스는 시뮬레이션에서 유저레벨 어플리케이션의 표현을 관리하는 메소드들을 제공한다.

공식 튜토리얼에서는 UdpEchoClientApplcationUdpEchoServerApplication 이라는 특수한 Application의 하위 클래스를 사용한다. 이 어플리케이션들은 시뮬레이팅된 네트워크 패킷을 생성하고 echo하는 클라이언트와 서버 역할을 수행한다.

Channel

리얼 월드에서는 호스트가 네트워크에 연결되면, 네트워크 내에서는 데이터가 channel을 통해 이동한다. 예를 들어, 이더넷 케이블을 벽에 꽂으면 컴퓨터는 이더넷 통신 채널에 연결된 것이다.

ns-3의 시뮬레이션 월드에서는 Node를 통신 채널을 표현하는 객체에 연결시킨다. 여기서 기본적인 통신 서브네트워크 abstraction은 channel 이라 불리며 C++ 클래스 Channel 로 표현된다.

Channel 클래스는 통신 서브네트워크 객체를 관리하고 노드들을 연결하는 메소드를 제공한다. Channel을 상속하여 만들어질 수 있는 특수한 클래스로는 wire와 같은 심플한 것부터 큰 이더넷 스위치, 또는 무선 네트워크의 경우 복잡한 3차원 공간까지 다양한 표현이 가능하다.

튜토리얼에서는 Channel의 특수한 하위 클래스 중 CsmaChannel, PointToPointChannel, WifiChannel을 사용한다.

예를 들어 CsmaChannelcarrier sense multiple access 통신 매체를 구현하는 통신 서브네트워크를 모델링한다. 이는 곧 이더넷과 유사한 기능성을 제공한다.

Device

리얼 월드에서 하드웨어에 장착되는 Network Interface Card(NIC,일명 ‘랜 카드’)에 해당하는 것은 device로 분류된다. 이러한 네트워크 디바이스는 네트워크 디바이스 driver에 의해 작동하는데, 이런 드라이버들을 net devices라고 한다. Unix와 Linux에서는 eth0과 같은 인터페이스 이름을 통해 net device들을 참조할 수 있다.

ns-3에서는 net device abstraction이 소프트웨어 드라이버와 시뮬레이션된 하드웨어를 모두 커버한다. net device는 Node설치되어 Channel들을 통해 시뮬레이션 내의 다른 Node들과 통신할 수 있도록 한다. 실제 컴퓨터와 같이, Node는 여러 개의 NetDevice들을 통해 하나 이상의 Channel 과 연결될 수 있다.

net device의 abstraction은 NetDevice C++ 클래스로 표현된다. NetDevice 클래스는 ChannelNode 객체들과의 연결을 관리할 수 있는 메소드들을 제공한다.

튜토리얼에서는 NetDevice의 특수한 하위 클래스 중 CsmaNetDevice, PointToPointNetDevice, WifiNetDevice을 사용한다.

이더넷 NIC가 이더넷 네트워크를 위해 디자인된 것과 같이, CsmaNetDeviceCsmaChannel 에 대응하기 위해 디자인되었고, 나머지도 마찬가지로 각각 대응되는 채널과 함께 작동할 수 있도록 디자인 되었다.

Topology Helpers

NetDevice를 만들고, MAC 주소를 추가하고, net device를 Node 에 추가하고, 노드의 프로토콜 스택을 설정하고, NetDeviceChannel에 연결하는 등의 작업을 하려면 ns-3의 여러 core operation을 필요로 할 수 있다. 이때 여러 개의 디바이스를 멀티포인트 채널들에 추가하고 여러 네트워크를 묶어 인터넷을 구현하려면 더 복잡해진다.

ns-3는 이런 복잡한 작업을 묶어서 구현이 더욱 편리하게 해주는 topology helper 객체들을 제공한다고 한다.


처음 만나는 ns-3 스크립트

ns-3.<버전> 디렉토리 하위의 examples/tutorial 경로에 가면, first.cc 파일이 있다. 이 파일은 두 개의 노드 간의 단순한 point-to-point 링크를 생성하고 단일 패킷을 노드 간 echo 하는 기능을 하는 스크립트이다. 이 스크립트를 line by line으로 살펴 보자.

Module Includes

#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/internet-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/applications-module.h"

위에서 include하는 모듈들은 high-level 유저들을 위해 여러 가지 모듈들을 재귀적으로 include 하는 큰 모듈들이다. 효율적인 방식은 아니지만 코드가 간결해지고 작성이 쉬워진다.

각각의 헤더 파일은 ns-3 소스코드 내 헤더파일들과의 혼동을 방지하기 위해 빌드 과정에서 빌드 디렉토리 하위의 ns3 폴더 아래에 위치하게 된다.

Ns3 네임스페이스

first.cc의 다음 줄은 네임스페이스 선언이다.

using namespace ns3;

ns-3 프로젝트는 ns3 이라는 C++ 네임스페이스에서 구현되었다. 이것은 모든 ns-3 관련 선언을 글로벌 네임스페이스 바깥의 범위에서 그룹짓고 다른 코드와 통합을 돕는다.

C++ using statement는 ns-3 네임스페이스를 현재 (global) declarative region으로 정의한다. 따라서 이제부터는 ns-3 코드를 작성하기 위해 ns3:: 라는 scope resolution operator를 사용할 필요가 없다.

로깅

다음 줄은

NS_LOG_COMPONENT_DEFINE("FirstScriptExample");

인데 API 공식 문서를 함께 참조할 좋은 타이밍이다.

공식 홈페이지에서 DOCUMENTS -> Models에 위치한 위의 링크에는 가장 먼저 위에서 언급한 module 헤더파일들에 대한 설명이 연결되어 있는 항목들이 나열되어 있다.

ns-3의 로깅 서브시스템에 대한 것은 Using the Logging Module 섹션에 설명이 나와 있다.

위의 statement에 대한 것은 Core 모듈의 Debugging tools 책에서 Logging 페이지를 살펴 보면 찾을 수 있다.

문서 내용을 간단히 요약하면, 해당 줄은 FirstScriptExample 이라는 로깅 컴퍼넌트를 선언하여 콘솔 메시지 로깅을 해당 이름에 대한 참조를 통해 활성화 또는 비활성화할 수 있도록 한다.

main 함수

int
main(int argc, char *argv[])
{

Command-line argument를 받는 평범한 C++ 프로그램의 메인 함수 선언이다.

다음 줄은 시간 해상도를 기본값인 1ns로 설정한다.

Time::SetResolution(Time::NS);

이 해상도는 표현 가능한 가장 작은 시간 값이다.

그리고 이 해상도는 정확히 한 번만 바꿀 수 있다.

다음 두 줄은 Echo 클라이언트와 Echo 서버 어플리케이션 내에 만들어지는 두 개의 로깅 컴포넌트를 활성화한다.

LogComponentEnable("UdpEchoClientApplication", LOG_LEVEL_INFO);
LogComponentEnable("UdpEchoServerApplication", LOG_LEVEL_INFO);

위의 Logging 컴포넌트 문서를 읽어 보면 많은 수의 로깅 디테일 레벨에 대한 설명을 볼 수 있다.

위의 두 줄은 에코 클라이언트 및 서버에 대해 그 레벨들 중 INFO 레벨로 설정한다. 이는 결과적으로 어플리케이션이 시뮬레이션 중 패킷 송수신 시마다 메시지를 프린트하게 한다.

이제 topology를 만들어 본격적으로 시뮬레이션을 돌리러 가 보자. topology helper 객체들을 사용하여 작업을 쉽게 수행할 수 있다.

Topology Helpers 사용

NodeContainer

다음 두 줄은 드디어 각각 클라이언트와 서버에 해당하는 ns-3 Node 두 개를 생성한다.

NodeContainer nodes;
nodes.Create(2);

여기서 NodeContainer 클래스에 대한 문서를 찾아보자. 공식 홈페이지의 DOCUMENTATION 아래 Doxygen 페이지에서 Classes 탭을 통해서도 주어진 클래스에 대한 문서를 찾아갈 수 있다.

NodeContainer 라는 topology는 Node를 간편하게 생성, 관리, 접근하게 도와준다. 위 코드를 보면 직관적으로 알 수 있듯이, nodes라는 NodeContainer 객체의 Create() 메소드를 통해 두 개의 Node를 생성하고 있다. 각 Node 들에 대한 포인터는 따로 내부적으로 저장된다.

아직 이때 생성된 노드들은 아무 것도 하지 않는다. 다음 단계에서 노드들을 네트워크로 연결할 것이다. 네트워크의 가장 단순한 형태는 단일 point-to-point 링크이다. 우리가 만들 링크에 해당한다.

PointToPointHelper

이 스크립트에서는 PointToPointNetDevicePointToPointChannel 의 설정 및 연결을 위해 단일 PointToPointHelper 를 이용한다.

다음 세 줄은,

PointToPointHelper pointToPoint;
pointToPoint.SetDeviceAttribute("DataRate", StringValue("5Mbps"));
pointToPoint.SetChannelAttribute("Delay", StringValue("2ms"));

이며 첫째 줄에서 pointToPoint 라는 PointToPointHelper 객체를 생성한다.

다음 줄은 PointToPointNetDevice 객체를 만들 때 pointToPoint 객체를 통해 5Mbps 라는 값을 DataRate로 사용하도록 설정한다.

DataRatePointToPointNetDeviceAttribute 중 하나에 대응된다. 문서를 보면 디바이스에 대해 정의된 Attribute들을 찾아볼 수 있다. 그 중 하나가 DataRate 이다.

그 다음 줄은 DataRate와 유사하게 Delay 라는 Attribute2ms 값으로 설정한다. 이는 잇따라 만들어지는 p2p 채널의 propagation delay에 해당하는 것이다.

NetDeviceContainer

Node를 만들기 위해 NodeContainer 라는 topology helper를 사용한 것과 마찬가지로 생성될 NetDevice 오브젝트를 저장할 리스트가 필요하므로 NetDeviceContainer를 사용할 것이다.

NetDeviceContainer devices;

이제 PointToPointHelper 에게 디바이스를 생성, 설정, 설치하도록 요청해 보자.

devices = pointToPoint.Install(nodes);

PointToPointHelper 클래스는 채널의 속성(Attributes)을 정의하고, PointToPointNetDevice를 노드 컨테이너에 있는 노드들에 설치해주는 기능을 한다. 그리고 이때 설치되는 PointToPointNetDevice 는 노드 하나에 여러 개가 설치될 수 있다. 정확히는 그 노드와 연결된 P2P 채널(또는 링크)의 개수와 대응된다. 만약 P2P 링크가 여러 개 존재한다면, 링크 수만큼 Install() 메소드를 호출하여 각 링크에 연결된 노드 2개가 담긴 노드 컨테이너를 argument로 넘겨줘야 한다. 여기서는 단일 링크만을 다루므로, 한 번의 호출로 충분하다.

이 한 줄로 nodes 라는 NodeContainer 에 있는 모든 노드들에게 네트워크 디바이스가 설치된다. 그리고 PointToPointChannel이 하나 생성되고 두 개의 PointToPointNetDevices가 attach된다. PointToPointHelper로 만들어진 객체들의 Attribute들은 헬퍼에서 미리 설정된 Attribute들로 초기화된다.

이후 두 노드에 대한 각각의 네트워크 디바이스는 위에서 생성한 devices 라는 디바이스 컨테이너에 저장된다.

위에서 정의한 Attribute에 따라 두 노드는 5Mbps의 속도와 2ms의 전송 딜레이를 갖는 P2P 채널을 통해 통신하게 된다.

InternetStackHelper

노드와 디바이스 설정이 완료되었으나, 우리 노드는 아직 프로토콜 스택이 설치가 안 됐다.

호엥호엥 다음 두 줄은 그걸 해결하는 부분이다.

InternetStackHelper stack;
stack.Install(nodes);

InternetStackHelper 역시 topology helper로써, PointToPointHelper가 연결한 p2p net device의 인터넷 스택을 구현한다. 단 여기서는 채널 수나 인터페이스 수와 관계 없이, 노드 당 한 번만 설치해 주면 된다.

Install() 메소드는 NodeContainer 객체를 parameter로 받는다. 실행되면, 노드 컨테이너에 있는 각 노드에 인터넷 스택 (TCP, UDP, IP, MAC 등)을 구현한다.

Ipv4AddressHelper

이제 각 노드들에 IP 주소를 할당해 보자. 이번에도 topology helper를 사용할 것이다. 유저에게 보이는 유일한 API는 베이스 IP네트워크 마스크를 설정하는 것이다.

다음 두 줄을 보면,

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

Address helper 객체를 선언하고 네트워크 주소 10.1.1.0 과 마스크 255.255.255.0을 사용해 주소 할당을 시작하도록 하는 것을 알 수 있다. 기본적으로 주소들은 1부터 시작하여 1씩 증가하면서 할당 된다. 따라서 위의 경우 10.1.1.1 부터 시작하여 다음은 10.1.1.2 등의 주소가 노드들에게 할당될 것이다.

그 다음 줄은 다음과 같다.

Ipv4InterfaceContainer interfaces = address.Assign(devices);

이 줄이 실제로 주소 할당을 실행한다. IP주소와 디바이스는 Ipv4Interface 객체를 통해 연결된다. 우리가 net device의 리스트가 필요한 것처럼, Ipv4Interface 도 여러 개 보관할 수 있는 리스트가 필요하다. Ipv4InterfaceContainer가 이 기능을 제공한다.

드디어 point-to-point 네트워크가 구축되었다. 프로토콜 스택도 만들어졌고, IP 주소까지 할당되었다. 이제 어플리케이션을 만들어 트래픽을 생성해 보자.

Applications

이 예제에서는 앞서 언급한 바와 같이 UdpEchoClientApplcationUdpEchoServerApplication 이라는 특수한 Application의 하위 클래스를 사용한다.

그리고 여기서도 마찬가지로 편의를 위해 helper 오브젝트를 사용한다. UdpEchoServerHelperUdpEchoClientHelper 라는 놈을 쓸 것이다.

UdpEchoServerHelper

UdpEchoServerHelper echoServer(9);

ApplicationContainer serverApps = echoServer.Install(nodes.Get(1));
serverApps.Start(Seconds(1.0));
serverApps.Stop(Seconds(10.0));

위 코드는 UDP echo 서버 어플리케이션을 전에 만든 노드들 중 하나에 설치하는 작업을 수행한다.

첫째 줄은 UdpEchoServerHelper 객체를 선언한다. 물론 이것은 어플리케이션 생성을 편리하게 해주는 helper이지 어플리케이션 그 자체가 아니다. helper constructor에는 필수적인 Attribute들을 넣는 것이 convention이라고 한다. 이 경우, 헬퍼는 클라이언트도 알고 있는 포트 번호가 제공되지 않는 한 아무런 유용한 작업을 수행할 수 없으므로 constructor에 대한 parameter로 포트 번호를 입력해 준 것이다. constructor는 전달받은 값으로 SetAttribute를 호출한다. 물론 나중에 SetAttribute를 직접 호출하여 “PortAttribute를 임의로 변경할 수도 있다.

Install 메소드는 서버 어플리케이션을 노드에 설치하는데, 코드로 보이는 것과 달리 실제로는 NodeContainer 객체를 parameter로 받는다. 위의 경우, nodes.Get(1) 로 반환된, 1번 노드에 대한 스마트 포인터 Ptr<Node> 가 C++의 implicit conversion을 통해 이름 없는 NodeContainer에 담겨져 그 이후 Install로 전달되는 것이다.

Install 메소드는 헬퍼에 의해 설치된 어플리케이션들에 대한 포인터를 갖고 있는 컨테이너를 반환한다.

어플리케이션은 트래픽 생성을 ‘시작’할 시간과 ‘끝’낼 optional할 시간이 필요하다. 이 시간들은 ApplicationContainer의 메소드 StartStop에 의해 설정 가능하다. 이 메소드들은 Time 객체를 parameter로 받는다.

serverApps.Start(Seconds(1.0));
serverApps.Stop(Seconds(10.0));

위 두 줄은 C++의 explicit conversion 으로 argument를 전달했다. double 값인 1.010.0Seconds로 캐스팅하여 전달했다. 서버 어플리케이션은 시뮬레이션이 시작되고 1초 시점에 시작되어 9초가 흐른 후, 10초 시점에 종료된다.

시뮬레이션 이벤트를 10초 시점에 선언했으므로, 전체 시뮬레이션은 최소한 10초는 실행됨을 알 수 있다.

UdpEchoClientHelper

클라이언트 역시 서버와 비슷한 방식으로 설정 가능하다.

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

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

그런데 echo 클라이언트는 5개의 서로 다른 Attribute를 설정해 줄 필요가 있다. 첫 두 개는 UdpEchoClientHelper 생성자로 설정한다. parameter는 convention 상 순서대로 “RemoteAddress”, “RemotePortAttribute에 해당한다.

노드 컨테이너에 있는 1번째 노드에 서버 어플리케이션을 설치하였고, Ipv4 인터페이스 역시 그에 대응하여 설치하였으므로 1번째 인터페이스의 주소를 argument로 전달하고, 포트 번호는 아까 설정한 9번으로 주었다.

MaxPacketsAttribute는 클라이언트가 시뮬레이션이 실행되는 동안 보낼 수 있는 최대 패킷 수를 지정한다. “IntervalAttribute는 패킷 사이에 클라이언트가 얼마나 대기해야 하는지 지정한다. “PacketSizeAttribute는 패킷 payload가 얼마나 큰지 지정한다. 위의 경우 클라이언트는 1024바이트의 패킷 하나를 전송한다.

서버에서 처럼 클라이언트도 시작과 종료 시간을 지정해준다. 위의 경우 서버보다 1초 늦게 클라이언트 어플리케이션이 실행된다.

Simulator

이제 실제로 시뮬레이션을 돌리는 일만 남았다.

Simulator::Run();

위 코드는 시뮬레이션을 실행한다. 위 함수가 호출되면, 시스템은 스케줄링된 이벤트들의 리스트를 검사한 후 해당되는 이벤트들을 실행하게 된다.

serverApps.Start(Seconds(1.0));
serverApps.Stop(Seconds(10.0));
...
clientApps.Start(Seconds(2.0));
clientApps.Stop(Seconds(10.0));

아까 위에서 등록했던 이벤트들이 처음 전달 받은 시간에 발생하게 된다. 그리고 각 이벤트는 내부적으로 실행과 동시에 다른 많은 이벤트들을 스케줄링하게 된다. 서버와 클라이언트가 시작되고, 클라이언트가 서버에게 echo request 패킷 하나를 보내고 나서 시간이 흐르면 연속된 이벤트들이 모두 처리되면서 서서히 시뮬레이션은 idle 상태가 되어 갈 것이다. 이제 남은 이벤트는 10초 시점의 서버와 클라이언트의 Stop뿐이다. Stop까지 실행하게 되면 더 이상 실행할 이벤트가 없으므로 Simulator::Run() 이 리턴하게 된다. 이렇게 시뮬레이션이 완료된다.

  Simulator::Destroy();
  return 0;
}

이제 남은 것은 클리닝뿐이다. 유저는 단순히 Simulator::Destroy(); 호출만 하면 된다. ns-3 시스템이 알아서 마무리에 필요한 복잡한 작업을 처리해준다.

시뮬레이션 정지 시점

임의로 Simulator::Stop(stopTime);을 호출하지 않는 한, 시뮬레이션은 이벤트 스케줄러에 이벤트가 스케줄링되어 있는 동안 돌아가다가 처리할 이벤트가 없으면 자동으로 종료된다.

Simulator::Stop을 반드시 사용해야만 할 때가 있는데, 그건 self-sustaining(recurring) event가 존재할 때이다. 이 이벤트들은 자기 자신을 계속해서 rescheduling 하므로 이벤트 큐를 비게 하지 않는다.

recurring event를 포함하는 다양한 모듈과 프로토콜들이 있다. 예를 들면 다음과 같다.

FlowMonitor - periodic check for lost packets

RIPng - periodic broadcast of routing tables update etc.

이럴 때는 Simulator::Stop이 필요하다. 추가로, ns-3이 emulation mode에 있을 때, RealtimeSimulator가 기계의 시계와 시뮬레이션 시계를 동기화하기 위해 사용되므로 Simulator::Stop이 프로세스를 멈추기 위해 필요하다.

시뮬레이션 도중 이벤트 큐가 비게 되어 자동으로 종료가 예상되는 프로그램에서도 원한다면 명시적으로 Simulator::Stop을 호출할 수 있다.

주의할 점은, 반드시 Simulator::Run 이전에 Simulator::Stop을 호출해야 한다는 것이다. 그렇지 않으면 Simulator::Run이 메인 프로그램에 stop을 실행하도록 컨트롤을 넘겨주지 않을 수 있다.

+  Simulator::Stop(Seconds(11.0));
   Simulator::Run();
   Simulator::Destroy();
   return 0;
 }

자신의 스크립트 빌드하기

scratch 폴더에 자신의 스크립트를 넣고 ns3를 실행하면 자동으로 빌드된다.

예로 first.ccscratch 폴더에 넣어 보자.

cd ../..
$ cp examples/tutorial/first.cc scratch/myfirst.cc

이제 ns3로 빌드해 보자.

./ns3 build

이제 ns3로 자신의 스크립트를 실행할 수 있다.

./ns3 run scratch/myfirst

참고 문헌