Deep Studying

[프라우드넷] 채팅 서버 만들기(1) 프로젝트 초기 설정하기 본문

게임서버

[프라우드넷] 채팅 서버 만들기(1) 프로젝트 초기 설정하기

miniSeop 2022. 1. 10. 22:15

 이제 본격적으로 서버 코드를 짜기 앞서 C계열 언어답게 초기 세팅이 필요합니다. 이전 포스트를 읽고 PIDL이 무엇인지를 이해하는 편이 글을 읽기 편하겠지만, 굳이 보시지 않아도 따라서 하실 수 있게 작성해보겠습니다.

 

이전 포스트: RPC 통신 이해하기 (2) - PIDL 파일에 대해

 

 

프로젝트 생성

Server 프로젝트 생성

비주얼 스튜디오를 켠 뒤 새로운 솔루션을 만들어줍니다.

 

 

콘솔 앱을 먼저 생성해줍니다.

 

 

프로젝트 이름은 정해진 것은 아니지만 우선 ChattingServer로 하겠습니다.

솔루션 이름은 원하는 이름으로 정합니다.

 

 

 

원하는 닷넷 프레임워크를 선택하시고 프로젝트를 생성해줍니다.

저는 6.0버전밖에 없어서 그냥 만들었지만 4.X 버전은 물론 3.X 버전도 지원이 되는 것 같습니다.

 

 

Common 프로젝트 생성

 

 

솔루션을 우클릭하여 프로젝트를 추가해줍니다.

 

 

 

클래스 라이브러리를 선택합니다.

 

 

 

 

이름은 ChattingCommon으로 설정하겠습니다.

PIDL을 포함하여 클라이언트, 서버 모두 쓰는 공용 코드들을 작성할 프로젝트입니다.

 

 프로젝트 이름을 그냥 Common으로 하면 추후에 설정이 꼬이는 경우가 생깁니다. 해결 자체가 어려운건 아니지만 처음 배울때는 이런 것 하나하나가 진행을 더디게 하는 요소가 될테니 일단은 피하고 가는게 좋을 것 같습니다.

 

 

Client 프로젝트 생성

 

 

Server 프로젝트와 동일하게 콘솔 앱을 선택하여 생성해줍니다.

 

 

솔루션 탐색기에 위와 같이 보인다면 제대로 생성된 것입니다.

 

 

PIDL 세팅하기

PIDL 파일 생성하기

우선 Common프로젝트에 PIDL 폴더와 PIDL파일을 생성해줍니다.

    > Common 프로젝트 우클릭 -> 추가 -> 폴더

    > PIDL폴더 우클릭 -> 추가 -> 새 항목

 

 

C2S.PIDL 파일을 만들어줍니다. 그리고 같은 방법으로 S2C.PIDL 파일도 만들어줍니다.

 

// C2S.PIDL
global C2S 2000
{
	Chat([in] string str);
}
// S2C.PIDL
global S2C 3000
{
	NotifyChat([in] string str);
}

내용은 위와 같이 작성해줍니다.

 

 

PIDL 파일 컴파일하기

 

프로젝트 폴더안에 Common 폴더에 들어가 PIDL.bat 파일을 생성해줍니다.

 

 

 

PIDL.bat 파일의 내용은 다음과 같이 작성합니다.

cd %~dp0
"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" -cs .\PIDL\S2C.PIDL -outdir .\PIDL
"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" -cs .\PIDL\C2S.PIDL -outdir .\PIDL
pause

 

"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" 은 프라우드넷이 설치된 경로입니다.

상황에 맞춰서 변경해주세요.

 

 

 

PIDL.bat 파일을 실행하면 위와 같이 파일이 생성됩니다.

 

 

Common.lib 빌드하기

프로젝트를 빌드하기 위해서는 ProudNet의 라이브러리를 가져와야합니다.

 

       > Common 프로젝트의 종속성 탭 우클릭 -> 프로젝트 참조 추가 -> 찾아보기

 

       C:\Program Files (x86)\Nettention\ProudNet\lib\DotNet 에서

       > ProudDotNetClient.dll ProudDotNetServer.dll 추가

 

 

 

