가변 길이 데이터를 다루는 데 있어서 문자열은 매우 중요한 요소 중 하나라고 할 수 있을 것이다.
문자 집합이 뭔지, 인코딩이 뭔지에 대해서 정리해 보고 실제로 코드를 작성해 보자.
1. 익숙한 친구들부터
우리는 문자를 표현하기 위해 처음 프로그래밍을 배울 때부터 「char」라는 자료형을 써왔다.
익숙하지만 데이터를 어떻게 저장하는지에 대해서 깊게 고민해 본 사람이 얼마나 될까?
어떤 문자 집합을 사용하는지, 애초에 문자 집합과 인코딩을 제대로 구분하는 사람이 일반적으론 드문 것 같다.
아래의 다양한 자료형의 문자열을 통해 알아보자.
char sendData[1000] = "가";
char sendData1[1000] = u8"가";
WCHAR sendData2[1000] = L"가";
TCHAR sendData3[1000] = _T("가");
이미 구분할 줄 안다면 기초에 소홀하지 않고 계속 고민한 「진짜 프로그래머」가 아닐까 싶다.
각 자료형이 어떤 형태로 데이터를 저장하는지 메모리를 보면서 알아보자.
한글을 제대로 표현하지 못하는 녀석도 있다.
「sendData」, 「sendData2」, 「sendData3」은 제대로 한글을 표현해 준다.
모두 한글을 표현하는데 2바이트를 사용하고 있다는 것도 알 수 있다.
「sendData3」은 「_T()」의 형태로 표현했음에도 불구 「L"가"」로 표현된다. 이는 나중에 알아보자.
왜 「sendData1」만 제대로 표현을 못해줄까? 같은 문자를 던져줬고, 같은 2바이트를 사용해서 표현하는데도 의문스럽다.
이를 알기 위해선 먼저 「문자 집합」과 「인코딩」에 대해서 알아야 한다.
2. 문자 집합
흔히 「Character Set」이라고 부른다. 이걸 인코딩과 혼동하는 일도 적지 않다.
보통 둘이 세트로 다니기 때문에 굳이 구분하지도 않는 경우도 많다.
가장 널리 쓰이는 문자 집합으론 「유니코드」가 있다. 유니코드가 인코딩이 아니었나 싶은 사람도 있을 것이다.
아스키를 생각해 보자.
16진수로 41은 10진수로 65를 나타내며, 아스키로 나타내면 'A'가 된다.
여기서 41은 문자 집합이고 10진수나 아스키는 인코딩이 된다고 볼 수 있다.
같은 0x41이라는 데이터를 두고 인코딩이라고 하는 다양한 해석법이 존재하는 것이다.
Visual Studio에선 문자집합을 크게 2개로 나누는데, 아래와 같다.
- Multi Byte Character Set
- char
- 로마자 등을 1Byte로 표시하고, 기타 한글 등의 문자를 2 ~ 4Byte를 활용해 표시한다. - Wide Byte Character Set
- wchar
- 어떤 문자든 2Byte를 활용해 표시한다.
- 가장 많이 쓰이는 문자 집합
요즘은 WBCS 이외의 다른 것을 쓸 일이 거의 없다.
대부분의 문자가 UTF-16으로 표현되기 때문.
3. 인코딩
그럼 인코딩이랑 정확히 무엇인가?
UTF-16 인코딩은 아래와 같은 구성요소가 합쳐져 이루어져 있다.
- Unicode 문자 집합
- 2바이트씩 끊어 읽는 인코딩 방식
- BMP 뒤의 문자에 대해서는 4바이트 사용
엄밀히 말하면 문자 집합까지 포함하는 것은 아니지만 정상적으로 문자가 출력될 때를 가정한다.
다시 말하면 인코딩은 어떠한 바이트 배열에 대해 2바이트씩 끊어 읽어 특정 문자에 대응시키는 체계이다.
각 문자 집합에 따라 CP949, UTF-8, UTF-16 등의 문자 집합에 맞는 인코딩을 선택해 표현하게 된다.
그럼 BMP란 뭘까?
BMP는 Basic Multilingual Plane의 약자로, 유니코드에서 구분되는 평면 중 가장 자주 사용되는 평면이다.
우리가 사용하는 대부분의 문자는 0번 평면에 포함되어 있기 때문에 우린 BMP 평면을 활용하고 있는 것이다.
FFFF까지가 0번 평면으로, 10000부턴 1번 평면이다. 1개의 평면 당 65536개의 문자를 가짐을 알 수 있다.
여하튼 UTF-16에선, 1번 평면부턴 무조건 4바이트를 활용해 표현하게 된다.
4. 다시 보기
다시 처음의 문자열로 돌아가 보자.
각 자료형은 아래와 같은 특성을 가진다고 볼 수 있다.
char sendData[4] = "가"; // CP949 = KS-X-1001(한글 2Byte) + KS-X-1003 (Roman 1Byte)
char sendData1[4] = u8"가"; // UTF8 = Unicode (한글 3Byte) + (Roman 1Byte)
WCHAR sendData2[4] = L"가"; // UTF16 = Unicode (한글/로마 2바이트)
TCHAR sendData3[4] = _T("가"); // 떠넘기기. 프로젝트 속성에서 어떤 문자 집합을 사용할 것인지에 따라 달라질 수 있다.
그럼 TCHAR는 정확히 무엇일까?
아까 「L"가"」의 형태로 표현되는 것을 보고 감이 왔을 수도 있는데, WCHAR의 표현을 따라감을 알 수 있다.
하지만 무조건 WCHAR를 따라가는 것은 아니다. 이는 프로젝트 문자 집합 설정에 따라 달라질 수 있다.
TCHAR는 이 옵션에 따라 동작이 달라진다.
프로젝트 설정에서 「멀티바이트 문자 집합 사용」을 설정하면,
더 이상 UTF-16을 사용하지 않고 다른 형식으로 문자를 표현하게 된다.
대체로 건드릴 일이 없으므로 유니코드를 사용하게 둔다.
5. 문자열 송수신을 위한 함수 수정
문자열을 주고받을 수 있도록 하기 위해 함수에 수정이 필요하다.
송신하는 쪽부터 고쳐보자.
SendBufferRef ServerPacketHandler::Make_S_TEST(uint64 id, uint32 hp, uint16 attack, vector<BuffData> buffs, wstring name)
{
SendBufferRef sendBuffer = GSendBufferManager->Open(4096);
BufferWriter bw(sendBuffer->Buffer(), sendBuffer->AllocSize());
PacketHeader* header = bw.Reserve<PacketHeader>();
// id(uint64), 체력(uint32), 공격력(uint16)
bw << id << hp << attack;
// 가변 데이터
bw << (uint16)buffs.size();
for (BuffData& buff : buffs)
{
bw << buff.buffId << buff.remainTime;
}
// 여기부터
bw << (uint16)name.size();
bw.Write((void*)name.data(), name.size() * sizeof(WCHAR)); // "안녕하세요" 보낼 경우 5 X 2 = 10
// 여기까지
header->size = bw.WriteSize();
header->id = S_TEST; // 1 : Test Msg
sendBuffer->Close(bw.WriteSize());
return sendBuffer;
}
문자열의 사이즈를 uint16로 캐스팅하여 넘겨준다. 헤더의 size는 2Byte 이기 때문.
데이터를 쓸 때, 크기를 넘겨주는 부분에서 그냥 size()를 넘겨주는 것이 아니라 WCHAR의 사이즈만큼 곱해줬다.
1바이트가 아니라 2Byte 이기 때문에 주의하여야 한다.
받는 쪽에도 아래와 같은 내용을 추가해 준다.
wstring name;
uint16 nameLen;
br >> nameLen;
name.resize(nameLen);
br.Read((void*)name.data(), nameLen * sizeof(WCHAR));
// 로케일 설정을 하지 않으면 wcout으로 한글 출력이 되지 않음
wcout.imbue(std::locale("kor"));
wcout << name << endl;
버퍼에 데이터를 넘겨줬으니 이제 출력해야 한다.
그러나 wstring으로 받았기 때문에 그냥 cout으론 출력할 수 없어서 wcout을 사용해야 한다.
로케일 설정을 하지 않으면 한글이 정상적으로 출력되지 않기 때문에 로케일 설정을 해 주고 출력해 준다.
서버가 "안녕하세요"를 보내게 하고 테스트해 보자.
6. 실행 결과
잘 보내고 잘 받는 것 같으니 별 문제는 없다고 할 수 있겠다.
이제 본격적으로 패킷 직렬화에 대해 공부할 때가 온 것 같다.
Reference
'Study > C++ & C#' 카테고리의 다른 글
[C++] Protobuf (0) | 2023.07.13 |
---|---|
[C++] Packet Serialization (0) | 2023.07.12 |
[C++] Packet Handler (0) | 2023.07.08 |
[C++] Buffer Helpers (0) | 2023.07.07 |
[C#] PingPlugin (0) | 2023.07.06 |