[소켓 #14] 멀티캐스트 & 브로드캐스트

이석환·2023년 6월 10일

Socket Programming

목록 보기
15/18

1. 멀티캐스트(Multicast)

멀티캐스트 방식의 데이터 전송은 UDP를 기반으로 한다.
따라서 UDP 서버/클라이언트의 구현방식이 매우 유사하다.
차이점이 있다면 UDP에서의 데이터 전송은 하나의 목적지를 두고 이뤄지지만 멀티캐스트에서의 데이터 전송은 특정 그룹에 가입되어 있는 다수의 호스트이다.
즉, 멀티캐스트 방식을 이용하면 단 한 번에 데이터 전송으로 다수의 호스트에게 데이터를 전송할 수 있다.

1-1. 멀티캐스트의 데이터 전송방식과 멀티캐스트 트래픽 이점

멀티캐스트의 데이터 전송 특성

  • 멀티캐스트 서버는 특정 멀티캐스트 그룹을 대상으로 데이터를 딱 한 번 전송한다.
  • 딱 한 번 전송하더라도 그룹에 속하는 클라이언트는 모두 데이터를 수신한다.
  • 멀티캐스트 그룹의 수는 IP 주소 범위 내에서 얼마든지 추가가 가능하다.
  • 특정 멀티캐스트 그룹으로 전송되는 데이터를 수신하려면 해당 그룹에 가입하면 된다.

여기서 말하는 멀티캐스트 그룹이란 클래스 D에 속하는 IP주소(224.0.0.0 ~ 239.255.255.255)를 나타내는 말이다.
즉, 멀티캐스트 그룹에 가입한다는 것은 다음과 같다.
"클래스 D에 속하는 IP주소 중에서 239.234.218.234를 목적지로 전송되는 멀티캐스트 데이터에 관심이 있으므로, 이 데이터를 수신하겠다."

멀티캐스트는 UDP를 기반으로 한다고 하였다.
즉, 멀티캐스트 패킷은 형태가 UDP 패킷과 동일하다.
다만 일반적인 UDP 패킷과 달리 하나의 패킷만 네트워크상에 띄워 놓으면 라우터들은 이 패킷을 복사해서 다수의 호스트에 이를 전달한다.


그룹 AAA로 전송된 하나의 멀티캐스트 패킷이 라우터들의 도움으로 AAA 그룹에 가입한 모든 호스트에 전송되는 과정이다.
그림만 봤을 때는 트래픽에 상당한 무리가 가는 것으로 보인다.
하나의 패킷이 여러 라우터를 통해 빈번히 복사되는 것처럼 보이기 때문이다.
하지만 실제로는 이렇지 않다.
왜냐하면 하나의 영역에 동일한 패킷이 둘 이상 전송되지 않기 대문이다.
기존의 방식대로라면 1000개의 호스트에 파일을 전송하려면, 총 1,000회의 파일을 전송해야 한다.
열 개의 호스트가 하나의 네트워크로 묶여있더라도 말이다.
하지만 멀티캐스트 방식으로 파일을 전송하면 한 번만 전송해만 된다.
1,000개의 호스트를 묶고 있는 라우터가 1,000개의 호스트에게 파일을 복사해주기 때문이다.
이러한 성격 때문에 멀티캐스트 방식의 데이터 전송은 "멀티미디어 데이터의 실시간 전송"에 주로 사용된다.

1-2. 라우팅(Routing)과 TTL(Time to Live), 그리고 그룹으로의 가입 방법

멀티캐스트 패킷의 전송을 위해서는 TTL이라는 것의 설정 과정을 반드시 거쳐야 한다.
TTL이란 Time to Live의 약자로써 "패킷을 얼마나 멀리 전달할 것인가"를 결정하는 주 요소가 된다.
TTL은 정수로 표현되며, 이 값은 라우터를 하나 거칠 때마다 1씩 감소한다.
그리고 이 값이 0이 된다면 패킷은 더 이상 전달되지 못하고 소멸된다.
따라서 TTL을 너무 크게 설정하면 네트워크 트래픽에 좋지 않은 영향을 준다.
반대로 너무 적게 설정할 경우 목적지까지 도달하지 않는 문제가 발생한다.

설정 방법
TTL은 [소켓 #09]에서 설명한 소켓의 옵션 설정을 통해 이뤄진다.
TTL 설정

  • 프로토콜 레벨 : IPPROTO_IP
  • 옵션 이름 : IP_MULTICAST_TTL
int send_sock;
int time_live = 64;

send_sock = socket(PF_INET, SOCK_DGRAM,0);
setsocketopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_live, sizeof((time_live));

멀티캐스트 그룹으로의 가입 역시 소켓의 옵션 설정을 통해 이뤄진다.
그룹 가입 방법

  • 프로토콜 레벨 : IPPROTO_IP
  • 옵션 이름 : IP_ADD_MEMBERSHIP
  • ip_mreq 구조체에 그룹 정보를 저장
int recv_sock;
struct ip_mreq join_adr;

recv_sock = socket(PF_INET, SOCK_DGRAM, 0);

join_adr.imr_multiadr.s_addr = "가입할 멀티캐스트 그룹 주소";
join_adr.imr_interface.s_addr = "그룹에 가입할 호스트의 주소 정보";

//멀티캐스트 그룹 가입
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*)&join_adr, sizeof(join_adr));