올바르게 추가가 되었다면 위와 같이 종속성에 ProudDotNetClient와 ProudDotNetServer가 보입니다.

 

 

namespace ChattingCommon
{
    public class Vars
    {
		public static System.Guid m_Version = new System.Guid("{ 0x3ae33249, 0xecc6, 0x4980, { 0xbc, 0x5d, 0x7b, 0xa, 0x99, 0x9c, 0x7, 0x39 } }");
		public static int m_serverPort = 33334;

		static Vars()
		{

		}
	}
}

Class1.cs 라는 파일 이름은 너무 모호하니 Common.cs로 변경하고 위와 같이 내용을 작성해줍니다.

Vars 클래스는 앞으로 클라이언트와 서버에서 공통으로 쓸 Object들을 정의하는 클래스가 될 것입니다.

 

이제 Common 프로젝트를 빌드합니다.

빌드에 성공했다면 Server와 Client 프로젝트로 넘어갑니다.

 

 

 

서버/클라이언트 프로젝트 세팅하기

 서버 프로젝트와 클라이언트 프로젝트는 동일하게 세팅하시면 됩니다. 포스트는 서버를 기준으로 설명드리겠습니다. 클라이언트도 똑같이 맞춰 세팅하시면 됩니다.

 

PIDL 수정하기

먼저 클라이언트 프로젝트와 서버 프로젝트에 각각 새 폴더를 추가하고 이름을 RMI로 합니다.

 

PIDL.bat에 명령어 두 줄을 추가해줍니다.

cd %~dp0
"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" -cs .\PIDL\S2C.PIDL -outdir .\PIDL
"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" -cs .\PIDL\C2S.PIDL -outdir .\PIDL
copy .\PIDL\*.cs ..\ChattingServer\RMI\
copy .\PIDL\*.cs ..\ChattingClient\RMI\
pause

copy로 시작되는 두 줄이 추가되었습니다.

 

이제 다시 PIDL.bat을 실행시키면 RMI 폴더에 common, proxy, stub 코드들이 생성되었을 것입니다.

 

common, proxy, stub코드를 복사하지 않고 추가->기존항목 을 통해 프로젝트에 추가하는 방법도 있습니다.

하지만 저는 해당 코드들을 gitignore에 등록해두려고 이러한 방법을 사용했습니다. 보시고 편한 방법을 선택하시면 됩니다.

 

 

종속성 추가하기

Common 프로젝트에서 처럼 Server와 Client 프로젝트에도 종속성을 추가해줍니다.

 

        > Server 프로젝트의 종속성 탭 우클릭 -> 프로젝트 참조 추가 -> 찾아보기

        C:\Program Files (x86)\Nettention\ProudNet\lib\DotNet 에서

        > ProudDotNetClient.dll  ProudDotNetServer.dll 추가

 

그리고 Common 프로젝트도 상속합니다.

> Server 프로젝트의 종속성 탭 우클릭 -> 프로젝트 참조 추가 -> 프로젝트 -> Common 선택

 

 

그 뒤에 아래 코드를 Server프로젝트의 Program.cs 파일에 복사해봅니다.

SimpleCSharp 프로젝트에서 조금만 변형한 코드입니다.

별 다른 동작도 하지 않는 서버 코드이며 빌드 테스트만을 위해 사용할 예정입니다.

using Nettention.Proud;
using ChattingCommon;

namespace ChattingServer
{

    class Program
    {
        // RMI stub instance
        // For details, check client source code first.
        static C2S.Stub g_Stub = new C2S.Stub();
        static S2C.Proxy g_Proxy = new S2C.Proxy();

        static void InitStub()
        {
            g_Stub.Chat = (Nettention.Proud.HostID remote, Nettention.Proud.RmiContext rmiContext, string str) =>
            {
                Console.Write("[Server] Chat :");
                Console.Write(" {0}\n", str);
                return true;
            };

        }

