khstar

Unix Socket 프로그래밍 본문

Unix&Linux/Socket

Unix Socket 프로그래밍

khstar 2008. 1. 3. 20:36
반응형

2절. UDP 프로그래밍

2.1절. UDP란

TCP/IP 4계층에서 봤을때 UDP 는 TCP 와 같은 Transport Layer 에 위치한다. 즉 UDP와 TCP는 동급의 프로토콜로 데이타를 전송하기 위해서 사용되는 프로토콜이다.

TCP가 연결지향적이고 신뢰할수 있는 데이타의 흐름을 제공하는 반면 UDP는 비연결지향성(connectionless)이며, 데이타의 흐름을 신뢰할수 없다는 특징이 있다.


2.1.1절. connectionless

TCP는 서로 통신을 하기전에 상대방을 확인하는 절차를 가짐으로써, session(통신선로)를 맺는 작업을 하며, 연결된 session 을 통해서 데이타의 흐름이 이루어진다. 그러나 UDP 는 이러한 session 을 만들기 위한 작업을 하지 않고, 그냥 보내고 받기만을 한다. 그러므로 우리가 UDP 서비스를 하는 서버로 메시지를 보냈다고 하더라도, 메시지가 실제로 도착되었는지는 알수가 없다. 데이타는 보내질수도 있고 그렇지 않을수도 있다.


2.1.2절. unreliable

또한 TCP와 달리 신뢰할수가 없다. TCP는 프로토콜자체에 메시지가 제대로 보내졌음을 체크할수 있는 다양한 장치를 가지고 있다. 즉 각 패킷에 순서를 매겨서, 순서가 뒤엉키지 않도록 재조립하며, 일정시간 동안 패킷이 도착하지 않으면, 해당 패킷을 다시 보내달라고 요청할수도 있다. 그러나 UDP는 이러한 어떠한 장치를 가지고 있지 않는다. UDP 로 전송된 패킷은 순서가 뒤바뀔수도 있으며, 중간에 패킷이 손실될수도 있다. 프로토콜 차원에서 패킷의 순서가 뒤바뀌었는지, 패킷이 손실되었는지 알수 있는 방법은 없다.

UDP 패킷에 신뢰성을 주기 위해서는 application 차원에서 직접 코딩을 해주어야만 한다. 보통은 패킷을 만들때 데이타 헤더를 따로 만들어서 일련번호등을 넣어서 서버측에 보내고 서버측에서는 이에 대한 응답을 보내는 방식을 이용하여 UDP 패킷에 신뢰성을 부여한다.

이렇듯 UDP 는 단순히 데이타 그램 위주의 통신을 하기 때문에, 데이타 그램 지향 프로토콜 이라고 불리우기도 한다. 실제로 UDP는 User Datagram Protocol 의 줄임말이다.


2.1.3절. 프로그래머 관점에서 봤을때의 특징

UDP는 TCP 프로토콜이 가지고 있는 다양한 기능을 가지고 있지 않다. 당연히 더 간단하고, 더빠른 처리를 보장해준다. 또한 프로그래밍 하기도 더욱 간단하다. 나중에 예제를 들겠지만 UDP 를 이용하는 서버의 경우 listen, accept 를 할필요 없이 그냥 소켓을 생성하고, 읽을 데이타가 있는지 기다리기만 하면된다(connectionless 이므로 당연히 클라이언트의 accept 를 기다릴 필요가 없다).


2.1.4절. UDP 를 어디에 사용할수 있을까?

언뜻 생각하기에 UDP는 TCP에 비해서 사용하기에 문제가 있을거라고 생각할수 있다. 그러나 UDP는 그 나름대로 적당한 사용처가 있다.

첫번째가 음성및 비디오를 위한 실시간 스트리밍 서비스이다. 음성서비스를 TCP로 해버릴경우의 문제점은 패킷이 중간에 빠질경우 음성비스가 중단되어버린다는 점이다(빠진 패킷에 대한 재 전송을 요청하므로). 하지만 이건 바람직한 현상이 아니다. 이건 마치 우리가 전화를 할때 중간에 약간의 잡음이 생겼다고 해서, 전화가 중단되는 것과 마찬가지의 상황이다. 우리는 약간의 잡음 때문에 (혹은 한두자 정도 언어가 전달이 안되는) 그걸 교정하느라고 서비스가 중단되는 것 보다는 서비스질이 약간 떨어지더라도 계속적으로 서비스가 되는걸 원할것이다. 즉 통신품질보다는 통신의 연속성이 더욱 중요시 되는곳에 유용하게 사용될수 있다. (물론 TCP로도 구현할수 있으며, 상당수의 서비스가 TCP로 서비스 된다. 다만 이러한 특징을 가지고 있음을 설명하는 것이다.)

