ns3 (4)

햄스터·2025년 4월 19일

NetworkProject

목록 보기
4/5

Applications (2)

ns3::Applications은 모든 ns3 내의 App에게 공통 상속됩니다.
이렇게 모든 App에게 공통되는 Public API를 제공하는 이유는, 역시
App의 Start과 Stop에 있어 일관성을 유지하기 위함이죠.

Application과 비슷하게, ApplicationHelper들은 Install 메서드를 지원하며
Simulation Script의 간소화를 추구합니다.

TypeId

ns3의 모든 class들은 TypeId라고 불리는, 메타데이터를 기록하는 class를 가질 수 있습니다.
TypeId는 해당 class의 메타데이터를 담을 수 있는데,
가령

  • class를 구분하는 unique string
  • Base class
  • 접근가능한 Constructor
  • Public하게 접근 가능한 Attributes

이런 정보들을 담을 수 있죠.

모든 헬퍼 시스템이 이 TypeId 기반이라, 매우 중요합니다.

TypeId
UdpEchoServer::GetTypeId (void)
{
  static TypeId tid = TypeId ("ns3::UdpEchoServer")
    .SetParent<Application> ()
    .SetGroupName("Applications")
    .AddConstructor<UdpEchoServer> ()
    .AddAttribute ("Port", "Port on which we listen for incoming packets.",
                   UintegerValue (9),
                   MakeUintegerAccessor (&UdpEchoServer::m_port),
                   MakeUintegerChecker<uint16_t> ())
    .AddTraceSource ("Rx", "A packet has been received",
                     MakeTraceSourceAccessor (&UdpEchoServer::m_rxTrace),
                     "ns3::Packet::TracedCallback")
    .AddTraceSource ("RxWithAddresses", "A packet has been received",
                     MakeTraceSourceAccessor (&UdpEchoServer::m_rxTraceWithAddresses),
                     "ns3::Packet::TwoAddressTracedCallback")
  ;
  return tid;
}

다음 코드는 ns3UdpEchoServer의 TypeId 정보를 담습니다.
보시면 식별자인 ns3::UdpEchoServer부터,
Application을 부모로 둔다는 .SetParent<Application>(),
Applications 그룹에 속한다는 .SetGroupName("Applications"),
기본 생성자 추가를 의미하는 .AddConstructor<UdpEchoServer>(),
헬퍼가 쓸 수 있는 Attribute를 정하는 .AddAttribute,
(이 경우는 기본값이 9번, m_port와 연관되어 있으며, 유효성은 uint16_t로 체크)

그 외에 TraceConnect로 콜백 연결을 돕는 .AddTraceSource까지.

전부 다 TypeId에 포함되어 있죠.

우리는 이 TypeId를 GetTypeId()로 호출할 수 있습니다.

위의 코드도 잘 보면 함수명이 GetTypeId()죠.

이제 기초적인걸 했으니, 직접 App을 만들어봅시다.

Create New App

NewApp을 만들어봅시다. Sender Mode, Receiver Mode가 존재하고,
Packet의 수, Data Rate 정도를 설정할 수 있습니다.
UDP Traffic Generator입니다.

당연히 New App을 만들면, 상응하는 Helper도 만들어줘야겠죠.

우선 전반적으로 내 App에서 쓸 변수와 함수들을 정의해줍시다.

NewApp.h

class NewApp: public Application
{
public:
	static TypeId GetTypeId(void);
    NewApp();
    virtual ~NewApp();

private:
	virtual void StartApplication(void);
    virtual void StopApplication(void);
    
    void ScheduleTx(void);
    void SendPacket(void);
    void HandleRead(Ptr<Socket> socket);
    
    bool m_mode;
    Address m_address;
    uint32_t m_nPackets;
    DataRate m_dataRate;
    
    Ptr<Socket> m_socket;
    uint32_t m_packetSize;
    uint32_t m_packetsSent;
    EventId m_sendEvent;
    bool m_running;
};

당연히 갖춰야 하는 GetTypeId()와, 생성자, 소멸자는 public으로 두고,

반드시 만들어야 하는
virtual void StartApplication(), virtual void StopApplication()도 override합니다.

각 함수들을 정의해봅시다.

NewApp.cc