        internal static void StartServer(NetServer server, Nettention.Proud.StartServerParameter param)
        {
            if ((server == null) || (param == null))
            {
                throw new NullReferenceException();
            }

            try
            {
                server.Start(param);
            }
            catch (System.Exception ex)
            {
                Console.WriteLine("Failed to start server~!!" + ex.ToString());
            }

            Console.WriteLine("Succeed to start server~!!\n");
        }

        static void Main()
        {

            // Network server instance.
            NetServer srv = new NetServer();

            // set a routine which is executed when a client is joining.
            // clientInfo has the client info including its HostID.
            srv.ClientJoinHandler = (clientInfo) =>
            {
                Console.Write("Client {0} connected.\n", clientInfo.hostID);
            };

            // set a routine for client leave event.
            srv.ClientLeaveHandler = (clientInfo, errorInfo, comment) =>
            {
                Console.Write("Client {0} disconnected.\n", clientInfo.hostID);
            };

            InitStub();

            // Associate RMI proxy and stub instances to network object.
            srv.AttachStub(g_Stub);
            srv.AttachProxy(g_Proxy);

            var p1 = new StartServerParameter();
            p1.protocolVersion = new Nettention.Proud.Guid(Vars.m_Version); // This must be the same to the client.
            p1.tcpPorts.Add(Vars.m_serverPort); // TCP listening endpoint

            try
            {
                /* Starts the server.
                This function throws an exception on failure.
                Note: As we specify nothing for threading model,
                RMI function by message receive and event callbacks are
                called in a separate thread pool.
                You can change the thread model. Check out the help pages for details. */
                srv.Start(p1);
            }
            catch (Exception e)
            {
                Console.Write("Server start failed: {0}\n", e.ToString());
                return;
            }

            Console.Write("Server started. Enterable commands:\n");
            Console.Write("1: Creates a P2P group where all clients join.\n");
            Console.Write("2: Sends a message to P2P group members.\n");
            Console.Write("q: Quit.\n");

            while (true)
            {

                if (Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape && Console.ReadKey(true).Key == ConsoleKey.Delete)
                {
                    break;
                }
                System.Threading.Thread.Sleep(1000);
            }

            srv.Stop();
        }
    }
}

 

복사하셨다면 빌드하여 실행해봅니다. 아마 빌드는 성공하고 실행에는 실패할 것입니다.

 

 

빌드 및 실행해보기

 

 

원인은 즉, 서버/클라이언트는 실행파일과 같은 디렉터리에 몇 가지 lib 파일이 있어야 정상 실행됩니다.

프라우드넷 디렉터리에서 가져오도록 합니다.

 

  C:\Program Files (x86)\Nettention\ProudNet\lib\DotNet\x64

  C:\Program Files (x86)\Nettention\ProudNet\Sample\bin\netcoreapp3.1

 

둘 중 아무 디렉터리에서나 가져와도 무방합니다.

 

파일은 총 6개이며 다음과 같습니다.

 

libcrypto-1_1-x64.dll

libssl-1_1-x64.dll

ProudNetClient.dll

ProudNetClientPlugin.dll

ProudNetServer.dll

ProudNetServerPlugin.dll

 

위 여섯 개 파일을 프로젝트 폴더 안 Server\bin\Debug\net.6.0 디렉터리에 넣습니다.

 

비주얼 스튜디오의 버전이나 닷넷의 버전에 따라 빌드 파일의 위치가 달라질 수 있습니다.

이 부분은 알맞게 변경하여 Server.exe가 빌드된 폴더에 해당 6개 파일을 추가해주세요

 

파일을 추가했다면 다시 빌드 후 실행해봅니다.

 

 

서버가 정상적으로 실행되었습니다.

 

클라이언트도 같은 방식으로 세팅을 한 뒤 빌드와 실행해 성공하는지만 확인합니다.

 

 

 

프로젝트 초기화

 

테스트가 끝났다면 서버와 클라이언트 코드를 지우고 아래와 새 클래스를 생성해줍니다.

 

process 디렉터리 > Process.cs

