왜 간보기냐면 C#으로 모델을 구현하고 패킷 핸들링을 진행하지 않기 때문.
입력을 받고 출력하기만 하는 창구 역할만 수행한다.
1. 구현 방법
이미 이전에 파이썬을 활용해 패킷 자동화까지 해 놓았다.
아무래도 C#에서까지 같은 작업을 하기보단 있는 걸 끌어오는 게 편할 것 같았다.
그래서 기존의 DummyClient
를 DLL로 만들어서 P/Invoke
를 활용하기로 했다.
따라서 아래의 기능들이 필요하게 된다.
- 접속부터 패킷 핸들링 까지 할 메인 스레드
- 상호 간 데이터를 전달하기 위한 송수신 데이터용 Queue
- 데이터를 주고받기 위한 C++ 함수들
- 클라이언트에서 출력과 입력을 받을 각각의 스레드
2. 메인 스레드
거창할 건 없고 그냥 기존 코드의 재활용이다.
extern "C" {
__declspec(dllexport) void RunThread()
{
ServerPacketHandler::Init();
this_thread::sleep_for(1s);
ClientServiceRef service = MakeShared<ClientService>(
NetAddress(L"127.0.0.1", 7777),
MakeShared<IocpCore>(),
MakeShared<ServerSession>, // TODO : SessionManager 등
1);
ASSERT_CRASH(service->Start());
for (int32 i = 0; i < 2; i++)
{
GThreadManager->Launch([=]()
{
while (true)
{
service->GetIocpCore()->Dispatch();
}
});
}
// TODO
GThreadManager->Join();
}
}
TODO는 나중에 채운다.
2. Queue
#pragma once
#include "Protocol.pb.h"
public class RecvChatQueue
{
public:
static string RecvGetQueueString()
{
string tmp = _RecvChatQueue.front();
_RecvChatQueue.pop();
return tmp;
}
static bool RecvIsEmpty()
{
return _RecvChatQueue.empty();
}
static uint32 RecvGetCount()
{
return _RecvChatQueue.size();
}
static Queue<string> _RecvChatQueue;
};
public class SendChatQueue
{
public:
static Protocol::C_CHATMSG SendGetQueueString()
{
Protocol::C_CHATMSG tmp = _SendChatQueue.front();
_SendChatQueue.pop();
return tmp;
}
static bool SendIsEmpty()
{
return _SendChatQueue.empty();
}
static uint32 SendGetCount()
{
return _SendChatQueue.size();
}
static bool HandleSendQueue(SendBufferRef* buf);
static Queue<Protocol::C_CHATMSG> _SendChatQueue;
};
그냥 전부 static
으로 때려 박았다.
송신 Queue에 대해서만 핸들러 함수를 두었다.
#include "pch.h"
#include "ChatQueue.h"
#include "ServerPacketHandler.h"
#include <string>
#include <sstream>
bool SendChatQueue::HandleSendQueue(SendBufferRef* buf)
{
if (SendIsEmpty())
return false;
Protocol::C_CHATMSG tmpPkt = SendGetQueueString();
string contentTmp = tmpPkt.content();
istringstream ss(contentTmp);
string strBuf;
getline(ss, strBuf, ' ');
if (strBuf == "/p")
{
contentTmp.erase(0, strBuf.size() + 1);
tmpPkt.set_channel(Protocol::MSG_CHANNEL_PARTY);
tmpPkt.set_content(contentTmp);
*buf = ServerPacketHandler::MakeSendBuffer(tmpPkt);
}
else if (strBuf == "/g")
{
contentTmp.erase(0, strBuf.size() + 1);
tmpPkt.set_channel(Protocol::MSG_CHANNEL_GLOBAL);
tmpPkt.set_content(contentTmp);
*buf = ServerPacketHandler::MakeSendBuffer(tmpPkt);
}
else if (strBuf == "/join")
{
Protocol::C_JOIN_PARTY joinPkt;
*buf = ServerPacketHandler::MakeSendBuffer(joinPkt);
}
else if (strBuf == "/leave")
{
Protocol::C_LEAVE_PARTY leavePkt;
*buf = ServerPacketHandler::MakeSendBuffer(leavePkt);
}
else
*buf = ServerPacketHandler::MakeSendBuffer(tmpPkt);
return true;
}
「 / 」가 첫 문자열이라면 명령어 입력으로 간주하여 명령어가 아닌 것들을 걸러내야 하지만 일단 동작을 확인하고 싶었다.
여기선 이전과 같이 문자열을 잘라서 앞의 내용을 확인한 후 분기하도록 했다.
처리가 다 끝나면 SendBuffer
를 만들어 buf
에 넘겨주고 결과를 리턴한다.
C_CHATMSG를 기본으로 활용하는 것은 썩 좋진 않지만 C#에서 C++로 직렬화해서 넘길 때,
데이터가 잘 넘어가는지 궁금했던 것도 있고 인자를 줄이는데도 도움이 되더라.
이제 이 함수를 메인 스레드에서 활용한다.
GThreadManager->Launch([=]()
{
SendBufferRef sendBuffer;
while (true)
{
if (!SendChatQueue::HandleSendQueue(OUT &sendBuffer))
continue;
service->Send(sendBuffer);
}
});
버퍼를 넘겨주면 함수를 호출해 내용을 채우고 그 버퍼를 보내는 것을 반복한다.
3. 네이티브 함수들
C# 클라이언트와의 데이터 전송을 위한 C++ 네이티브 함수들이다.
// C++
extern "C" {
__declspec(dllexport) bool RecvGetQueueString(char* buffer)
{
if (RecvChatQueue::RecvIsEmpty())
return false;
string data = RecvChatQueue::RecvGetQueueString();
strcpy(buffer, data.c_str());
return true;
}
}
extern "C" {
__declspec(dllexport) bool RecvIsEmpty()
{
return RecvChatQueue::RecvIsEmpty();
}
}
extern "C" {
__declspec(dllexport) uint32 RecvGetCount()
{
return RecvChatQueue::RecvGetCount();
}
}
extern "C" {
__declspec(dllexport) void Send(BYTE* input, int size, const char* inputContent)
{
Protocol::C_CHATMSG pkt;
pkt.ParseFromArray(input, size);
pkt.set_content(inputContent);
SendChatQueue::_SendChatQueue.push(pkt);
}
}
// C#
[DllImport("ChatClientLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void RunThread();
[DllImport("ChatClientLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern bool RecvGetQueueString([Out] StringBuilder buffer);
[DllImport("ChatClientLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern bool RecvIsEmpty();
[DllImport("ChatClientLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Send(byte[] serial, int size, string inputContent);
처음엔 그냥 string
으로 주고받으면 될 줄 알았다.
이래 저래 찾아본 결과 포인터를 사용해 넘겨줘야 함을 알았다.
그래서 char*
를 활용해 문자열 데이터를 주고받게 했다.
내가 여기서 wchar_t*
가 아니라 char*
를 사용한 이유는 후술.
4. 입출력 스레드
private const int BUFFER_SIZE = 4096;
private static MsgChannel channel = Protocol.MsgChannel.Global;
private void PrintQueueThread()
{
while (true)
{
if (!RecvIsEmpty())
{
StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
if (RecvGetQueueString(buffer))
{
Console.Write(buffer.ToString());
}
else
{
Console.WriteLine("에러 발생");
}
}
}
}
private void InputThread()
{
while (true)
{
string input = Console.ReadLine();
if (string.IsNullOrEmpty(input))
continue;
else
{
if (input == "/p")
{
channel = MsgChannel.Party;
continue;
}
else if (input == "/g")
{
channel = MsgChannel.Global;
continue;
}
// UI에서 설정 받아오게
Protocol.C_CHATMSG pkt = new()
{
Channel = channel,
ChatType = MsgType.Common,
};
int size = pkt.CalculateSize();
byte[] pktBuf = new byte[size];
pkt.WriteTo(pktBuf);
Send(pktBuf, size, input);
}
}
}
처음엔 C_CHATMSG
에 문자열 데이터도 같이 넣어서 직렬화해서 넘겨주려고 했다.
C++에서 역직렬화하니 문제가 생기더라.
그렇게 하다가 답을 찾은 게 const char*
로 문자열을 전달받는 것이다.
근데 진짜 왜 되는지 모르겠다.
분명 C#은 입력을 UTF16으로 받을 텐데 왜 그대로 C++에서 char
로 받아도 인코딩 문제가 안 생기는 건지...
C++의 큐에서 문자열을 뽑아오는 건 char*
를 넘겨줘 C#에서 StringBuilder
로 받으니 문제가 없었다.
진짜 로마자는 나오는데 한글은 안 나오는 인코딩 문제 때문에 시간을 엄청 많이 소모해 버렸다.
여하튼 일단(...) 나오긴 한다.
5. 잘 나오나?
일단 나오나 보긴 하자.
채널 전환도 잘 되고 한글 출력도 문제없다.
FF14의 채팅 시스템을 조금 따라 해봤다.
이제 이걸 유니티에 올려보자...
'Study > C++ & C#' 카테고리의 다른 글
[C++] JobQueue (2 / 3) (0) | 2023.08.19 |
---|---|
[C++] JobQueue (1 / 3) (0) | 2023.08.16 |
[C++] IOCP를 활용한 채팅 서버 구현 (0) | 2023.07.21 |
[C++/Python] 패킷 자동화 (0) | 2023.07.13 |
[C++] Protobuf (0) | 2023.07.13 |