ns3 (3)

햄스터·2025년 4월 19일

NetworkProject

목록 보기
3/5

Multiple Access Protocol

살다보면 여러 Node가 하나의 Channel을 같이 이용하는 경우도 있겠죠?

앞서 봤던 P2P의 경우는 말 그대로 Point To Point이기 때문에,
두개의 Node가 Directly 연결된 경우입니다.

하지만 얼라리? CSMA/CD나, WiFi같은 경우는 하나의 Channel을 여러 Node가 같이 쓰죠.

집에 있는 공유기에 스마트폰 10대가 붙으면 급격히 느려지죠? 다 이런 이유입니다.

이런 네트워크 환경의 경우는 Collision이 발생할 수도 있구요.

그래서,

  • Distributed Protocol이면서, (중앙에서 처리하는 관리인이 없음)
  • Extra Control Channel도 없고,
  • 여러 node들을 질서정연하게 관리해주는

그런 프로토콜이 필요하게 됐고, 그게 Multiple Access Protocol 입니다.

그 중 하나가 Random Access Protocol입니다.
Node가 Packet을 보내고 싶으면,
channel이 비었는지를 확인하고, 비었다면 Full Rate로 패킷을 발사!

Collision이 발생한다면 그걸 Protocol이 정의한대로 잘 처리도 해주구요.

대표적인 예시가 CSMA/CD, CSMA/CA, ALOHA 등인데,
우린 그 중에서 ns3의 CSMA를 배워볼 겁니다.

CSMA (Carrier Sense Multiple Access)

Listen Before Transmit

즉, 보내기 전에 Channel이 비었는지를 확인하라.
CSMA의 기본 정책입니다.

CSMA/CD (Collision Detection)의 경우는,
짧은 시간에 Collision을 감지하고,
충돌이 일어날 것 같으면 바로 멈춥니다.

CSMA/CD는 어떻게 구현되어있을까요?

CSMA in ns-3

아쉽게도 ns3에선 시뮬레이션이기 때문에, 충돌 감지는 안하고, csma만 사용합니다.
CSMA/CD가 아닌 그냥 CSMA에요.

똑같이 NetDevice 설치하듯, CsmaNetDevice를 설치하고 PointToPointChannel 열듯 CsmaChannel을 열어주면 돼요.

CsmaChannel

현실의 경우처럼 패킷이 이동할 빛의 속도를 계산한다던가,
Collision을 determine하는 방법을 고려한다던가 하지 않습니다.

어디까지나 Simulation이잖아요.

또, P2P가 아니기에 하나의 Channel에 여러 Device가 붙을 수 있습니다.

Data Rate와 Delay를 P2P처럼 설정할 수 있고,
DataRate는 기본값이 4294967295bps (INT_MAX*2 + 1),
Delay는 기본값이 0.0ns입니다.

CsmaNetDevice

똑같아요. 단, 속성들이 조금 다양합니다.

  • Address : Device의 MAC주소입니다. 기본치는 ff:ff:ff:ff:ff:ff입니다.
  • Mtu : MAC 단위에서의 최대 Transmission Unit입니다. 기본치는 1500입니다.
  • SendEnable : device에서 '보내기'를 켤지 끌지를 정합니다.
  • ReceiveEnable : device에서 '받기'를 켤지 끌지를 정합니다.
  • ReceiveErrorModel : packet loss를 강제로 만드는, 시뮬레이션용 에러 모델을 정합니다.
  • TxQueue : Transmit Queue로 쓸 수 있는 큐입니다.

CsmaHelper

CsmaNetDeviceCsmaNetChannel을 세트로 쉽게 묶을 수 있는 헬퍼입니다.

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

처럼 정의해서 쓸 수 있고,
P2P에서의 NetDeviceContainer에서 했듯이,
NetDeviceContainer = Helper.Install(NodeContainer)의 형태를 따르며,