TypeId
NewApp::GetTypeId() {
  static TypeId tid = TypeId ("ns3::NewApp")
      .SetParent<Application> ()
      .AddConstructor<NewApp>()
      .AddAttribute ("Mode", "The mode : Sender(true), Receiver(false)",
	  BooleanValue(false),
	  MakeBooleanAccessor(&NewApp::m_mode),
	  MakeBooleanChecker())
      .AddAttribute ("Address", "The Address",
	  AddressValue(),
	  MakeAddressAccessor(&NewApp::m_address),
	  MakeAddressChecker())
      .AddAttribute ("NPackets", "The total number of packets to send",
	  UintegerValue(10000),
	  MakeUintegerAccessor(&NewApp::m_nPackets),
	  MakeUintegerChecker<uint32_t>())
      .AddAttribute ("DataRate", "The data rate",
	  DataRateValue(DataRate("500kb/s")),
	  MakeDataRateAccessor(&NewApp::m_dataRate),
	  MakeDataRateChecker())
      ;
  return tid;
}

우선 GetTypeId()입니다. 얘는 무조건 있어야 하죠?
중요한 내용들 (메타데이터)를 담은 TypeId인데, 여기서 알 수 있는 내용들은

  • 이 App의 명시적 이름은 ns3::NewApp
  • 이 App은 생성자가 있고, Application을 상속받음
  • 이 App은 mode를 Attribute로 수정할 수 있고, boolean이며, m_mode 변수와 연결되어 있음
  • 이 App은 Address를 Attribute로 수정할 수 있고, Address 타입이며, m_address 변수와 연결되어 있음
  • 이 App은 NPackets를 Attribute로 수정할 수 있고, uint32_t 타입이며, 기본값은 10000이고, m_nPackets 변수와 연결되어 있음
  • 이 App은 DataRate를 Attribute로 수정할 수 있고, DataRate 타입이며, 기본값은 500Kb/s이고, m_dataRate 변수와 연결되어 있음

정도입니다.

NewApp::NewApp()
    :m_socket(0),
     m_packetSize(1000),
     m_packetsSent(0),
     m_running(false)
{
     NS_LOG_FUNCTION (this);
}

NewApp::~NewApp()
{
  m_socket = 0;
}

생성자와 소멸자입니다.
생성자는 여러 필수변수들을 초기화 해줍니다.
지금 보면 어차피 m_mode, m_address, m_nPackets, m_dataRate는 위에서
TypeId 선언할 때 다 default값이 갖춰져서 선언되어서 그런지는 모르겠는데,

그 변수들은 생성자에 안 보이네요.

그리고 NS_LOG_FUNCTION도 걸어준 모습입니다.

void NewApp::StartApplication()
{
    NS_LOG_FUNCTION (this);
    if(m_mode == true)
    {
        if(!m_socket){
	    TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
	    m_socket = Socket::CreateSocket(GetNode(), tid);
	    m_socket ->Bind();
	    m_socket ->Connect(m_address);
	}
	m_running = true;
	SendPacket();
    } else {
	if(!m_socket){
	    TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
	    m_socket = Socket::CreateSocket(GetNode(), tid);
	    m_socket ->Bind(m_address);
	    m_socket ->Listen();
	    m_socket ->ShutdownSend();
	    m_socket ->SetRecvCallback(MakeCallback(&NewApp::HandleRead, this));
	}
    }
}

무조건 만들어야 하는 StartApplication입니다.
Attribute로 설정된 mode로 Sender와 Receiver를 구분합니다.

Sender는 ns3::UdpSocketFactory 내에서 TypeId를 주워온 후,
그걸 이용해서 Socket을 만듭니다.
Socket을 Bind시키고, 지정된 address에 연결까지 합니다.

Receiver도 ns3::UdpSocketFactory에서 TypeId를 주워온 후,
그걸 이용해 소켓을 만들고,
m_address에 바인딩 후 듣습니다.
Socket의 Send Mode를 끄고,
패킷 수신 시 HandleRead 함수를 실행하게끔 콜백을 걸어놓습니다.

void NewApp::SendPacket(void)
{
    Ptr<Packet> packet = Create<Packet> (m_packetSize);

    m_socket ->Send(packet); 
    if(++m_packetsSent < m_nPackets) 
    {
        ScheduleTx();
    }
}

SendPacket 함수입니다.
Packet을 만들고, socket을 이용해 패킷을 발사합니다.
발사할 수 있는 packet 수보다 발사한 패킷 수가 적다면, 새 전송을 Schedule합니다.

void NewApp::ScheduleTx()
{
    if(m_running)
    {
	Time tNext (
	    Seconds (m_packetSize*8/static_cast<double>(m_dataRate.GetBitRate())));
	m_sendEvent = Simulator::Schedule(tNext, &NewApp::SendPacket, this);
    }
}

새 전송을 Schedule하는 ScheduleTx() 함수입니다.
앱이 돌고 있다면, 전송 간격을 계산하여, 다음 sendEvent를 예약해서,
SendPacket을 해당 타이밍에 실행되게 스케줄 시킵니다.

간단한 UDP 보내는 앱인데도 고려할 게 좀 있죠.

