소리바다에서 한게임까지 TCP/IP 소켓 프로그래밍 에서...


  • TCP에 대한 이야기

    TCP 프로토콜은 가상으로 연결된 상태입니다.  그렇기 때문에 서버와 클라이언트 프로그램 모두에 신경을 써야 합니다.

    그리고, 1회 전송한 패킷이 한 번에 전송되리라고 기대하서는 안됩니다.  또한, 여러번에 걸쳐 전송한 패킷이 역시 한 번에 전송될 수 있습니다.

    TCP가 동작하는 방식은, 우리에게 보여지지 않습니다.  중요한 것은, 연결이 된 이후부터 연결을 닫을 때까지 전송한 데이터가 모두 하나의 데이터라는 점입니다.

    가령, 엄청나게 큰 버퍼(100MB 정도)를 할당하고, 앞에서부터 차례대로 채워나간다고 생각하면 됩니다.  변수를 하나 생성할 때마다 앞에서부터 차곡차곡 채워 넣습니다.  변수를 읽어 올 때는 어디서부터 어디까지가 하나의 변수인지 확인이 필요합니다.  모든 변수가 동일한 크기를 갖는 것이 아니기 때문입니다.  따라서 TCP에서는 어디서부터 어디까지가 의미있는 하나의 패킷인지 확인하는 작업이 필수입니다.

     

    중요한 것은 전송하는 측이나 수신 측 모두 두 개의 버퍼를 사용하는 것이 핵심입니다.  소켓 라이브러리는 전송 또는 수신을 위해 자체적으로 별도의 버퍼를 사용합니다.  그렇기 때문에 send함수를 호출했다고 해서 패킷이 실제로 상대 소켓으로 전달되었다고 가정해서는 안 됩니다.  send함수는 소켓 라이브러리의 버퍼로 데이터를 옮겨놓는 순간 반환합니다.

     

    이 것을 기억합시다.  TCP 프로토콜에서 연결이 구축되어 있는 동안 전송되는 패킷은 모두 연속되어 있다.  패킷을 처리할 때는, 각각의 패킷 길이만큼 잘라서 처리해야 합니다.

  • 패킷의 구성

    패킷은 크게 두 부분으로 나뉩니다.  이 두 부분은 소켓 라이브러리가 자동으로 구성해 주는 것이 아니고, 프로그래머가 프로그램의 성격에 맞게 구성해야 합니다.  패킷의 앞 부분에는 헤더가 오고, 뒷 부분에는 데이터가 옵니다.  헤더에 들어가는 내용은 다 다릅니다.  그러나, 반드시 포함되어야 하는 요소가 패킷 전체의 길이입니다.  없다면, 패킷을 해당 패킷에 맞게 잘라낼 수 없습니다.

     

    패킷 헤더는 일반적으로 어떠한 패킷이던지 동일한 길이로 구성합니다.  그래야 패킷을 분석하기가 쉽습니다.   대체적으로 패킷 헤더에는 패킷 전체 길이와 식별자를 넣는 것이 좋습니다.  다음은 패킷의 예제입니다.

       

    패킷전체길이

    길이 : 4 (int)

    식별자 (ID)

    길이 : 4 (int)

    헤더

 

    패킷 전체 길이

    길이 : 24

    식별자

    FILE_LOGIN

    회원 식별자

    길이 : 8 (char[8])

    암호

    길이 : 8 (char[8])

    로그인

     

    패킷 전체 길이

    길이 : 80

    식별자

    FILE_REQUEST

    회원 식별자

    길이 : 8 (char[8])

    암호

    길이 : 64 (char[64])

    파일 요청

     

    패킷 전체 길이

    길이 : 파일길이 + 8

    식별자

    FILE_TRANSFER

    파일 내용

    길이 : 가변

    파일 전송

     

    위의 패킷을 사용하는 시나리오를 말하자면 다음과 같습니다.

    클라이언트는 먼저 서버에 접속하고, 이 때 로그인 패킷을 사용함.  소리바다 등에서 파일을 검색한 후, 다운로드할 파일을 다른 회원에게 요청함.  이 때 파일 요청 패킷을 사용.  파일 요청을 받은 패킷은 파일을 요청한 회원에게 전송하고, 이 때 파일 전송 패킷을 사용함.  각각의 패킷은 모두 패킷 헤더를 갖고 있고, 패킷 헤더의 앞에는 전체 패킷의 길이가 들어 있음.  패킷을 수신했을 때의 처리는, 헤더의 두 번째 부분인 식별자에 따라 분기가 일어남.  로그인 패킷이라면 이미 로그인한 회원인지를, 파일 요청이라면 파일이 존재하는지 검사할 수 있습니다.  패킷 식별자에 들어 있는 영어 단어는 식별자를 define 문으로 정의해 놓은 상수들입니다.

     

    이 부분에서 제일 중요한 것은 패킷의 활용 부분입니다.  그리고 반드시 패킷의 맨 앞에 패킷 길이를 알려주도록 합시다.

     

  • 패킷에 관하여

    패킷이 다음과 같이 구성되어 있다고 가정합시다.

    파일명

    파일 데이터

    1번 패킷

    파일명

    파일 데이터

    2번 패킷

    파일명

    파일 데이터

    3번 패킷

    파일명

    파일 데이터

    4번 패킷

     

    모두 4개의 패킷이 있습니다.  이 패킷을 1번부터 순차적으로 전송을 합니다.  하지만 목적지에 4번이 먼저 도착할 수 있습니다.

    왜냐하면, 패킷은 라우터라는 기계를 거쳐서 목적지까지 가는데, 이 라운터라는 녀석이 판단하기에, 가장 빠를 것 같은 경로로 보내기 때문에, 3번 패킷보다 4번 패킷이 먼저 도착할 수 있습니다.  그렇기 때문에 저 패킷에 패킷의 번호를 나타내는 부분이 추가되어야 합니다.  또한, 프로그램에서 전송하는 패킷의 크기와 시스템이 전송하는 패킷의 크기가 다를 수 있습니다.  그러면, 결국 우리가 전송한 패킷은 여러 개의 패킷으로 분할될 수도 있고, 여러 번에 걸쳐 전송한 패킷이 하나의 패킷으로 결합될 수도 있습니다.  그러므로, 여러 개의 패킷이 합쳐져서 한 번에 전송될 경우, 우리는 각각의 패킷을 구분할 수 있어야 합니다.  그렇기 때문에 한가지 더 추가되어야 합니다.  위에서 얘기했던 패킷의 길이 부분이 필요합니다.

    위의 내용을 적용해서 패킷을 구성하면 다음과 같습니다.

    길이

    파일명

    번호[1]

    파일 데이터

    1번 패킷

    길이

    파일명

    번호[2]

    파일 데이터

    2번 패킷

    길이

    파일명

    번호[3]

    파일 데이터

    3번 패킷

    길이

    파일명

    번호[4]

    파일 데이터

    4번 패킷

     

    위의 경우는 특수한 경우이고, 대부분의 경우는 [길이] 필드와 [데이터] 필드만 있으면 패킷의 구성이 가능합니다.  그리고 이에 추가적으로 거의 필수에 가까운 것이, 데이터를 구분할 수 있는 식별자 입니다.

  • 패킷 전송

    패킷을 전문적으로 전송하는 함수가 윈도우 소켓 라이브러리 버젼 2에 새롭게 추가되었습니다.  이름은 TransmitPackets입니다.

    #include "stdafx.h"
    #include <WINSOCK2.H>
    #include <mswsock.h>
    #include <cassert>

    int _tmain(int argc, _TCHAR* argv[])
    {
        WSADATA wsaData;
        WSAStartup(MAKEWORD(2, 2), &wsaData);

        SOCKET ClientSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

        sockaddr_in ServerAddr;
        ZeroMemory(&ServerAddr,
    sizeof(ServerAddr));

        ServerAddr.sin_family       = AF_INET;
        ServerAddr.sin_addr.s_addr  = inet_addr(
    "211.254.138.79");
        ServerAddr.sin_port         = htons(50000);

        connect(ClientSocket, (sockaddr*) &ServerAddr,
    sizeof(ServerAddr));

        printf(
    "서버에 연결되었습니다..\n");

        
    //-------------------------------------------------
        // TransmitPackets 함수에 전달할 인자를 구축합니다.
        //-------------------------------------------------

        
    //함수 인자
        char pData[][30] = {"안녕하세요!", "김정훈입니다."};                // 원격지 소켓으로 전송될 실제 데이터 배열.

        TRANSMIT_PACKETS_ELEMENT pPacketElems[2];                           
    // 패킷 유형과 패킷 데이터에 대한 정보를 저장하고 있는 구조체 배열.
        ZeroMemory(&pPacketElems, sizeof(pPacketElems));

        
    int nPacketCount = sizeof(pPacketElems) / sizeof(pPacketElems[0]);

        
    // 배열 각각의 요소를 전송 데이터로 설정합니다.
        for(int i = 0; i < nPacketCount; i++)
        {
            pPacketElems[i].dwElFlags   = TP_ELEMENT_MEMORY;                
    // TP_ELEMENT_MEMORY -> 메모리에 있는 버퍼를 전송한다고 알려줌.
            pPacketElems[i].cLength     = lstrlen(pData[i]) + 1;            // 버퍼의 길이
            pPacketElems[i].pBuffer     = pData[i];                         // 버퍼 포인터.
        }

        
    //-------------------------------------------------
        // TransmitPackets 함수의 포인터를 구한 다음,
        // TransmitPackets 함수를 실제로 호출하는 부분.
        //-------------------------------------------------

        
    // 함수 포인터
        DWORD                   dwBytesReturned;

        
    // TransmitPackets 함수를 가리키는 WSAID_TRANSMITPACKETS GUID를 변수에 저장.
        // 포인터가 넘어가야 하기 때문에, GUID를 직접 사용하지는 못합니다.
        // GUID는 오직 하나 밖에 존재하지 않는 유일한 값을 말합니다.
        // 이 GUID 값은 mswsock.h 파일에서 찾으실 수 있습니다.
        GUID                    guid = WSAID_TRANSMITPACKETS;

        
    // 우리가 사용할 TransmitPackets 함수의 자료형입니다.
        // 사실 TransmitPackets 함수는 실제로 존재하는 함수가 아니고, 제가 만든 함수 이름입니다.
        // 헤더 파일에 함수 선언이 존재하는 것이 아니라, 프로금애 실행되는 동안에 직접 함수 포인터를 구해서 사용하기 때문입니다.
        // 그래서, 이름 대신 함수의 자료형인 LPFN_TRANSMITPACKETS 자료형이 존재합니다.
        LPFN_TRANSMITPACKETS    TransmitPackets;

        
    /*
        위의 LPFN_TRANSMITPACKETS의 원형은 다음과 같습니다.
        typedef BOOL (PASCAL FAR * LPFN_TRANSMITPACKETS) (
        SOCKET                      hSocket,
        LPTRANSMIT_PACKETS_ELEMENT  lpPacketArray,
        DWORD                       nElementCount,
        DWORD                       nSendSize,
        LPOVERLAPPED                lpOverlapped,
        DWORD                       dwFlags
        );
        */


        
    // ConnectEx, TransmitPackets 함수 등의 확장 함수는 WSAIoctl 함수를 호출해서 해당 함수의 포인터를 구해야 합니다.
        int nRet = WSAIoctl(
            ClientSocket,                           
    // 소켓 핸들
            SIO_GET_EXTENSION_FUNCTION_POINTER,     // 제어 코드로 확장 함수를 가리키는 제어 코드.
            &guid,                                  // 제어 코드와 관련된 값으로 확장 함수 일 경우, 해당 함수를 가리키는 GUID 값.
            sizeof(GUID),                           // 세번째 인자의 크기
            &TransmitPackets,                       // 함수 포인터를 반환 받을 버퍼.
            sizeof(LPFN_TRANSMITPACKETS),           // 다섯 번째 인자의 크기
            &dwBytesReturned,                       // 버퍼에 반환된 데이터 크기
            NULL,                                   // 중첩 구조체
            NULL);                                  // 완료 함수 포인터.

        assert( nRet != SOCKET_ERROR);

        TransmitPackets(
            ClientSocket,                           
    // 소켓 핸들
            pPacketElems,                           // 패킷 배열의 시작 주소
            nPacketCount,                           // 배열의 크기
            0, NULL, 0);

        closesocket(ClientSocket);
        WSACleanup();
        
    return 0;
    }


2003년 8월 26일 화요일

안정적인 DNS서비스 DNSEver DNS server, DNS service
Posted by 키르히아이스
,