7.4.2 응용 프로토콜 스택 구현 [TCP/IP 소켓 프로그래밍 with 윈도우즈]

응용 프로토콜 스택은 설계 단계에서 구현할게요. 구현 단계에서는 이들을 이용하여 Peer와 각 서비스를 구현하는 부분을 다룰 것입니다.

제일 먼저 EH 메신저의 메시지 종류에 관계없이 패킷을 만들고 이를 전송하거나 수신하는 클래스를 정의합시다. 여기에서는 이를 동적 라이브러리 EHPacketLib이름의 컴포넌트에 정의할게요.

먼저 EHPacketLib를 생성합니다. 이 때 솔루션 이름은 EH 메신저라 정의할게요.

[그림 7.12] EH 메신저 솔루션 EHPacketLib 프로젝트 만들기(개발 도구 버전에 따라 화면은 다를 수 있습니다.)

 EHPacketLib 프로젝트의 유형을 DLL을 선택하세요. 이 책에서는 Win32 API의 DLL로 만들 것입니다. 여러분께서 아직 Win32 API를 학습한 적이 없으면 필요한 부분을 위한 추가적인 학습을 요구합니다. 이 책에서는 Win32 API에 관한 사항은 자세히 설명하지 않습니다.(개발도구 버전에 따라 화면은 다를 수 있습니다.)

[그림 7.13] EHPacketLib를 DLL로 만들기

 동적 라이브러리를 제작하면서 약속 부분을 정의한 헤더 파일은 동적 라이브러리를 사용하는 곳에서도 필요합니다. 이에 공통으로 사용할 헤더 파일들만 추가할 빈 프로젝트를 생성할게요. 여기에서는 빈 프로젝트 이름을 Common으로 정할게요.

[그림 7.14] 빈 프로젝트 생성

 EHPacketLib에는 EH 메신저에서 주고 받을 패킷을 생성하고 전송 및 수신하는 역할을 제공할 것입니다.

먼저 Common 프로젝트에 EHPacket.h 파일을 추가하세요.

#pragma once

윈속 헤더 파일을 포함합니다.

#include <WinSock2.h>

EHPacketLib에서는 __declspec(dllexport)로 이를 사용하는 곳에서는 __declspec(dllimport)로 나타낼 수 있도록 매크로 EH_DLL을 정의합니다. __declspec은 동적 링크 라이브러리에서 함수나 형식을 사용하는 곳에 노출하기 위해 사용하는 구문입니다. 자세한 내용은 Win32 API 관련 자료를 참고하세요.

#ifdef EHPACK037DIECUEMFHCUEKUEJDI
#define EH_DLL __declspec(dllexport)
#else
#define EH_DLL __declspec(dllimport)
#endif

EH 메시지 패킷의 최대 크기를 정의합시다.

#define EH_PACKET_SIZE  2000 //EH 메시지 패킷의 최대 크기

클래스 EHPacket을 정의합니다.

class EH_DLL EHPacket
{

패킷 데이터를 저장할 버퍼를 선언합니다.

    char base[EH_PACKET_SIZE]; //패킷 버퍼

패킷을 수신한 후 목적에 맞게 데이터를 디캡슐할 위치를 기억하기 위한 멤버를 선언합니다.

    char *ptr;//패킷의 내용을 디캡슐할 위치
public:

메시지를 패킷으로 만들 때 사용할 생성자를 선언합니다.

    EHPacket(int msgid);

소켓에서 데이터 수신하여 패킷을 만드는 생성자를 선언합니다.

    EHPacket(SOCKET sock);

패킷의 메시지 아이디를 반환하는 메서드를 선언합니다.

    int GetMsgId()const;

데이터를 패킷의 데이터로 캡슐화하는 메서드를 선언합니다.

    bool Packetize(void *data,int dlen);

패킷의 데이터를 디캡슐화하는 메서드를 선언합니다.

    bool DePacketize(void *buf,int blen);

패킷을 소켓으로 전송하는 메서드를 선언합니다.

    int Serialize(SOCKET sock);
private:

EHPacket 클래스 내부에서 메시지 바디 위치를 구하는 메서드를 선언합니다.

    char *GetBody();
};

이제 EHPacketLib 프로젝트에 EHPacket.cpp 소스 파일을 추가하세요.

EHPacket.h 파일에서 EHPacket 라이브러리 내부와 라이브러리를 사용하는 곳에 EH_DLL 매크로의 의미를 다루게 정의하기 위해 사용한 매트로 상수를 정의한 후에 EHPacket.h 파일을 포함합니다. EHPacket.h 파일 위치는 이전 폴더에서 common 폴더 내부에 있습니다.

#define EHPACK037DIECUEMFHCUEKUEJDI
#include "..\\common\\EHPacket.h"

윈속을 사용하기 위해 필요한 파일을 추가합니다.

#pragma comment(lib,"ws2_32")

EH 메신저에서는 패킷을 헤더와 바디로 구성하기로 하였습니다. 바디는 메시지 종류에 따라 별도로 정의하지만 메시지 헤더는 아이디와 바디 길이로 같습니다. 먼저 메시지 헤더를 정의합시다.

