[C++/C#] C# 채팅 클라이언트 간보기

2023. 8. 1. 22:15·Study/C++ & C#

왜 간보기냐면 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
'Study/C++ & C#' 카테고리의 다른 글
  • [C++] JobQueue (2 / 3)
  • [C++] JobQueue (1 / 3)
  • [C++] IOCP를 활용한 채팅 서버 구현
  • [C++/Python] 패킷 자동화
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (168)
      • Thoughts (14)
      • Study (69)
        • Japanese (3)
        • C++ & C# (46)
        • Javascript (3)
        • Python (14)
        • Others (3)
      • Play (1)
        • Battlefield (1)
      • Others (11)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    Selenium
    스타필드
    7계층
    c#
    네트워크
    boost
    JS
    클라우드
    bot
    discord py
    Python
    프로그래머스
    로깅
    서버
    네트워크 프로그래밍
    cloudtype
    db
    FF14
    포인터
    docker
    C++
    베데스다
    Server
    Asio
    OSI
    암호화
    discord
    IOCP
    Network
    Dalamud
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
[C++/C#] C# 채팅 클라이언트 간보기
상단으로

티스토리툴바