두번째는 상당히 많은 패킷이 오가면서, 별로 중요하지 않은 몇개의 데이타 손실 정도는 눈감아줄수 있는 곳이다. 가장 유명한게 start craft 의 베틀넷 서비스가 아닐가 싶다. 이 베틀넷 서비스에는 수많은 유저가 접속해서 사용할건데, 서비스의 모든 부분에 TCP를 사용하기에는 TCP는 너무 느린 감이 있다. 특히 게임을 할때 서로 교환되는 수많은 패킷의 경우 매우 중요한 데이타가 아니므로, 그리고 게임의 흐름이 끊기면 안되므로 UDP로 처리되는게 더 유리할것이다.

이밖에도 UDP 를 통신 프로토콜로 사용하는 서비스로는 DNS 와 NFS, SNMP, syslog 등이 있다.


2.1.5절. UDP 를 이용하는 서버 작성

UDP 서버역시 socket 를 이용해서 통신을 하지만 TCP와는 달리 연결지향이 아니므로 listen 과정과 accetp 과정이 필요 없다. socket > bind 과정후 만들어진 소켓 지정 번호 에서 데이타가 있을경우 이를 읽기만 하면 된다. 또한 클라이언트와 연결을 맺지 않기 때문에, 각 클라이언트의 요청해결을 위해서 fork, select, poll, thread 등을 이용해서 프로세스를 분기할 필요가 없다. 기본적으로 UDP를 이용한 서버의 경우 최초 socket 함수를 이용해서 만들어진 소켓 지정 번호 만을 가지고 통신이 가능하다. TCP에 있어서 최초 만들어진 소켓 지정 번호가 클라이언트의 연결을 accept 하기 위한 end point 전용으로 쓰이는 것과는 다르다. TCP를 이용한 플로그래밍에 있어서는 각 클라이언트와의 연결을 위해서 최초 생성된 하나의 소켓 지정 번호를 end point 로 하고 연결이 만들어지면 전용 통신 선로를 위한 소켓 지정 번호를 생성하고 이 소켓 지정 번호를 이용해서 통신을 하게 된다. UDP 는 이러한 작업이 필요 없으므로 서버를 매우 간편하고, 직관적으로 이해하기 쉽게 만들수 있다(단지 while 문을 돌리기만 하면 된다).

여기에서 한가지 의문점이 생긴다. 연결을 맺지 않는다고 했는데, 그렇다면 어떻게 여러개의 클라이언트로 부터 요청을 받았을때, 요청한 클라이언트에게 결과 데이타를 보낼수 있을까(단지 하나의 소켓 지정 번호를 이용해서)? 가장 간단하게 생각해볼수 있는 방법은 데이타를 받을때, 데이타를 보낸 클라이언트의 정보를 받아오고, 이 클라이언트의 정보를 토대로 데이타를 보내면 될것이다. Unix 는 이러한 함수를 제공하고 있다.

int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen); 
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
recvfrom 과 sendto 를 이용해서 원하는 클라이언트로 데이타를 보낼수 있다. recvfrom 은 TCP와 UDP 모두에서 사용이 가능한데, UDP 에서 사용할경우 sockaddr 구조체가 채워져서 돌아온다. 그러므로 우리는 클라이언트의 연결 정보를 알수 있게 된다. INET 서버의 경우라면 struct sockaddr_in 을 사용하게 될것이다. 우리는 sockaddr_in 의 멤버 변수를 확인함으로써 port 와 address 등 통신을 위해서 꼭 필요한 정보를 얻을수 있다.
struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;             /* Port number.  */
    struct in_addr sin_addr;        /* Internet address.  */
    ....
};
sockaddr_in 은 /usr/include/netinet/in.h 에 선언되어 있다.

sendto 도 recvfrom 과 마찬가지로 TCP/UDP 모두에 사용가능하며, struct sockaddr 에 메시지를 보낼 호스트(클라이언트 혹은 서버 호스트)의 정보를 채워 넣음으로써 원하는 클라이언트 에게 메시지를 보낼수 있다.


2.1.6절. UDP 를 이용하는 클라이언트 작성

UDP 클라이언트는 그야말로 초 간단이다. socket 을 열고나서 sendto 함수이용해서 쓰기만 하면 그걸로 끝이다.


2.2절. 예제 프로그램

이제 간단한 예제를 만들어 보도록 하겠다. 덧셈 서버/클라이언트로, 클라이언트측에서 2개의 숫자를 보내면 서버측에서 이걸 받아서 더한다음 돌려주는 간단하지만 UDP 의 프로그래밍을 하기 위한 최소한의 내용을 담고 있다.


2.2.1절. 서버 에제

예제 : sum_server.c

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>

struct data
{
    int a;
    int b;
    int sum;
};

