멀티캐스트 개념에 대해 간략히 먼저 정리한다.
- 브로드캐스트란?
일대다 전송이다. TV에서 영상 송출하는 방식을 생각하면 이해하기 쉽다.
- 멀티캐스트란?
브로드캐스트의 오버헤드를 줄이기위해 만들어졌다. 일대다 전송이라는 특징은 같지만, 호스트는 멀티캐스트 그룹 한 곳에만 보내면 된다. (해당 데이터그램에 관심이 있는 수신자가 멀티캐스트 그룹에 join해야한다.) 따라서 데이터그램 한 번의 복사로 모든 link에 전달될 수 있다는 큰 이점이 있다.
멀티캐스트 프로그래밍의 기본 단계에 대해 살펴본다.
전송부와 수신부가 있다.
*[참고] 연결지향통신(ex.tcp)에서는 server와 client라고 칭하지만, 비연결지향(ex.udp)에서는 sender와 receiver로 부른다.
*[참고] tcp 패킷은 segment, udp 패킷은 datagram이라고 칭한다.
- 전송부
1.socket을 생성한다.
2.주소를 설정한다.
3.loopback에 대해 설정한다.
4.multicast 데이터그램을 전송할 로컬 인터페이스를 설정한다.
5.데이터그램을 전송한다.
6.소켓을 닫는다.
- 수신부
1.socket을 생선한다.
2.address 재사용 옵션을 설정한다.
3.로컬 인터페이스 주소에 bind한다.
4.멀티캐스트 그룹에 join한다.
5.데이터그램을 수신한다.
6.소켓을 닫는다.
- loopback이란?
loopback interface는 가상의 네트워크 인터페이스이다. 네트워크 프로토콜을 사용하여 같은 호스트 내에서 서로 통신하려고 할 때 사용한다.
loopback을 활성화하면, 로컬 전달 시 IP 계층에서 패킷이 복사된다. (즉 IP계층에서 loopback된다)
*[참고] OSI 7계층에서 IP datagram은 3계층에 속한다. Network or Internetwork layer
=> 멀티캐스트에서 간단히 이해하자면, 내가 보낸 데이터그램을 내가 수신하기를 원할 때 loopback을 활성화하면 된다.
- Linux C++ loopback 코드 예제
uint_t loop;
setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop, sizeof(loop))
// loop=0 : 루프백 비활성화
// loop=1 : 루프백 활성화
- bind 코드 예제
// ip address and port
std::string addr = "...";
int port = 1024;
// bind
struct sockaddr_in sock_addr;
memset(&sock_addr, 0, sizeof(sock_addr));
sock_addr.sin_family = AF_INET;
sock_addr.sin_port = htons(port);
sock_addr.sin_addr.s_addr = INADDR_ANY; // (1) bind any IP address
//inet_pton(AF_INET, addr.c_str(), &(sock_addr.sin_addr)) // (2) bind specific IP address
bind(sock, (struct sockaddr *)&sock_addr, sizeof(sock_addr))
- bind를 하는 이유?
디바이스는 여러 개의 IP주소를 가질 수 있는데, 소켓이 어떤 IP주소로부터 수신할지 결정한다.
예를 들어 bind할 때 IP Address에 ANY를 넣으면, 현재 디바이스의 모든 IP Address에 들어오는 데이터를 수신하겠다는 의미이다.
네트워크 OSI 7계층과 TCP/IP 프로토콜의 매핑을 보면 이해하기 수월하다.
데이터는 가장 아래 1번 계층에 먼저 도착한다. 물리적 계층의 케이블을 통해 데이터가 들어오고, NIC에서 먼저 수신한다.
- NIC(Network Interface Card) 란?
네트워크 인터페이스 카드. 네트워크를 관장하는 하드웨어이다. PC 뒤쪽에 이더넷 꽂는 구멍을 보면 몇 개의 NIC가 있는지 확인할 수 있다.
NIC와 매핑되어 있는 IP address 테이블이 있다. 패킷의 헤더를 열어보며 다음 계층으로 전달된다.
등등..
- 멀티캐스트 프로그래밍에서 로컬인터페이스 인덱스를 설정하는 경우
(1) IP_MULTICAST_IF
멀티캐스트 전송부에서 특정 네트워크 인터페이스를 지정하여 데이터그램을 전송하고싶을 때
(2) IP_ADD_MEMBERSHIP
멀티캐스트 수신부에서 멀티캐스트 그룹에 가입할 때, 특정 네트워크 인터페이스를 지정 사용하여 수신하기 위해
- 네트워크 인터페이스 인덱스를 얻는 방법
네트워크 인터페이스 이름을 통해 인덱스를 가져올 수 있다.
if_nametoindex()
#include <net/if.h>
unsigned int index = if_nametoindex("eth0");
if (index == 0) {
// error
}
그러나 위와 같이 고정 "eth0"으로 설정하는 것은 유연성이 떨어지므로 좋은 방법이 아니다.
대신 시스템에서 인터페이스 이름을 동적으로 읽어와서 사용하는 것이 더 바람직하다.
- 특정 IP Address의 네트워크 인터페이스 인덱스를 구하는 예제 코드
1. getifaddrs() // 모든 네트워크 정보를 읽어온다.
2. 읽어온 네트워크의 address와 찾으려는 Ip address의 매칭되면
3. if_nametoindex() // 인터페이스 이름을 통해서 인덱스를 얻는다.
#include <arpa/inet.h>
#include <cerrno>
#include <ifaddrs.h>
#include <iostream>
#include <net/if.h> // if_nameindex
#include <string>
#include <string.h>
#include <sysexits.h>
#include <sys/socket.h>
#include <sys/types.h>
inline int getEthInterface(std::string local_addr) {
std::string eth_interface_name;
struct ifaddrs* ptr_ifaddrs = nullptr;
auto result = getifaddrs(&ptr_ifaddrs);
if (result != 0) {
std::cout << "`getifaddrs()` failed: " << strerror(errno) << std::endl;
return EX_OSERR;
}
for (struct ifaddrs* ptr_entry = ptr_ifaddrs; ptr_entry != nullptr; ptr_entry = ptr_entry->ifa_next) {
std::string ipaddress_human_readable_form;
std::string netmask_human_readable_form;
std::string interface_name = std::string(ptr_entry->ifa_name);
sa_family_t address_family = ptr_entry->ifa_addr->sa_family;
if (address_family == AF_INET) {
// IPv4
// Be aware that the `ifa_addr`, `ifa_netmask` and `ifa_data` fields might contain nullptr.
// Dereferencing nullptr causes "Undefined behavior" problems.
// So it is need to check these fields before dereferencing.
if (ptr_entry->ifa_addr != nullptr) {
char buffer[INET_ADDRSTRLEN] = {
0,
};
inet_ntop(address_family, &((struct sockaddr_in*)(ptr_entry->ifa_addr))->sin_addr, buffer, INET_ADDRSTRLEN);
ipaddress_human_readable_form = std::string(buffer);
}
if (ptr_entry->ifa_netmask != nullptr) {
char buffer[INET_ADDRSTRLEN] = {
0,
};
inet_ntop(address_family, &((struct sockaddr_in*)(ptr_entry->ifa_netmask))->sin_addr, buffer, INET_ADDRSTRLEN);
netmask_human_readable_form = std::string(buffer);
}
std::cout << interface_name << ": IP address = " << ipaddress_human_readable_form << ", netmask = " << netmask_human_readable_form << std::endl;
if (ipaddress_human_readable_form == local_addr) {
// FOUND!!
eth_interface_name = interface_name;
break;
}
} else if (address_family == AF_INET6) {
// IPv6
uint32_t scope_id = 0;
if (ptr_entry->ifa_addr != nullptr) {
char buffer[INET6_ADDRSTRLEN] = {
0,
};
inet_ntop(address_family, &((struct sockaddr_in6*)(ptr_entry->ifa_addr))->sin6_addr, buffer, INET6_ADDRSTRLEN);
ipaddress_human_readable_form = std::string(buffer);
scope_id = ((struct sockaddr_in6*)(ptr_entry->ifa_addr))->sin6_scope_id;
}
if (ptr_entry->ifa_netmask != nullptr) {
char buffer[INET6_ADDRSTRLEN] = {
0,
};
inet_ntop(address_family, &((struct sockaddr_in6*)(ptr_entry->ifa_netmask))->sin6_addr, buffer, INET6_ADDRSTRLEN);
netmask_human_readable_form = std::string(buffer);
}
std::cout << interface_name << ": IP address = " << ipaddress_human_readable_form << ", netmask = " << netmask_human_readable_form
<< ", Scope-ID = " << scope_id << std::endl;
if (ipaddress_human_readable_form == local_addr) {
// FOUND!!
eth_interface_name = interface_name;
break;
}
}
}
if (!eth_interface_name.empty()) {
return if_nametoindex(eth_interface_name.c_str());
}
return 0;
}
int main(void) {
struct ipv6_mreq group;
group.ipv6mr_interface = getEthInterface("fde1:53ba:...");
std::cout << "interface index:" << std::to_string(group.ipv6mr_interface) << std::endl;
return 1;
}
아래 출처에서 코드를 가져와 method call구조로 변경했다.
출처: https://dev.to/fmtweisszwerg/cc-how-to-get-all-interface-addresses-on-the-local-device-3pki
참고:
https://www.ibm.com/docs/en/i/7.4?topic=designs-examples-using-multicasting-af-inet