Handler.cs

ServerLauncher.cs

 

 

프로젝트의 파일트리는 이런 모습입니다.

앞으로의 프로젝트는 아래와 같이 초기화된 상태에서 시작할 계획입니다.

 

아래 소스코드들을 작성해주시고 다음 포스트를 봐주시기 바랍니다.

 

다음 포스트: 채팅서버 만들기(2) RMI 통신하기

 

Server 프로젝트

Program.cs

namespace ChattingServer
{

    class Program
    {
        static void Main()
        {
            ServerLauncher server = new ServerLauncher();

            try
            {
                server.ServerStart();
                Console.Write("Server started\n");

                while (server.RunLoop)
                {
                    if (Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape && Console.ReadKey(true).Key == ConsoleKey.Delete)
                    {
                        break;
                    }
                    System.Threading.Thread.Sleep(1000);
                }

                Console.Write("Server Closed\n");
                server.Dispose();

            }
            catch (Exception e)
            {
                Console.Write(e.ToString());
            }
        }
    }
}

 서버 프로젝트의 Main입니다. 필요하다면 키 입력을 받고 명령을 처리합니다. 주요한 구현은 아래 코드들에서 합니다.

 

ServerLauncher.cs

using ChattingCommon;
using Nettention.Proud;

namespace ChattingServer
{
    public class ServerLauncher
    {
        public bool RunLoop;
        public static readonly NetServer NetServer = new NetServer();
        private readonly Nettention.Proud.ThreadPool _netWorkerThreadPool = new Nettention.Proud.ThreadPool(8);
        private readonly Nettention.Proud.ThreadPool _userWorkerThreadPool = new Nettention.Proud.ThreadPool(8);

        Handler Handler = new Handler();
        process.CommonProcess Process = new process.CommonProcess();

        public void InitializeStub()
        {
            Process.InitStub();
        }
        public void InitializeHandler()
        {
            NetServer.ConnectionRequestHandler = Handler.ConnectionRequestHandler;
            NetServer.ClientHackSuspectedHandler = Handler.ClientHackSuspectedHandler;
            NetServer.ClientJoinHandler = Handler.ClientJoinHandler;
            NetServer.ClientLeaveHandler = Handler.ClientLeaveHandler;
            NetServer.ErrorHandler = Handler.ErrorHandler;
            NetServer.WarningHandler = Handler.WarningHandler;
            NetServer.ExceptionHandler = Handler.ExceptionHandler;
            NetServer.InformationHandler = Handler.InformationHandler;
            NetServer.NoRmiProcessedHandler = Handler.NoRmiProcessedHandler;
            NetServer.P2PGroupJoinMemberAckCompleteHandler = Handler.P2PGroupJoinMemberAckCompleteHandler;
            NetServer.TickHandler = Handler.TickHandler;
            NetServer.UserWorkerThreadBeginHandler = Handler.UserWorkerThreadBeginHandler;
            NetServer.UserWorkerThreadEndHandler = Handler.UserWorkerThreadEndHandler;
        }
        public void InitialzieServerParameter()
        {
            var parameter = new StartServerParameter();
            parameter.protocolVersion = new Nettention.Proud.Guid(Vars.m_Version);
            parameter.tcpPorts.Add(Vars.m_serverPort);
            NetServer.Start(parameter);
        }
        public void ServerStart()
        {
            InitializeHandler();
            InitializeStub();
            InitialzieServerParameter();
            RunLoop = true;
        }
        public void Dispose()
        {
            NetServer.Dispose();
        }
    }
}

실질적인 ProudNet 서버를 실행시키는 역할을 합니다.

 

Handler.cs

using System.Diagnostics.CodeAnalysis;
using Nettention.Proud;
namespace ChattingServer
{
    internal class Handler
    {
        process.CommonProcess Process = new process.CommonProcess();

        public bool ConnectionRequestHandler(AddrPort clientAddr, ByteArray userDataFromClient, [NotNull] ByteArray reply)
        {
            reply = new ByteArray();
            reply.Clear();

            return true;
        }