struct MsgHead
{
    int msgid;
    int bdlen;
};

EHPacket 클래스에 선언한 메서드를 하나씩 정의합시다.

먼저 메시지를 보내는 곳에서 사용하는 EHPacket 생성자를 구현합시다.

EHPacket::EHPacket(int msgid)
{

먼저 패킷 버퍼의 시작 위치에 메시지 헤더 내용을 담을 것입니다. 패킷 버퍼를 MsgHead 포인터로 형변환합시다.

    MsgHead *mh = (MsgHead *)base;

입력 인자로 받은 메시지 아이디를 설정하고 바디 길이를 0으로 설정합니다.

    mh->msgid = msgid;
    mh->bdlen = 0;

그리고 바디가 시작하는 위치로 ptr을 설정합니다.

    ptr = GetBody();
}

소켓에서 데이터를 수신하여 EHPacket을 생성하는 생성자를 구현합시다.

EHPacket::EHPacket(SOCKET sock)
{

메시지 헤더와 바디 위치를 구합니다.

    MsgHead *mh = (MsgHead *)base;
    ptr = GetBody();

먼저 메시지 헤더를 수신한 후에 바디 위치에 바디 길이 만큼 수신합니다.

    recv(sock,(char *)mh,sizeof(MsgHead),0);
    recv(sock,ptr,mh->bdlen,0);
}

메시지 아이디를 반환하는 메서드를 구현합시다.

int EHPacket::GetMsgId()const
{

패킷 버퍼의 시작 위치에 있는 메시지 헤더의 멤버 아이드를 반환합니다.

    MsgHead *mh = (MsgHead *)base;
    return mh->msgid;
}

데이터를 패킷에 캡슐화하는 메서드를 구현합시다.

bool EHPacket::Packetize(void *data,int dlen)
{

메시지 헤더와 바디 위치를 구합니다.

    MsgHead *mh = (MsgHead *)base;
    char *body = GetBody();

만약 바디 길이와 추가할 데이터 길이와 메시지 헤더의 길이의 합이 최대 패킷 크기보다 크면 캡슐화할 수 없습니다.

    if(mh->bdlen + dlen + sizeof(MsgHead) > EH_PACKET_SIZE)
    {
        return false;
    }

바디 위치에서 바디 길이를 더한 위치에 데이터를 캡슐화합니다.

    memcpy(body+mh->bdlen,data,dlen);

바디 길이를 변경합니다.

    mh->bdlen += dlen;
    return true;
}

패킷의 데이터를 디캡슐화하는 메서드를 구현합시다.

bool EHPacket::DePacketize(void *buf,int blen)
{

메시지 헤더와 바디 위치를 구합니다.

    MsgHead *mh = (MsgHead *)base;
    char *body = GetBody();

현재까지 디캡슐화한 위치에서 바디 위치의 상대적 거리를 구합니다. 만약 바디 길이가 상대적 거리와 디캡슐화할 크기보다 작으면 디캡슐화할 수 없습니다.

    int gap = ptr - body;
    if(mh->bdlen < gap + blen)
    {
        return false;
    }

디캡슐화할 위치의 내용으로 버퍼에 내용을 복사합니다.

    memcpy(buf,ptr,blen);

디캡슐화할 위치를 변경합니다.

    ptr += blen;
    return true;
}

패킷을 전송하는 메서드를 구현합시다.

int EHPacket::Serialize(SOCKET sock)
{

먼저 메시지 헤더와 바디 위치를 구합니다.

    MsgHead *mh = (MsgHead *)base;

바디 길이와 메시지 헤더의 크기를 더한 전송할 패킷 길이를 구합니다.

    int total = mh->bdlen + sizeof(MsgHead);

소켓에 패킷을 전송합니다.

    return send(sock,base,total,0);
}

바디 위치를 구하는 메서드를 구현합시다.

char *EHPacket::GetBody()
{

바디 위치는 패킷 버퍼에서 메시지 헤더 크기를 더한 위치입니다.

    return base + sizeof(MsgHead);
}

실제 이와 같은 라이브러리를 만들 때는 먼저 라이브러리로 만들 로직을 간단한 콘솔 응용 프로젝트에서 테스트한 후에 안정성과 신뢰성 등을 테스트 한 후에 라이브러리로 제작하는 것이 전체 비용을 줄이는데 기여합니다. 이미 충분히 숙련 상태에 도달하여 굳이 테스트 과정을 거칠 필요가 없다고 생각하고 바로 라이브러리를 만들어도 논리적 버그가 발생합니다. 라이브러리로 만들었을 때 이를 사용하는 프로젝트에 버그가 발생했을 때 사용하는 라이브러리 때문에 발생한 버그를 판단하지 못할 때가 많습니다.

따라서 직접 라이브러리로 만들기 전에 만들려고 하는 로직이나 형식을 간단한 형태의 콘솔 응용 프로젝트에서 구현하고 테스트해 본 후에 라이브러리로 제작하길 권합니다.