위에 나오는 구조체 ip_mreq에 대해 설명하겠다.

struct im_mreq
{
	struct in_addr imr_multiaddr;
    struct in_addr imr_interface;
}

구조체 in_addr은 [소켓 #03]을 참고하자.
첫 번째 멤버 imr_multiaddr에는 가입할 그룹의 IP주소를 넣는다.
두 번째 멤버 imr_interface에는 그룹에 가입하는 소켓이 속한 호스트의 IP주소를 명시하는데, INADDR_ANY를 이용하는 것도 가능하다.

1-3. 멀티캐스트 Sender와 Receiver의 구현

  • Sender : 파일에 저장된 뉴스 정보를 AAA 그룹으로 방송(Broadcasting)한다.
  • Receiver : AAA 그룹으로 전송된 뉴스 정보를 수신한다.

1-3-1. Sender

Sender는 UDP 소켓을 생성하고 주소로 데이터를 전송만 하면 된다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define TTL 64
#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int send_sock;
	struct sockaddr_in mul_adr;
	int time_live=TTL;
	FILE *fp;
	char buf[BUF_SIZE];
	
	
	if(argc!=3){
		printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
		exit(1);
	}
  	
	send_sock=socket(PF_INET, SOCK_DGRAM, 0);
	memset(&mul_adr, 0, sizeof(mul_adr));
	mul_adr.sin_family=AF_INET;
	mul_adr.sin_addr.s_addr=inet_addr(argv[1]); 
	mul_adr.sin_port=htons(atoi(argv[2]));       
	
	setsockopt(send_sock, IPPROTO_IP, 
		IP_MULTICAST_TTL, (void*)&time_live, sizeof(time_live));
	if((fp=fopen("news.txt", "r"))==NULL)
		error_handling("fopen() error");

	while(!feof(fp))
	{
		fgets(buf, BUF_SIZE, fp);
		sendto(send_sock, buf, strlen(buf), 
			0, (struct sockaddr*)&mul_adr, sizeof(mul_adr));
		sleep(2);
	}
	fclose(fp);
	close(send_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 데이터를 전송할 주소 정보 설정에서 멀티캐흐트 주소로 설정해야 한다.
  • UDP 소켓을 기반으로 데이터 전송이 이뤄지므로 sendto 함수를 사용

1-3-2. Receiver

Receiver는 그룹의 가입 과정이 필요하다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int option = 1;
	int recv_sock;
	int str_len;
	char buf[BUF_SIZE];
	struct sockaddr_in adr;
	struct ip_mreq join_adr;
	
	if(argc!=3) {
		printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
		exit(1);
	 }
  
	recv_sock=socket(PF_INET, SOCK_DGRAM, 0);
 	memset(&adr, 0, sizeof(adr));
	adr.sin_family=AF_INET;
	adr.sin_addr.s_addr=htonl(INADDR_ANY);	
	adr.sin_port=htons(atoi(argv[2]));
	
    setsockopt(recv_sock, SOL_SOCKET, SO_REUSEPORT, (void*)&option, sizeof(option));
    
	if(bind(recv_sock, (struct sockaddr*) &adr, sizeof(adr))==-1)
		error_handling("bind() error");
	
	join_adr.imr_multiaddr.s_addr=inet_addr(argv[1]);
	join_adr.imr_interface.s_addr=htonl(INADDR_ANY);
  	
	setsockopt(recv_sock, IPPROTO_IP, 
		IP_ADD_MEMBERSHIP, (void*)&join_adr, sizeof(join_adr));
	  
	while(1)
	{
		str_len=recvfrom(recv_sock, buf, BUF_SIZE-1, 0, NULL, 0);
		if(str_len<0) 
			break;
		buf[str_len]=0;
		fputs(buf, stdout);
	}
	close(recv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 구조체 ip_mreg형 변수를 초기화 하고 있다.
  • 그 후 IP_ADD_MEMBERSHIP을 이용해서 멀티캐스트 그룹에 가입하고 있다. 이제 위에서 지정한 멀티캐스트 그룹으로 전송되는 데이터를 수신할 준비가 끝났다.
  • recvfrom 함수 호출을 통해서 멀티캐스트 데이터를 수신하고 있다. 참고로 전송한 호스트의 주소 정보를 알 필요가 없다면, 다섯 번째와 여섯 번째 인자로 NULL과 0을 전달하면 된다.

실행 결과

참고로 필자는 receiver가 2개 이상 실행이 되도록 코드를 수정 후 실행한 결과이다.
이때 setsockopt 함수를 활용하여 이미 실행중인 소켓에 접근하여 같은 port를 사용할 수 있도록 구현하였다.
추가로 멀티캐스트는 어디까지나 방송의 개념이기 때문에 sender와 receiver의 실행 순서는 상관없지만, receiver를 늦게 실행하면, 그 이 전에 전송된 멀티캐스트 데이터는 수신이 불가능하다.

2. 브로드캐스트(Broadcast)

브로드캐스트는 한 번에 여러 호스트에게 데이터를 전송한다는 점에서 멀티캐스트와 유사하다.
그러나 전송이 이뤄지는 범위에서 차이가 난다.
멀티캐스트는 서로 다른 네트워크 상에 존재하는 호스트라 할지라도, 멀티캐스트 그룹에 가입만 되어 있으면 데이터의 수신이 가능하다.
반면 브로드캐스트는 동일한 네트워크 내에 존재하는 호스트로, 데이터의 전송 대상이 제한된다.

2-1. 브로드캐스트의 이해와 구현 방법

브로드캐스트는 동일한 네트워크에 연결되어 있는 모든 호스트에게 동시에 데이터를 전송하기 위한 방법이다.
멀티캐스트와 동일하게 UDP를 기반으로 데이터를 송수신한다.
데이터 전송의 대상이 호스트가 아닌 네트워크이다.

데이터 전송 시 사용되는 IP주소의 형태에 따라서 다음과 같이 두 가지 형태로 구분된다.

  • Direct 브로드캐스트
    IP 주소에서 네트워크 주소를 제외한 호스트 주소를 모두 1로 설정
    해당 네트워크로 데이터가 전송
    예를 들어 네트워크 주소가 192.12.34인 네트워크에 연결되어 있는 모든 호스트에게 데이터를 전송하려면 192.12.34.255로 데이터를 전송하면 된다.
    이렇듯 특정 지역의 네트워크에 연결된 모든 호스트에게 데이터를 전송하려면 해당 방식으로 데이터를 전송하면 된다.

  • Local 브로드캐스트
    255.255.255.255라는 IP주소가 특별히 예약되어 있다.
    예를 들어서 네트워크 주소가 192.32.24인 네트워크에 연결되어 있는 호스트가 IP주소 255.255.255.255를 대상으로 데이터를 전송하면, 192.32.24로 시작하는 IP주소의 모든 호스트에게 데이터가 전달된다.

브로드캐스트 Sender와 Receiver는 언뜻 봐서는 UDP 예제와 구분이 잘 안 된다.
즉, 데이터 송수신에 사용되는 IP주소가 UDP 예제와의 유일한 차이점이다.
다만 기본적으로 생성되는 소켓은 브로드캐스트 기반의 데이터 전송이 불가능하도록 설정되어 있기 때문에 다음의 코드를 통해 이를 변경할 필요가 있다.

int send_sock;
int so_broad = 1; // SO_BROADCAST의 옵션 정보를 1로 변경하기 위한 변수 초기화

send_sock = socket(PF_INET, SOCK_DGRAM, 0);

setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*)&so_broad, sizeof(so_broad));
  • setsockopt 함수 호출을 통해서 SO_BROADCAST의 옵션 정보를 변수 bcast에 저장된 값인 1로 변경하는데 이는 브로드캐스트 기반의 데이터 전송이 가능함을 의미한다.
    위의 함수 호출은 데이터를 전송하는 Sender에서만 필요할 뿐, Receiver에서는 필요가 없다.

2-2 : 브로드캐스트 기반의 Sender와 Receiver의 구현

2-2-1 : Sender

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int send_sock;
	struct sockaddr_in broad_adr;
	FILE *fp;
	char buf[BUF_SIZE];
	int so_brd=1;
	
	if(argc!=3) {
		printf("Usage : %s <Boradcast IP> <PORT>\n", argv[0]);
		exit(1);
	}
  
	send_sock=socket(PF_INET, SOCK_DGRAM, 0);	
	memset(&broad_adr, 0, sizeof(broad_adr));
	broad_adr.sin_family=AF_INET;
	broad_adr.sin_addr.s_addr=inet_addr(argv[1]);
	broad_adr.sin_port=htons(atoi(argv[2]));
	
	setsockopt(send_sock, SOL_SOCKET, 
		SO_BROADCAST, (void*)&so_brd, sizeof(so_brd));	
	if((fp=fopen("news.txt", "r"))==NULL)
		error_handling("fopen() error");

	while(!feof(fp))
	{
		fgets(buf, BUF_SIZE, fp);
		sendto(send_sock, buf, strlen(buf), 
			0, (struct sockaddr*)&broad_adr, sizeof(broad_adr));
		sleep(2);
	}

	close(send_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • UDP 예제와의 유일한 차이점은 옵션정보를 변경하는 것이다.

2-2-2 : Receiver

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int option = 1;
	int recv_sock;
	struct sockaddr_in adr;
	int str_len;
	char buf[BUF_SIZE];
	
	if(argc!=2) {
		printf("Usage : %s  <PORT>\n", argv[0]);
		exit(1);
	 }
  
	recv_sock=socket(PF_INET, SOCK_DGRAM, 0);
	
	memset(&adr, 0, sizeof(adr));
	adr.sin_family=AF_INET;
	adr.sin_addr.s_addr=htonl(INADDR_ANY);	
	adr.sin_port=htons(atoi(argv[1]));
	
	setsockopt(recv_sock, SOL_SOCKET, SO_REUSEPORT, (void*)&option, sizeof(option));

	if(bind(recv_sock, (struct sockaddr*)&adr, sizeof(adr))==-1)
		error_handling("bind() error");
  
	while(1)
	{
		str_len=recvfrom(recv_sock, buf, BUF_SIZE-1, 0, NULL, 0);
		if(str_len<0) 
			break;
		buf[str_len]=0;
		fputs(buf, stdout);
	}
	
	close(recv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

실행 결과

필자는 멀티캐스트 예제와 동일하게 두 개 이상의 Receiver가 실행되게 하기 위해서 SO_REUSEPORT를 사용하여 옵션 정보를 변경하였다.
해당 결과는 Local 브로드캐스트의 실행 결과이다.

3. Multicast 채팅방

Multicast를 이용한 채팅 프로그램을 만들어보았다.
조건은 다음과 같다.

  • sender와 receiver를 하나의 소스코드로 구현
  • fork() 함수 호출을 이용한 멀티프로세스 생성 후 통신에 사용
    -자식 프로세스 : 데이터 수신(recvfrom())
    -부모 프로세스 : 데이터 송신(sendto())
  • 프로그램 실행 시 사용자 이름을 입력 받아서 메시지를 전송할 때 함께 전송
    -전송 형태 : "[사용자 이름] 입력 메시지"
  • 'q' or 'Q'를 입력하면 프로그램 종료
    -참고)setsockopt(소켓, IPPROTO_IP, IP_DROP_MEMBERSHIP, 주소, 주소 크기)

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

#define MAXLINE 1024

#define TTL 64

int main(int argc, char * argv[]) {
    int send_s, recv_s;
    int pid;
    unsigned int option = 1;
    struct sockaddr_in mcast_group;
    struct ip_mreq mreq;
    char line[MAXLINE];
    char name[10];
    int n, len;
    int time_live = TTL;
    int no = 1;

    if(argc != 4) {
        printf("Usage : %s <multicast_address> <port> <My_name> \n", argv[0]);
        exit(0);
    }
    sprintf(name, "[%s]", argv[3]);

    memset(&mcast_group, 0, sizeof(mcast_group));
    mcast_group.sin_family = AF_INET;
    mcast_group.sin_port = htons(atoi(argv[2]));
    mcast_group.sin_addr.s_addr = inet_addr(argv[1]);
    if ( (recv_s=socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        printf("create receive socket error!!\n");
        exit(0);
    }
    
   
    mreq.imr_multiaddr = mcast_group.sin_addr;
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);
    if (setsockopt(recv_s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))<0){
        printf("add membership error!!\n");
        exit(0);
    }
    
    if (setsockopt(recv_s, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option)) <0) {
        printf("reuse setsockopt() error!!\n");
        exit(0);
    }
    
    if (bind(recv_s, (struct sockaddr*)&mcast_group, sizeof(mcast_group)) < 0) {
        printf("bind() error!!\n");
        exit(0);
    }

    
    if ((send_s=socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        printf("create send socket error!!\n");
        exit(0);
    }

    setsockopt(send_s, IPPROTO_IP, IP_MULTICAST_LOOP, &no, sizeof(no));
    setsockopt(send_s, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_live, sizeof(time_live));

    
    if ((pid=fork()) < 0) {
        printf("fork() error!!\n");
        exit(0);
    
    } else if (pid == 0) {
        struct sockaddr_in from;
        char message[MAXLINE+1];
        for (;;) {
        len = sizeof(from);
        if ((n=recvfrom(recv_s, message, MAXLINE, 0, (struct sockaddr*)&from, &len))<0) {
            printf("recvfrom() error!\n");
            exit(0);
        }
        message[n] = 0;
        printf("Received Message: %s", message);
        }
   
    } else {
        char message [MAXLINE+1], line[MAXLINE+1];
        while (fgets(message, MAXLINE, stdin) != NULL) {
            sprintf(line, "%s %s", name, message);
            len = strlen(line);
            if((!strcmp(message, "q\n") || !strcmp(message,"Q\n"))){
                if(setsockopt(recv_s, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0){
                    printf("add membership error!!\n");
                    exit(0);
                }
                exit(0);
            }
            if(sendto(send_s, line, strlen(line), 0, (struct sockaddr*)&mcast_group, sizeof(mcast_group)) < len) {
                printf("sendto() error!!\n");
                exit(0);
            }
        }
    }
    return 0;
}
  • 실행 결과

참고 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍
Git : https://github.com/im2sh/Socket_Programming/tree/main/lab13

profile
반갑습니다.

0개의 댓글