        public void ClientHackSuspectedHandler(HostID clientId, HackType hackType)
        {

        }

        public void ClientJoinHandler(NetClientInfo clientInfo)
        {

        }

        public void ClientLeaveHandler(NetClientInfo clientInfo, ErrorInfo errorinfo, ByteArray comment)
        {

        }

        public void ErrorHandler(ErrorInfo errorInfo)
        {

        }

        public void WarningHandler(ErrorInfo errorInfo)
        {

        }

        public void ExceptionHandler(Exception e)
        {

        }

        public void InformationHandler(ErrorInfo errorInfo)
        {

        }

        public void NoRmiProcessedHandler(RmiID rmiId)
        {

        }

        public void P2PGroupJoinMemberAckCompleteHandler(HostID groupHostId, HostID memberHostId, ErrorType result)
        {

        }

        public void TickHandler(object contextBoundObject)
        {

        }

        public void UserWorkerThreadBeginHandler()
        {

        }

        public void UserWorkerThreadEndHandler()
        {

        }
    }
}

통신에 대한 핸들러들을 관리합니다.

 

Process.cs

using Nettention.Proud;

namespace ChattingServer.process
{
    internal class CommonProcess
    {
        static S2C.Proxy S2CProxy = new S2C.Proxy();
        static C2S.Stub C2SStub = new C2S.Stub();

        public void InitStub()
        {
            ServerLauncher.NetServer.AttachProxy(S2CProxy);
            ServerLauncher.NetServer.AttachStub(C2SStub);
        }

    }
}

통신에 관한 로직을 처리를 합니다. Stub과 Proxy에 관련된 내용이 주로 나옵니다.

 

 

Client 프로젝트

Program.cs

// Program.cs
using Nettention.Proud;
using ChattingCommon;

namespace Client
{
    class Program
    {
        static object g_critSec = new object();
        static NetClient netClient = new NetClient();
        static S2C.Stub S2CStub = new S2C.Stub();
        static C2S.Proxy C2SProxy = new C2S.Proxy();
        static bool isConnected = false;
        static bool keepWorkerThread = true;

        static void InitializeStub()
        {

        }
        static void InitializeHandler()
        {
            netClient.JoinServerCompleteHandler = (info, replyFromServer) =>
            {
                lock (g_critSec)
                {
                    if (info.errorType == ErrorType.Ok)
                    {
                        Console.Write("Succeed to connect server. Allocated hostID={0}\n", netClient.GetLocalHostID());
                        isConnected = true;
                    }
                    else
                    {
                        Console.Write("Failed to connect server.\n");
                        Console.WriteLine("errorType = {0}, detailType = {1}, comment = {2}", info.errorType, info.detailType, info.comment);
                    }
                }
            };

            netClient.LeaveServerHandler = (errorInfo) =>
            {
                lock (g_critSec)
                {
                    Console.Write("OnLeaveServer: {0}\n", errorInfo.comment);

                    isConnected = false;
                    keepWorkerThread = false;
                }
            };

        }
        static void initializeClient()
        {
            netClient.AttachStub(S2CStub);
            netClient.AttachProxy(C2SProxy);
        }
        static void InitializeClientParameter()
        {
            NetConnectionParam cp = new NetConnectionParam();
            cp.protocolVersion.Set(Vars.m_Version);
            cp.serverIP = "localhost";
            cp.serverPort = (ushort)Vars.m_serverPort;
            netClient.Connect(cp);
        }
        static void Main(string[] args)
        {
            InitializeHandler();
            initializeClient();
            InitializeStub();
            InitializeClientParameter();

            Thread workerThread = new Thread(() =>
            {
                while (keepWorkerThread)
                {
                    Thread.Sleep(10);
                    netClient.FrameMove();
                }
            });
            workerThread.Start();


            while (keepWorkerThread)
            {
                string userInput = Console.ReadLine();
                if (userInput == "q")
                {
                    keepWorkerThread = false;
                }
            }
            workerThread.Join();
            netClient.Disconnect();
        }
    }
}