NetDeviceContainer devices;
csmaDevices = csma.Install(csmaNodes)

처럼 사용 / 설치가 가능합니다.

Bridge Model

Node들에 대해서 설명할 때, Host도 노드고, Router도 노드라고 설명했는데,
실제로 ns3에선 Router를 구현할 때, 살짝 차이를 둡니다.

그게 바로 Bridge입니다.
Node에 BridgeNetDevice를 붙이면, 그 Node는 이제 Bridge (= Switch, Router)
의 역할을 합니다.

일반적으로는 learning bridge 알고리즘이 ns3에 내장되어 있어, 그 방식을 사용해요.
그렇기 때문에, 맨 처음에는 들어오는 패킷을 모든 port로 broadcast합니다.
(맨 처음엔 패킷을 어디로 보낼지 모르니까요)

또, 이 Bridging은 CsmaNetDevice, WifiNetDevice 같은 NetDevice에서만 작동합니다.

Bridge도 Channel, Helper, NetDevice가 있습니다.

BridgeChannel

BridgeChannel은 아무것도 하지 않습니다. (네?)
속성도 없고, trace source도 없습니다.

BridgeNetDevice

  • Mtu : MAC level Transmission Unit이고, 초기값은 1500입니다.
  • EnableLearning : Bridge의 Learning mode를 켤지 정합니다. 초기값은 True
  • ExpirationTime : 학습한 MAC State를 얼마나 기억할지를 정합니다.
    초기값은 3000억ns (= 5분)입니다.

BridgeHelper

Bridge를 쉽게 짓기 위해 도와주는 Helper입니다.

헬퍼니까, 아까처럼
NetDeviceContainer = Helper.Install(NodeContainer)의 형태를 따를 것 같지만,
곰곰히 생각해보면 Bridge는 하나의 Node를 Router로 바꾸는거니까, 뭔가 좀 다를 것 같죠.
그래서,

NetDeviceContainer = Helper.Install(Node, NetDeviceContainer)의 형태를 띕니다.

즉, 이런 느낌이에요.

BridgeHelper bridge;
NetDeviceContainer bridgeDev;

bridgeDev = bridge.Install(
	nodes.Get(0),
    NetDeviceContainer(dev01.Get(0), dev02.Get(0))
);

즉, switch화 하고 싶은 node와, 그 node가 보유한 NetDevice들을 Container에 담아 Bridge화 하는 것이죠.


예제로 알아보자!

OnOffApplication으로,
Flow 1 :n0 -> n1 UDP 5Mbps, 1-10s
Flow 2 :n3 -> n0 UDP 10Mbps, 3-13s


다음과 같은 경우를 만들어보겠습니다.
일반 Node를 4개, Switch Node를 1개 만들어야하고,
CSMA Link를 4개 만들어야 합니다.

Includes

#include <iostream>
#include <fstream>
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/applications-module.h"
#include "ns3/internet-module.h"
#include "ns3/bridge-module.h"
#include "ns3/csma-module.h"

using namespace ns3;

아주 기본적인 include들입니다.
우리는 bridge도, csma link도 써야 하니 추가적인 모듈을 2개 더 추가해줍니다.

Create Nodes

NodeContainer terminals;
terminals.Create(4);

NodeContainer csmaSwitch;
csmaSwitch.Create(1);

기본 Node 4개, Switch 1개를 만듭니다.

Create Helpers and NetDevices

CsmaHelper csma;
csma.SetChannelAttribute("DataRate", DataRateValue(5000000));
csma.SetChannelAttribute("Delay", TimeValue(MilliSeconds(2)));

NetDeviceContainer terminalDevices;
NetDeviceContainer switchDevices;

Node별로 NetDevice도 있어야 하고, csma를 쓸거니 csmaHelper도 있어야겠죠?
DataRate를 설정할 때, StringValue("50Mbps")처럼도 쓸 수 있고,
DataRateValue(5000000)처럼도 쓸 수 있는게 인상적이네요.

