책정리/윈도우 네트워크 프로그래밍

6장 UDP 서버/클라이언트

GONII 2015. 4. 20. 15:24
  • UDP 서버/클라이언트 구조

    TCP와 UDP 프로토콜은 전송 계층 프로톸ㄹ이라는 점에서 다음과 같은 공통점을 지닌다

    • 포트 번호를 이용하여 주소를 지정한다

      두 애플리케이션이 통신하기 위해 반드시 포트 번호를 결정해야 한다

    • 데이터 오류를 체크한다

      IP가 제공하는 오류 체크 기능은 프로토콜 동작을 위해 반드시 필요한 부분에만 국한된다. 즉 IP헤더에 대한 오류는 체크하지만 데이터에 대한 오류는 체크하지 않는다. 반면, TCP와 UDP는 헤더와 데이터에 대한 오류를 체크한다.

    TCP와 UDP의 차이점은 다음과같다.

항목

TCP

UDP

1

연결형(connection-oriented) 프로토콜

연결이 성공해야 통신 가능

비연결형(connectionless) 프로토콜

연결 없이 통신 가능  

2

데이터 경계를 구분하지 않음

바이트 스트림(byte-stream) 서비스

데이터 경계를 구분한

데이터그램(datagram) 서비스

3

신뢰성 있는 데이터 전송

데이터를 재전송함

비신뢰적인 데이터 전송

데이터를 재전송하지 않음

4

1 대 1통신(unicast)

1 대 1 통신(unicast)
1 대 다 통신(broadcast)
다 대 다 통신(multicast)

UDP의 항목별 특징을 소켓 함수 관점에서 정리하면 다음과 같다

항목1 : 연결을 하지 않으므로 connect( )함수를 사용하지 않는다. 그러나 몇 가지 이유로 인해 connect( ) 함수를 사용하는 경우도 있다.

항목2 : 데이터 경계 구분을 위한 작업을 애플리케이션이 하지 않아도 된다.

항목3 : 프로토콜 수준에서 신뢰성 있는 데이터 전송을 보장하지 않으므로, 필요하다면 애플리케이션 수준에서 신뢰성 있는 데이터 전송 기능을 구현해야 한다.

항목4 : 간단한 소켓 함수 호출 절차만 따르면 다자 간 통신을 쉽게 구현할 수 있다.

   

UDP는 데이터 재전송과 데이터 순서 유지를 위한 작업을 하지 않는다. 도착한 데이터에 오류가 있다고 판단하면 이 데이터를 애플리케이션에 보내지 않고 그대로 삭제해버린다. 애플리케이션은 데이터에 오류가 있어서 버려졌다는 사실을 알지 못한다. 한편 TCP는 데이터의 순서 유지를 위해 각 바이트마다 번호를 부여하지만 UDP는 이러한 기능을 제공하지 않는다. 따라서 UDP를 이용하는 애플리케이션에서 신뢰성 있는 데이터 전송을 하려면 데이터 재전송 + 데이터 순서 유지, 두 기능을 반드시 구현해야 한다.

동작원리

UDP서버는 TCP서버와 달리 하나의 소켓으로 다수의 클라이언트에 대한 서비스를 할 수 있다.

UDP서버와 UDP클라이언트가 통신을 수행하는 원리는 다음과 같다

  • 서버는 소켓을 생성한 후 클라이언트가 데이터를 보내기를 기다린다. 이때 서버가 사용하는 소켓은 특정 포트 번호(예를 들면 9000)와 결합되어(bind) 있어서 이 포트 번호로 보내는 데이터만 수신할 수 있다.

  • 클라이언트는 접속 과정 없이 곧바로 서버와 데이터를 주고 받는다.(sendto, recvfrom)

  • 또 다른 클라이언트 역시 접속 과정 없이 곧바로 서버와 데이터를 주고 받는다.(sendto, recvfrom)

  • 'UDP 클라이언트#n'의 경우처럼 한 클라이언트가 두 개 이상의 소켓을 사용하여 서버와 통신하는 것도 가능하다.

[실습] 간단한 UDP 서버/클라이언트