int main(int argc, char **argv)
{
    int sockfd;
    int clilen;
    int state;
    int n;
    int sum;
    struct data add_data;    

    struct sockaddr_in serveraddr, clientaddr;

    clilen = sizeof(clientaddr);
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket error : ");
        exit(0);
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(1234);

    state = bind(sockfd, (struct sockaddr *)&serveraddr, 
        sizeof(serveraddr));
    if (state == -1)
    {
        perror("bind error : ");
        exit(0);
    }

    while(1)
    {
        n =recvfrom(sockfd, (void *)&add_data, sizeof(add_data), 0, (struct sockaddr *)&clientaddr, &clilen);
        add_data.sum = add_data.a + add_data.b;
        sendto(sockfd, (void *)&add_data, sizeof(add_data), 0, (struct sockaddr *)&clientaddr, clilen);
    }
    close(sockfd);
}
예제 프로그램은 더할나위 없이 간단하다. 소켓을 생성해서 bind 하는것 까지는 TCP 프로그래밍과 매우 비슷하다. 다른것이 있다면 최초 socket 함수를 호출할때 2번째 인자로 SOCK_STREAM 대신 SOCK_DGRAM 을 쓴다는것이다. SOCK_STREAM 을 명시해 줌으로써 UDP 소켓을 사용할수 있다. 그리고 listen, accept 함수가 없이 바로 데이타 전송/수신 과 관련된 함수를 호출함을 알수 있다. 이는 클라이언트와 연결을 생성시키지 않기 때문이다.

그리고 redvfrom 함수를 호출하여서, 클라이언트로 부터 데이타를 읽어 들이고 읽어들인 데이타를 더하고 그 결과값을 sendto 를 이용해서 클라이언트측으로 보낸다. recvfrom과 sendto 의 5번째 아규먼트를 주목하기 바란다. 5번째 아규먼트로 클라이언트의 소켓구조체 주소를 가져옴으로 우리는 다중의 클라이언트에 대한 요청을 처리할수 있게 된다.


2.2.2절. 클라이언트 예제

예제 : sum_client.c

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>

struct data
{
    int a;
    int b;
    int sum;
};
int main(int argc, char **argv)
{
    int sockfd;
    int clilen;
    int state;
    char buf[255];

    struct sockaddr_in serveraddr;
    struct data add_data;


    memset(buf, 0x00, 255); 
    clilen = sizeof(serveraddr);
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket error : ");
        exit(0);
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serveraddr.sin_port = htons(1234);

    add_data.a = atoi(argv[1]);
    add_data.b = atoi(argv[2]);

    sendto(sockfd, (void *)&add_data, sizeof(add_data), 0, (struct sockaddr *)&serveraddr, clilen);
    recvfrom(sockfd, (void *)&add_data, sizeof(add_data), 0, NULL, NULL); 

    printf("--> %d + %d = %d
", add_data.a, add_data.b, add_data.sum);
    close(sockfd);
}
클라이언트는 더 간단하다. socket 만 만들고 나서 바로 통신에 들어간다. 클라이언트는 아규먼트로 2개의 숫자를 받아들인 다음 이것을 서버에 보내고, 서버의 결과값(더한값)을 가져오고, 이것을 출력시켜준다.


2.2.3절. 문제점

위의 UDP 를 이용한 서버/클라이언트 모델은 몇가지 문제점을 가지고 있다. 위의 예제를 가지고 테스트를 해보면 알겠지만, 서버 프로그램이 떠있지 않더라도 클라이언트는 이를 감지 하지 못하고, 메시지를 보낸다. 또한 메시지가 정확히 전달되었는지 그렇지 않은지 클라이언트는 감지 하지 못한다. 데이타를 보내는 걸로 끝이기 때문이다. 그리고 무작정 서버로부터의 응답을 기다리는데, 서버는 죽어 있음으로 당연히 클라이언트는 응답을 받지 못할것이고, 클라이언트는 계속 block 된 상태로 떠있게 될것이다

사실 이건 어쩔수 없는 문제이다. UDP 프로토콜 자체가 데이타의 흐름을 제어할수 있는 어떤 장치를 제공해주지 않기 때문이다. 이를 해결하기 위해서는 어플리케이션 차원에서 해결하는 수 밖에 없다. 즉 최초에 서버에 어떤 메시지를 보내고(HELO 메시지) 일정시간안에 서버로 부터 메시지가 도착하는지 확인하고나서, 통신을 시작하는 것이다. 통신할때도 역시 일정시간안에 응답 메시지가 서버로 부터 도착하는지를 확인해주어야 할것이다.

출처 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/article/UDP_%BC%D2%C4%CF_%C7%C1%B7%CE%B1%D7%B7%A1%B9%D6

반응형
Comments