void NewApp::HandleRead(Ptr<Socket> socket)
{
  Ptr<Packet> packet;
  Address from;
  while ((packet = m_socket ->RecvFrom(from)))
  {
  	if (packet->GetSize() > 0)
    {
    	NS_LOG_INFO("NewApp Received Packet");
    }
  }
}

HandleRead 앱입니다.
단순하게 socket에서 뭐 받은 것 같으면 INFO 로그만 찍고 있죠?

void NewApp::StopApplication()
{
  m_running = false;
  if(m_sendEvent.IsRunning())
  {
      Simulator::Cancel (m_sendEvent);
  }
  if(m_socket)
  {
      m_socket ->Close();
  }
}

필수로 만들어야 하는 StopApplication입니다.
m_running 상태를 끄고,
만약 다음 sendEvent가 예정되어 있다면 Simulator에서 없애죠.
그 후 socket을 닫습니다.

NewAppHelper.h

class NewAppHelper 
{
  public:
    NewAppHelper (bool mode, Address address);
    void SetAttribute (std::string name, const AttributeValue &value);
    ApplicationContainer Install (Ptr<Node> node) const;
    ApplicationContainer Install (std::string nodeName) const;
    ApplicationContainer Install (NodeContainer c) const;
  private:
    Ptr<Application> InstallPriv (Ptr<Node> node) const;
    ObjectFactory m_factory;
};

헬퍼에선 Install을 반드시 지원해야 하며,
앞서 정의한 수많은 Attribute를 설정할 수 있게 SetAttribute 정도도 지원해줘야 합니다.

NewAppHelper.cc

NewAppHelper::NewAppHelper (bool mode, Address address) {
    m_factory.SetTypeId(NewApp::GetTypeId());
    m_factory.Set("Mode", BooleanValue(mode));
    m_factory.Set("Address", AddressValue(address));
}

헬퍼의 생성자입니다. ObjectFactorym_factory에 많은 값을 담고,
나중에 이를 이용해서 NewApp을 생성하고자 하는게 보입니다.

void NewAppHelper::SetAttribute (std::string name, const AttributeValue &value)
{
    m_factory.Set(name, value);
}

SetAttribute입니다. 값을 받아서, m_factory에 대입합니다.
일단은 다 그렇게 진행하나봐요.

NewAppHelper::InstallPriv (Ptr<Node> node) const
{
  Ptr<Application> app = m_factory.Create<NewApp> ();
  node ->AddApplication (app);

  return app;
}

InstallPriv 함수입니다.
NewAppHelper 내부에서 실제로 m_factory를 이용해 NewApp을 만들고,
노드에 붙이는 핵심 함수입니다.

얘는 좀 이따가 Install에 의해서 호출될거에요.

ApplicationContainer NewAppHelper::Install(Ptr<Node> node) const 
{
    return ApplicationContainer(InstallPriv(node));
}

ApplicationContainer NewAppHelper::Install (std::string nodeName) const
{
  Ptr<Node> node = Names::Find<Node> (nodeName);
  return ApplicationContainer (InstallPriv (node));
}

ApplicationContainer NewAppHelper::Install (NodeContainer c) const
{
  ApplicationContainer apps;
  for (NodeContainer::Iterator i = c.Begin (); i != c.End (); ++i)
  {
      apps.Add (InstallPriv (*i));
  }

  return apps;
}

앞서 혹시 그런 궁금증 가진 적 있나요?

Install은 보통 NetDeviceContainer = Helper.Install(NodeContainer)던데,
그러면 Node 1개에 설치하고 싶어도 NodeContainer 안에 넣어야 할까?

해보면 알겠지만, 실제로는 그렇지 않구요.
그 이유가 여기서 나오네요.
Helper Function은 NodeContainer 외에도 Node 객체나, 심지어는 Node 이름까지도 받을 수 있습니다.

아무쪼록 받아서, 내부에 있는 InstallPriv를 실행하는 걸 목표로 삼고 있죠.

아무래도 Priv 붙은 함수들은 다 외부에선 직접호출이 불가하니까요.

생성 끝! 실제로 써볼까?

아마 다음과 같이 사용하면 되겠죠?

int main(int argc, char* argv[])
{
	....
    
    uint16_t port = 8080;
    Address destination(InetSocketAddress(interfaces.GetAddress(1), port));
    
    NewAppHelper sender(true, destination);
    sender.SetAttribute("NPackets", UintegerValue(10));
    ....
    
    ApplicationContainer senderApp = sender.Install(nodes.Get(0));
    ....
}

Receiver측도 비슷할거구요!
mode만 false로 해서 헬퍼 생성자를 호출하면 되겠네요.

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

0개의 댓글