Connect Switch node and Terminal Node

for (int i = 0 ; i < 4 ; i++)
{
	NetDeviceContainer link = csma.Install(NodeContainer(terminals.Get(i), csmaSwitch));
    terminalDevices.Add(link.Get(0))
    switchDevices.Add(link.Get(1))
}

당연히 switch에는 NetDevice가 4개 붙어야겠죠?

NetDeviceContainer = Helper.Install(NodeContainer)의 형태를 잘 지키며,
terminals[0]번 Node와 csmaSwitch를 이어주고,
NetDeviceContainer에는 이제 0번Node의 NetDevice, switch-0 NetDevice가 들어있을 테니, 각각 뽑아서 NetDeviceContainer에 넣어줍니다.

그걸 4번 하는거에요.

Make Bridge

Ptr<Node> switchNode = csmaSwitch.Get(0);
BridgeHelper bridge;
bridge.Install(switchNode, switchDevices);

평범한 Node인 Switch를 Bridge로 업그레이드 해줍니다.
앞서 switchDevices에 switch-0번 넷카드, switch-1번 넷카드 ... 이걸 다 넣어놨었죠.
걔네를 전부 Bridge용 NetDevice로 승급시켜주는 작업입니다.

Install Internet Stack && Assign IP Addresses

InternetStackHelper internet;
internet.Install(terminals);

Ipv4AddressHelper ipv4;
ipv4.SetBase("10.1.1.0", "255.255.255.0");
ipv4.Assign(terminalDevices);

모든 terminal 노드에 프로토콜 스택을 달아줍니다.
switch에는 달아줄 필요 없어요. 얘는 그냥 중간자 역할입니다.

또, terminalDevices에 IP주소를 부여합니다.
당연합니다. Node가 아닌 NIC에 주소를 부여해야죠.
하나의 Node에 여러 주소가 있을 수 있으니까요.

Make Flow from node 0 -> node 1

uint16_t port = 9;

OnOffHelper onoff ("ns3::UdpSocketFactory", Address(InetSocketAddress(Ipv4Address("10.1.1.2", port)));
onoff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1.0]"));
onoff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=1.0]));
onoff.SetAttribute(DataRateValue(5000000));

ApplicationContainer app1 = onoff.Install(terminals.Get(0));
app1.Start(Seconds(1.0));
app1.Stop(Seconds(10.0));

OnOffApplication Helper를 하나 설정하고,
UDP, 받을사람의 주소 (10.1.1.2니까 node 1이겠죠?) 를 설정한 후,

Install을 해서 Application을 등록하고, 시작합니다.

Create Sink

PacketSinkHelper sink("ns3::UdpSocketFactory", Address(InetSocketAddress(Ipv4Address::GetAny(), port)));

ApplicationContainer sinkApp1 = sink.Install(terminals.Get(1));
sinkApp1.Start(Seconds(1.0));
sinkApp1.Get(0)->TraceConnect("Rx", "Flow1", MakeCallback(&RxTime));

n1이 패킷을 받았으면 소모를 해줘야겠죠.
n1을 Sink로 등록합니다.
Sink는 '모든 IP 주소에서 오는 패킷을 수신'해야 하니까,
Ipv4Address::GetAny()를 등록합니다.

마지막 줄은 추후에 설명하겠습니다.

Create Another Flow from node 3 -> node 0

onoff.SetAttribute("Remote", AddressValue(InetSocketAddress("10.1.1.1"), port)));
onoff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=0.3]"));
onoff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0.7]));
onoff.SetAttribute(DataRateValue(1000000));

ApplicationContainer app2 = onoff.Install(terminals.Get(3));
app2.Start(Seconds(3.0));
app2.Stop(Seconds(13.0));