서버와 클라이언트의 동작은 다음과 같다

  • 서버 : 클라이언트가 보낸 데이터를 받아(recvfrom), 이를 문자열로 간주하여 무조건 화면에 출력한다.(printf) 그리고 받은 데이터를 변경 없이 다시 클라이언트에 보낸다.(sendto)
  • 클라이언트 : 사용자가 키보드로 입력한 문자열을(fgets) 서버에 보낸다(sendto) 서버가 받은 데이터를 그대로 되돌려 보내면, 클라이언트는 이를 받아(recvfrom) 화면에 출력한다(printf)

  • 예제 UDPServer

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

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

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

if( sock == INVALID_SOCKET) err_quit("socket()");

   

// bind()

SOCKADDR_IN serveraddr;

ZeroMemory(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR)

{

err_quit("bind()");

}

   

// 데이터 통신에 사용할 변수

SOCKADDR_IN clientaddr;

int addrlen;

char buf[BUFSIZE+1];

   

// 클라이언트와 데이터 통신

while(1)

{

// 데이터 받기

addrlen = sizeof(clientaddr);

retval = recvfrom(sock, buf, BUFSIZE, 0,

(SOCKADDR*)&clientaddr, &addrlen);

if( retval == SOCKET_ERROR )

{

err_display("recvfrom()");

continue;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("UDP/%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port), buf);

   

// 데이터 보내기

retval = sendto(sock, buf, retval, 0,

(SOCKADDR*)&clientaddr, sizeof(clientaddr));

   

if( retval == SOCKET_ERROR )

{

err_display("sendto()");

continue;

}

}

   

// closesocket();

closesocket(sock);

// 윈속 종료

WSACleanup();

   

return 0;

}

  • 예제 UDPClient

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char *msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

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

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

if(sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// 소켓 주소 구조체 초기화

SOCKADDR_IN serveraddr;

ZeroMemory(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

   

// 데이터 통신에 사용할 변수

SOCKADDR_IN peeraddr;

int addrlen;

char buf[BUFSIZE+1];

int len;

   

// 서버와 데이터 통신

while(1)

{

// 데이터 입력

printf("\n[보낼 데이터] ");

if( fgets(buf, BUFSIZE+1, stdin) == NULL )

{

break;

}

   

// '\n' 문자 제거

len = strlen(buf);

if( buf[len-1] == '\n' )

{

buf[len-1] = '\0';

}

if( strlen(buf) == 0 )

{

break;

}

   

// 데이터 보내기

retval = sendto(sock, buf, strlen(buf), 0,

(SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR )

{

err_display("sendto()");

continue;

}

printf("[UDP클라이언트] %d바이트 보냄\n", retval);

   

// 데이터 받기

addrlen = sizeof(peeraddr);

retval = recvfrom(sock, buf, BUFSIZE, 0,

(SOCKADDR*)&peeraddr, &addrlen);

   

if( retval == SOCKET_ERROR )

{

err_display("recvfrom()");

continue;

}

   

// 송신자 IP주소 체크

if(memcmp(&peeraddr, &serveraddr, sizeof(peeraddr)))

{

printf("[오류] 잘못된 데이터입니다.\n");

continue;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("[UDP 클라이언트]%d바이트 받음\n", retval);

printf("[받은 데이터] %s\n", buf);

}

   

// closesocket()

closesocket(sock);

   

WSACleanup();

return 0;

}

  • UDP 서버/클라이언트 분석

    아래 그림은 UDP 서버/클라이언트가 소켓을 이용하여 통신할 때 운영체제가 제공하는 데이터 구조체를 보여주고 있다. TCP소켓의 경우와 비교해보면 데이터 송신 버퍼가 없다는 사실을 알 수 있다. UDP/IP를 이용하여 애플리케이션이 통신을 수행하기 위해서는 다음과 같은 요소가 결정되어야 한다.

    • 프로토콜

      소켓을 생성할 때 결정한다

    • 지역(local) IP 주소와 지역 포트 번호

      서버 또는 클라이언트 자신의 주소를 의미

    • 원격(remove) IP 주소와 원격 포트 번호

      서버 또는 클라이언트가 통신하는 상대의 주소를 의미

      UDP 서버/클라이언트에서 사용하는 소켓 함수는 지역 주소와 원격 주소를 결정하기 위한 일련의 절차라 할 수 있다.

      데이터 전송 함수

      데이터 전송 함수는 크게 데이터를 보낸느 함수와 받는 함수로 구분할 수 있다. 가장 기본이 되는 함수는 sendto( ) 와 recvfrom( )이며, 그 밖에 WSA*( )형태의 확장 함수가 존재한다.

      • sendto( )함수

        sendto( )함수는 하부 프로토콜(예를 들면, UDP/IP)을 이용하여 애플리케이션 데이터는 보내는 역할을 한다. sendto( )함수를 호출할 때, 소켓의 지역 IP 주소와 지역 포트 번호가 아직 결정되지 않은 상태라면 시스템이 자동으로 결정한다. 즉 sendto( )함수가 bind( )함수 역할을 암시적으로(Implicitly)하는 것이다.

        int sendto(

        SOCKET s,

        const char* buf,

        int len,

        int flags,

        const struct sockadd* to,

        int tolen

        ); // 성공: 보낸 바이트 수, 실패 : SOCKET_ERROR

        • s

          통신을 위한 소켓이다.

        • buf

          보낼 데이터를 담고 있는 버퍼의 주소다

        • len

          보낼 데이터 크기(바이트 단위)다.

        • flags

          sendto( )함수의 동작을 바꾸는 옵션이다. 대부분 0을 사용하며, 드물게 MSG_DONTROUTE와 MSG_OOB를 사용하는 경우가 있다.

        • to

          목적지 주소를 담고 있는 소켓 구조체 변수의 주소값을 대입한다. UDP의 경우 목적지 IP주소로 브로드캐스트나 멀티캐스트 주소를 사용할 수 있다.

        • tolen

          소켓 주소 구조체의 크기(바이트 단위)다.

        sendto( )함수와 관련하여 주의해야 할 사항은 다음과 같다

        • TCP소켓과 달리 UDP 소켓은 송신 버퍼가 별도로 존재하지 않는다. 애플리케이션 데이터는 커널(운영체제) 영역에 복사되어 전송된 후 곧바로 버려진다. sendto( )함수가 리턴했다고 실제 데이터 전송이 완료된 것은 아니며, 데이터 전송이 끝났더라도 상대방이 받았는지 확인할 수는 없다.
        • sendto( )함수로 보낸 데이터는 독립적인 UDP데이터그램(패킷)으로 만들어져 전송되며, 수신측에서는 한 번의 recvfrom( )함수 호출로 이 데이터를 읽을 수 있다. 즉 TCP와 달리 UDP를 사용할 경우 애플리케이션 수준에서 메시지 경계를 구분하기 위한 작업을 할 필요가 없다.
        • UDP소켓에 대해 sendto( )함수를 호출할 경우 한 번에 보낼 수 있는 데이터의 크기에 제한이 있다. 최대 값은 65507(65535-20(IP 헤더 크기)-(UDP 헤더 크기))바이트다. 실제적으로 이보다 훨씬 작은 크기를 사용하는 것이 바람직하다.
        • 블로킹 소켓의 경우, 커널 영역에 복사할 공간이 부족하면 sendto( )함수 호출 시 블록된다.

        sendto( )함수 사용 예는 다음과 같다

// 소켓 변수를 선언한다

SOCKET sock;

   

// 소켓 주소 구조체를 수신자 주소로 초기화한다

SOCKADDR_IN serveraddr;

...

   

// 보낼 데이터를 저장할 버퍼를 선언한다.

char buf[BUFSIZE];

   

// 보낼 데이터를 버퍼에 저장한다.

...

   

// 데이터를 보낸다

retval = sendto(sock, buf, strlen(buf), 0,

(SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR ) 오류 처리;

PRINTF("%d바이트 보냄\n", retval);

  • recvfrom( )함수

    recvfrom( )함수는 수신 버퍼에 도착한 데이터를 애플리케이션 버퍼로 복사하는 역할을 한다. recv( )함수와 다른 점은, 한 번에 하나의 UDP 데이터만 읽을 수 있다는 점이다. 즉 버퍼를 크게 잡는다고 많은 데이터를 한꺼번에 읽을 수 있는 것은 아니다.

    int recvfrom(

    SOCKET s,

    char* buf,

    int len,

    int flags,

    struct sockaddr* from,

    int* fromlen

    ); // 성공:받은 바이트 수, 실패:SOCKET_ERROR

    • s

      통신을 위한 소켓이다. 이 소켓은 반드시 지역 주소(IP주소, 포트 번호)가 결정되어 있어야 한다.

    • buf

      받은 데이터를 저장할 버퍼의 주소이다.

    • len

      버퍼의 크기(바이트 단위)다. 읽어야 할 데이터가 이 값보다 크면 버퍼 크기만큼만 복사하고 나머지는 버린다.

    • flags

      recvfrom( )함수의 동작을 바꾸는 옵션이다. 대부분 0을 사용하며, 드물게 MSG_PEEK와 MSG_OOB를 사용하는 경우가 있다. recvfrom( )함수는 수신 버퍼의 데이터를 애플리케이션 버퍼로 복사한 후 해당 데이터를 수신 버퍼에서 삭제한다. 그러나 MSG_PEEK옵션을 사용하면 수신 버퍼에 데이터가 계속 남아 있게 된다.

    • from

      소켓 주소 구조체 변수의 주소값을 대입하면, 이 변수에 송신자 주소(IP 주소, 포트 번호)가 저장된다.

    • fromlen

      정수형 변수를 from이 가리키는 메모리 영역의 크기로 초기화한 후, 이 변수의 주소값을 여기에 대입한다. recvfrom( )함수가 리턴하면, fromlen이 가리키는 정수형 변수는 recvfrom( )함수가 초기화한 메모리 크기값(바이트 단위)을 가진다.

    recvfrom( )함수와 관련하여 주의해야 할 사항을 정리하면 다음과 같다

    • sendto( )함수로 보낸 데이터는 독립적인 UDP데이터그램(패킷)으로 만들어져 전송되며, 수신측에서는 한 번의 recvfrom( )함수 호출로 이 데이터를 읽을 수 있다. 즉 TCP와 달리 UDP를 사용할 경우 애플리케이션 수준에서 메시지 경계를 구분하기 위한 작업을 할 필요가 없다.
    • UDP소켓에 대해 recvfrom( )함수를 호출할 경우 리턴값이 0이 될 수 있으며, 이 경우 연결 종료를 의미하는 것은 아니다. 반면 TCP소켓에 대해 recvfrom( )함수를 호출할 경우, 리턴값 0은 정상 종료를 의미한다.
    • 블로킹 소켓의 경우, 도달한 데이터가 없으면 recvfrom( )함수 호출 시 블록된다.

    recvfrom( ) 함수 사용 예는 다음과 같다

// 소켓 변수를 선언한다

SOCKET sock;

   

// 송신자 주소 저장을 위한 변수를 선언한다

SOCKADDR_IN peeraddr;

int addrlen;

   

// 받은 데이터를 저장할 버퍼를 선언한다

char buf[BUFSIZE];

   

// 데이터를 받는다.

addrlen = sizeof(peeraddr);

retval = recvfrom(sock, buf, BUFSIZE, 0,

(SOCKADDR*)&peeraddr, &addrlen);

if( retval == SOCKET_ERROR ) 오류 처리;

printf("%d바이트를 받음\n", retval);

코드 분석

일반적으로 UDP서버는 다음과 같은 순서로 소켓을 호출한다

  • socket( )함수를 이용하여 소켓을 생성한다
  • bind( )함수를 이용하여 지역 IP주소와 지역 포트 번호를 결정한다
  • sendto( ), recvfrom( )등의 데이터 전송 함수를 이용하여 클라이언트오 통신을 수행한다
  • 일반적으로 3과정을 계속 반복하며, 서버를 종료할 경우 closesocket( )함수를 이용하여 소켓을 닫는다.

UDP 클라이언트는 다음과 같은 순서로 소켓 함수를 호출한다

  • socket( )함수를 이용하여 소켓을 생성한다
  • sendto( ), recvfrom( )등의 데이터 전송 함수를 이용하여 서버와 통신을 수행한다.
  • closesocket( )함수를 이용하여 소켓을 닫는다

위의 모델을 사용할 때 주의할 점은 다음과 같다

  • 블로킹 소켓의 경우, sendto( ), recvfrom( )함수 호출 순서가 맞지 않으면 교착 상태(deadlock)가 발생할 수 있다.
  • 클라이언트는 데이터를 받은 후 송신자의 주소(IP 주소, 포트 번호)를 확인해야 한다. recvfrom( )함수는 UDP서버가 보낸 데이터는 물론, 다른 UDP 애플리케이션이 보낸 데이터도 모두 수신하기 때문이다.

UDP 소켓에 대해 connect( )함수를 호출할 경우, 내부적으로 통신할 상대의 주소 정보를 기억해두므로 send( ), recv( )함수를 이용하여 특정 UDP 서버와 통신할 수 있다.

위의 모델의 장점은 다음과 같다

  • sendto( )함수를 사용한 경우보다 속도가 빠르다. connect( )함수로 서버 주소를 한 번만 설정해두면 send( )함수가 이 정보를 계속 사용할 수 있기 때문이다.
  • 데이터를 받은 후 송신자의 주소(IP 주소, 포트 번호)를 확인하지 않아도 된다. recvfrom( )함수와 달리 recv( )함수는 connect( )함수로 설정한 서버를 제외한 다른 UDP애플리케이션이 보낸 데이터는 수신하지 않기 때문이다.

그러나 이 모델 역시 다음 사항을 주의해야 한다

  • 블로킹 소켓의 경우, sendto( ), recvfrom( ), send( ), recv( )함수 호출 순서가 맞지 않ㅇ느면 교착 상태가 발생할 수 있다.
  • 서버 코드

    46~47행 : socket( )함수 두 번째 인자에 SOCK_DGRAM을 사용하여 UDP소켓을 생성

    50~56행 : 소켓의 지역 주소를 설정한다

    59~61행 : 데이터 통신에 사용할 변수를 선언한다. 받은 데이터 끝에 널문자('\0')를 추가하기 위해 버퍼 크기를 +1하였다

    62~75행 : 클라이언트가 보낸 데이터를 받는다. 받은 데이터가 없을 경우 recvfrom( )함수에서 블록된다

    75~77행 : 받은 데이터 끝에 널 문자('\0')를 추가한 후 화면에 출력한다. 이때 클라이언트의 IP주소와 포트 번호도 같이 출력한다.

    80~85행 : 받은 데이터를 변경없이 다시 클라이언트에 보낸다.

  • 클라이언트 코드

    46~47행 : UDP소켓을 생성한다.

    50~54행 : 소켓 주소 구조체를 서버 주소로 초기화한다.

    57~60행 : 데이터 통신에 사용할 변수를 선언한다. 받은 데이터 끝에 널 문자('\0')를 추가하기 위해 버퍼 크기를 +1한다.

    65~67행 : fgets( )함수를 이용하여 사용자로부터 문자열을 입력 받는다.

    70~74행 : '\n'문자를 제거한다. 데이터 출력 시 줄바꿈 여부를 서버가 결정하도록 하기 위함이다.

    77~83행 : 서버에 데이터를 보낸다.

    86~92행 : 서버가 보낸 데이터를 받는다.

    95~98행 : 송신자 주소를 비교한다. 서버가 보낸 데이터가 아니면 오류를 출력한다.

    101~103행 : 받은 데이터 끝에 널 문자('\0')를 추가한 후 화면에 출력한다.

  • 브로드캐스팅

    TCP와 구별되는 UDP의 독특한 특징은 브로드캐스팅과 멀티캐스팅을 지원한다는 점이다. 이를 이용하면 다자 간 통신을 쉽게 구현할 수 있다. 인터넷에서 통신에 참여하는 개체간 상호 작용을 특성에 따라 분류하면 다음과 같다

    • 유니캐스팅(unicasting)

      가장 흔한 형태로, 두 애플리케이션이 통신하는 모델

    • 브로드캐스팅(broadcasting)

      한 네트워크에 속한 모든 개체와 통신하는 모델

    • 멀티캐스팅(multicasting)

      하나 또는 여러 네트워크에 속한 선별된 개체와 통신하는 모델이다. 브로드캐스팅은 멀티캐스팅의 특수한 경우에 속한다.

      동작 원리

      브로드캐스팅은 송신자가 데이터 복사본을 여러 개 보내는 것이 아닌 오직 하나의 데이터만 보내는 것이므로 다수의 수신자에게 효율적으로 데이터를 보낼 수 있다. 아래 그림은 이더넷(Ethernet)에서 브로드캐스팅 메시지를 보내는 상황을 나타낸 것이다. 송신자가 보내는 (하나의) 메시지는 이더넷을 통해 모든 수신자에게 전달되고, 수신자는 이 메시지가 브로드캐스트 주소로 보낸 것임을 알고 읽어 들인다.

      소켓 애플리케이션에서 브로드캐스트 데이터를 보내는 절차는 다음과 같다

      • 소켓에 대해 브로드캐스팅을 활성화한다.

BOOL bEnable = TRUE;

retval = setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char*)&bEnable, sizeof(bEnable));

if(retval == SOCKET_ERROR) err_quit("setsockopt( )");

socket( )함수로 생성한 소켓 디스크립터는 기본적으로 유니캐스팅만 가능하다. 소켓 옵션 중 SO_BROADCAST를 사용하면 브로드캐스팅을 활성화할 수 있다. setsockopt( )함수의 첫번째 인자로 소켓, 두 번째와 세 번째 인자로 SOL_SOCKET과 SO_BROADCAST을 대입한 상태에서 네 번째 인자에 TRUE값을 사용하면 해당 소켓에 대한 브로드캐스팅이 활성화되며, 이때부터 브로드캐스트 데이터를 보낼 수 있다.

  • 브로드캐스트 주소로 데이터를 보낸다.

// 소켓 주소 구조체를 초기화한다.

SOCKADDR_IN remoteaddr;

ZeroMemory(&remoteaddr, sizeof(remoteaddr));

remoteaddr.sin_family = AF_INET;

remoteaddr.sin_port = htons(9000);

remoteaddr.sin_addr.s_addr = htonl(INADDR_BROADCAST);

   

// 보낼 데이터를 버퍼에 저장한다.

char buf[BUFSIZE];

...

   

// 데이터를 보낸다.

retval = sendto(sock, buf, strlen(buf, 0,

(SOCKADDR*)&remoteaddr, sizeof(remoteaddr));

if(retval==SOCKET_ERROR) 오류 처리;

printf("%d바이트 보냄\n", retval);

브로드캐스트 데이터를 보내는 코드는 IP주소 부분을 제외하면 유니캐스트 데이터를 보내는 것과 동일하다. 따라서 브로드캐스트를 위한 IP주소 설정 방법만 알아두면 된다.

IP주소는 크게 네트워크 ID와 호스트 ID로 나눌 수 있다. 서브넷(subnet)을 사용하는 경우라면 호스트 ID의 일부는 서브넷 ID로 사용된다. 브로드캐스팅을 위해 예약되어 있는 IP 주소는 다음과 같다.

  • 네트워크-방향 브로드캐스트(net-directed broadcast)

    호스트 ID 비트가 모두 1인 경우로, 특정 네트워크에 대한 브로드캐스트를 의미한다. 브로드캐스트 데이터가 라우터를 거쳐야 하므로 라우터 설정에 따라 브로드캐스팅이 불가능할 수도 있다. 따라서 실용적으로 브로드캐스팅을 위한 목적으로 사용하기는 어렵다.

  • 서브넷-방향 브로드캐스트(subnet-directed broadcast)

    서브넷 ID를 제외한 호스트 ID 비트가 모두 1인 경우로, 특정 서브넷에 대한 브로드캐스트를 의미한다. 서브넷 브로드캐스트는 일반적으로 라우터를 통과하지 못하므로 외부 서브넷에 대한 브로드캐스팅을 위한 목적으로 사용하기는 어렵다. 주로 자신이 속한 서브넷에 대한 브로드캐스팅 용도로 사용하며, 이 경우 지역 브로드캐스트와 동일하다고 볼 수 있다.

  • 지역 브로드캐스트(local broadcast 또는 limited broadcast)

    자신이 속한 네트워크에 대한 브로드캐스트를 의미한다. 항상 브로드캐스팅이 가능하며, 브로드캐스트 데이터가 라우터 경계를 넘어가지 않는다. 지역 브로드캐스트 주소는 winsock2.h파일에 INADDR_BROADCAST값으로 정의되어 있다.

[실습]브로드캐스팅 예제

두 애플리케이션의 동작은 다음과 같다

  • 송신자 : 사용자가 키보드로 입력한 문자열을(fgets) 브로드캐스트 주소로 보낸다(sendto)
  • 수신자 : 브로드캐스트 데이터를 받아(recvfrom), 이를 문자열로 간주하여 무조건 화면에 출력한다.(printf)

  • 예제 BroadcastSender

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

//소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

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

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

if( sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// 브로드캐스팅 활성화

BOOL bEnable = TRUE;

retval = setsockopt(sock, SOL_SOCKET, SO_BROADCAST,

(char*)&bEnable, sizeof(bEnable));

if( retval == SOCKET_ERROR )

{

err_quit("setsockopt()");

}

   

// 소켓 주소 구조체 초기화

SOCKADDR_IN remoteaddr;

ZeroMemory(&remoteaddr, sizeof(remoteaddr));

remoteaddr.sin_family = AF_INET;

remoteaddr.sin_port = htons(9000);

remoteaddr.sin_addr.s_addr = htonl(INADDR_BROADCAST);

   

// 데이터 통신에 사용할 변수

char buf[BUFSIZE+1];

int len;

   

// 브로드캐스트 데이터 보내기

while(1)

{

// 데이터 입력

printf("\n[보낼데이터]");

if(fgets(buf, BUFSIZE+1, stdin) == NULL)

{

break;

}

// '\n' 문자 제거

len = strlen(buf);

if( buf[len-1] == '\n' )

{

buf[len-1] = '\0';

}

if( strlen(buf) == 0 )

{

break;

}

   

// 데이터 보내기

retval = sendto(sock, buf, strlen(buf), 0,

(SOCKADDR*)&remoteaddr, sizeof(remoteaddr));

if(retval == SOCKET_ERROR)

{

err_display("sendto()");

continue;

}

printf("%d바이트를 보냄\n", retval);

}

   

// closesocket()

closesocket(sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

  • 예제 BroadcastReceiver

#include <winsock2.h>

#include <stdio.h>

#include <stdlib.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

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

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

if( sock == INVALID_SOCKET )

{

err_quit("socket()");

}

   

// bind()

SOCKADDR_IN localaddr;

ZeroMemory(&localaddr, sizeof(localaddr));

localaddr.sin_family = AF_INET;

localaddr.sin_port = htons(9000);

localaddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(sock, (SOCKADDR*)&localaddr,

sizeof(localaddr));

if( retval == SOCKET_ERROR )

{

err_quit("bind()");

}

   

// 데이터 통신에 사용할 변수

SOCKADDR_IN peeraddr;

int addrlen;

char buf[BUFSIZE+1];

   

// 브로드 캐스트 데이터 받기

while(1)

{

// 데이터 받기

addrlen = sizeof(peeraddr);

retval = recvfrom(sock, buf, BUFSIZE, 0,

(SOCKADDR*)&peeraddr, &addrlen);

if( retval == SOCKET_ERROR )

{

err_display("recvfrom()");

continue;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("[UDP/%s:%d] %s\n", inet_ntoa(peeraddr.sin_addr),

ntohs(peeraddr.sin_port), buf);

}

   

//closesocket()

closesocket(sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

반응형

'책정리 > 윈도우 네트워크 프로그래밍' 카테고리의 다른 글

9장 소켓 입출력 모델(I)  (0) 2015.06.23
7장 소켓 옵션  (0) 2015.04.23
5장 멀티스레드  (0) 2015.04.20
4장 TCP 서버/클라이언트 구조  (0) 2015.04.13
3장 소켓 주소 구조체 다루기  (0) 2015.04.11