ApplicationContainer sinkApp2 = sink.Install(terminals.Get(1));
sinkApp2.Start(Seconds(1.0));
sinkApp2.Get(0)->TraceConnect("Rx", "Flow2", MakeCallback(&RxTime));

똑같습니다. 다만 차이점은, 아까의 onoffHelper를 재사용하고 있기 때문에, Address를 "Remote"를 이용해 재등록해줬다는 점이 있습니다.

OnOffHelper onoff = ("udp...", Address ...)
ApplicationContainer app1 = onoff.Install(node_1)

이후,
onoff.SetAttribute("Remote", NewAddress ...)
ApplicationContainer app2 = onoff.Install(node_2)

처럼 사용하는 것은? 아주 무난하게 가능한 사용법입니다.

Enable PCAP and Run

csma.EnablePcapAll("star", false);

Simulator::Stop(Seconds(15));
Simulator::Run();
Simulator::Destroy();

Pcap을 켜고, 15초간 켜는 것으로 마무리합니다.

예? LOG를 안 켰는데 뭐에 대한 Pcap을 찍나요?

그 코드를 마지막에 설명해보겠습니다.

How to Log? (RxTime)

다음과 같은 로깅 함수를 정의해봅시다.

static void Rxtime(std::string ctx, Ptr<const Packet> p, const Address &a)
{
	static double byte1, byte2 = 0;
    if (ctx == "Flow1") {
    	bytes1 += p->GetSize();
        NS_LOG_UNCOND("1\t" << Simulator::Now().GetSeconds() <<
        "\t" << bytes1 * 8 / 1e6 / (Simulator::Now().GetSeconds() - 1));
    }
    else if (ctx == "Flow2") {
    	bytes2 += p->GetSize();
        NS_LOG_UNCOND("2\t" << Simulator::Now().GetSeconds() <<
        "\t" << bytes2 * 8 / 1e6 / (Simulator::Now().GetSeconds() - 3));
    }
}

무슨 코드일까요?

바로, '획득한 패킷에 대해 획득량과, 획득한 시간 정보를 로그로 출력'하는 코드입니다.

앞서 이런 코드가 있었죠?
sinkApp1.Get(0)->TraceConnect("Rx", "Flow1", MakeCallback(&RxTime));

sinkApp1.Get(0)은 우리가 만든 Sink Application인데요.
그 Sink Application의 Trace Source에 콜백을 거는겁니다.

Rx는 패킷을 수신했을 때 트리거가 되고, Flow1은 콜백 식별 이름,
MakeCallback(&RxTime)은 수신 이벤트가 발생했을 때 실행할 함수 포인터인거죠.

즉, 해당 Sink에 패킷이 수신되면?
RxTime이 실행되고?
같이 전달된 정보를 RxTime이 분석해서 로그를 찍으며?
우리는 그 로그를 NS_LOG_UNCOND로 출력하기 때문에,
stderr로 출력됩니다.

이렇게 출력되는 수많은 로그들을,

./waf --run fileName 2> log.dat으로 리다이렉션해서 저장하면,

다음과 같이 어느 Flow가 뭘 받았느냐에 따라 구분이 되는데,

이걸 Linux 기본 command 중 하나인 awk로 구분하면,
awk '$1== "1" {print $2 "\t" $3}' log.dat > flow1.dat처럼,

'첫 인자가 1일 때 '시간 byte수' 형식으로 출력해서 flow1.dat으로 저장'
같은 명령이 수행 가능하고,

그렇게 flow별로 정리된 데이터를
python이나, Gnuplot으로 시각화가 가능합니다.

python은 뭐 그렇다 치고, gnuplot을 사용한다면

plot "flow1.dat" using 1:2 title 'Flow1' with linespoints, "flow2.dat" using 1:2 title 'Flow2' with linespoints처럼
뭐 거의 자연어로 입력해도 그래프가 그려지는게 신기하긴 하더라구요.

profile
햄스터가 세상을 지배한다.

0개의 댓글