Deep Studying

[프라우드넷] 채팅 서버 만들기 (3) Marshaler 이용하기 본문

게임서버

[프라우드넷] 채팅 서버 만들기 (3) Marshaler 이용하기

miniSeop 2022. 1. 12. 23:01

 지난 포스트에서는 RMI 통신을 이용하여 정말 간단한 채팅 서버를 만들어보았습니다.

 하지만 PIDL을 통해 생성한 Proxy와 Stub은 기본적으로 ProudNet 라이브러리의 일부 혹은 닷넷에서 지원하는 기본 자료형 밖에 사용하지 못합니다. 지난 포스트에서는 string 값만 주고 받았기 때문에 문제가 되지 않았던 것입니다.

 이번 포스트에서는 유저의 닉네임을 출력하는 기능을 넣으면서 직접 만든 클래스를 RMI로 전송하는 법을 알아보도록 하겠습니다.

 

이전 포스트: 채팅 서버 만들기 (2) RMI 통신 이용하기

 

 

요구사항

   메세지는 시스템 메세지 채팅 메세지 두 종류이다.

- 클라이언트가 접속하면 모든 클라이언트에게 시스템 메세지를 보낸다.

- 클라이언트가 접속을 종료하면 모든 클라이언트에게 시스템 메세지를 보낸다.

+ 유저는 로그인 한 뒤 채팅을 할 수 있다.

+ 로그인 할 때는 사용할 닉네임을 서버로 보낸다. 로그인에 성공하면 서버는 유저 정보 객체를 클라이언트로 보낸다. 

+ 클라이언트가 로그인하면 모든 클라이언트에게 시스템 메세지를 보낸다.

   클라이언트가 채팅을 입력하면 모든 클라이언트에게 채팅 메세지를 전달한다.

 

지난 요구사항에서 두 가지가 사라지고 세 가지가 추가되었습니다. 추가되는 로직은 다음과 같습니다.

 

1. 클라이언트의 로그인 메세지 전송 ( C2S, 파라미터로 닉네임 전송 )

2. 서버에서 로그인 성공 메세지 전송 ( S2C )

3. 서버에서 로그인 성공 메세지가 오기 전까지 클라이언트는 채팅을 칠 수 없음

4. 클라이언트가 로그인하면 시스템 메세지 전송

 

서버에 집중한 포스트이기 때문에 사실 3번은 필수 사항이 아니라고 생각하고, 4번은 지난 포스트에서 클라이언트가 연결되면 SystemChat을 호출하던 것과 똑같습니다. 따라서 1번과 2번에 집중하여 설명하도록 하겠습니다.

 

 

유저 클래스 등록

Common > Common.cs

using Nettention.Proud;

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()
		{

		}
	}
	public class User
	{
		static int UserId = 0;
		public HostID HostId { get; set; }
		public string UserName { get; set; }
		public int UserID { get; set; }
		public User(string UserName, HostID HostId)
		{
			UserID = ++UserId;
			this.UserName = UserName;
			this.HostId = HostId;
		}
		public User()
		{
			UserName = "Unknown";
		}
	}
}

 

 유저 아이디는 간단하게 static 변수를 이용하여 1씩 증가하도록 했습니다. 공유 자원에 대한 이슈가 따라올 수 있는 위험한 값이지만 단지 테스트를 위해서라면 이보다 간단한 설정은 없을 것 같습니다.

 User(string UserName, HostID HostId) 생성자는 새로운 유저가 접속했을 때만 호출하겠습니다.

 

 

Marshaler 작성

Common 프로젝트에 Marshaler.cs 파일을 생성한 뒤 아래와 같은 내용을 작성해줍니다. 설명은 아래 덧붙이겠습니다.

 

Common > Marshaler.cs

using Nettention.Proud;
namespace ChattingCommon
{
    public class Marshaler : Nettention.Proud.Marshaler
    {
        public static void Write(Message msg, User user)
        {
            msg.Write(user.HostId);
            msg.Write(user.UserName);
            msg.Write(user.UserID);
        }
        public static User Read(Message msg, out User user)
        {
            msg.Read(out HostID HostId);
            msg.Read(out string UserName);
            msg.Read(out int UserID);

            user = new User();
            user.HostId = HostId;
            user.UserName = UserName;
            user.UserID = UserID;

            return user;
        }
    }
}

 

 Marshaling은 객체의 메모리 값을 RMI 메세지에 넣는 과정입니다. 직렬화( Serialize )라고 부르기도 합니다.

 메세지를 수신하는 쪽에서 해당 객체의 복사본을 만들 수 있게 하는 것이 목적입니다. 그러기 위해서는 객체를 메세지로 marshaling 하는 규칙이 필요하고, 반대로 메세지를 객체로 unmarshaling 하는 과정도 필요합니다.

 

 ProudNet에서 제공하는 Marshaler는 기본 자료형에 대한 marshaling 처리를 모두 지원합니다. 하지만 우리가 작성한 클래스에 대해서는 직접 marshaling하는 로직을 작성해야하는데요, 연산자 오버로딩과 비슷한 개념입니다.

 

Write( ) 함수는 marshaling규칙을, Read( ) 함수는 unmarshaling 규칙을 오버로딩합니다.

 marshaling 과정에서는 위 코드와 같이 Message 객체에 기본 자료형 혹은 이미 marshaling 규칙을 정의해둔 Class의 객체를 Message.Write( ) 함수를 사용하여 하나씩 넣어주어야 하고 unmarshaling 과정에서는 메세지 객체에 담겨진 자료들을 Message.Read( ) 함수를 사용하여 하나씩 꺼내야합니다.

 

out 파라미터에 대한 이해가 아직 모잘라 Read 함수의 정의가 약간 어색해보입니다.

동작은 잘 되니 이대로 사용하셔도 되고 추후에 변경하도록 하겠습니다.

 

 

PIDL파일 수정

 

1. Login 요청과 그에 대한 응답인 ResponseLogin 요청을 각각 추가하겠습니다.

2. 또한 채팅을 주고받을 때 유저 정보도 같이 받도록 기존 요청을 수정하겠습니다.

3. PIDL에서 Marshaler를 지정할 수 있습니다. 아래와 같이 작성해줍니다.

 

C2S.PIDL

[marshaler(cs) = ChattingCommon.Marshaler]
global C2S 2000
{
	Chat([in] string str);
	Login([in] String UserName);
}

S2C.PIDL

[marshaler(cs) = ChattingCommon.Marshaler]
global S2C 3000
{
	NotifyChat([in]string UserName,[in] string str);
	SystemChat([in] string str);
	ResponseLogin([in] ChattingCommon.User user);
}

 

작성하셨다면 PIDL.bat 을 실행해줍니다.

 

Stub 등록

 

지난 포스트의 순서대로 Stub 객체에 함수들을 먼저 등록해주겠습니다.

그리고 PIDL 규칙이 바뀌면서 오류가 나는 라인도 같이 바꿔주겠습니다.

 

Server > Process.cs

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

        public void InitStub()
        {
            C2SStub.Chat = Chat;
            C2SStub.Login = Login;

            ServerLauncher.NetServer.AttachProxy(S2CProxy);
            ServerLauncher.NetServer.AttachStub(C2SStub);
        }
        // Chat 함수 로직 작성
        static public bool Chat(HostID remote, RmiContext rmiContext, string str)
        {
            User user = new User();
            Console.WriteLine("{0}: {1}",user.UserName, str);
            S2CProxy.NotifyChat(ServerLauncher.NetServer.GetClientHostIDs(), rmiContext, user.UserName, str);
            return true;
        }
        static public bool Login(HostID remote, RmiContext rmiContext, string UserName)
        {
            return true;
        }

        public void SystemChat(string str)
        {
            S2CProxy.SystemChat(ServerLauncher.NetServer.GetClientHostIDs(), RmiContext.ReliableSend, str);
        }
    }
}

Chat 함수의 로직이 바뀌었습니다.

S2CProxy.NotifyChat( )을 실행할 때 UserName을 넣어주도록 바뀌었으므로 유저 객체를 생성하여 파라미터에 추가해줍니다. 우선 이렇게만 두고 실제 로직을 구현할 때 파라미터로 온 HostID로 유저 정보를 가져오도록 바꾸도록 하겠습니다.

 

Login 메소드도 추가하고 C2SStub에 추가합니다. 

 

Client > Program.cs 

    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 bool isLoggedin = false;
        static User me = new User();
        
        static void InitializeStub()
        {
            S2CStub.SystemChat = (HostID remote, RmiContext rmiContext, string str) =>
            {
                lock (g_critSec)
                {
                    Console.WriteLine("[System] {0}", str);
                }
                return true;
            };
            S2CStub.NotifyChat = (HostID remote, RmiContext rmiContext, string UserName, string str) =>
            {
                lock (g_critSec)
                {
                    Console.WriteLine("{0}: {1}", UserName, str);
                }
                return true;
            };
            S2CStub.ResponseLogin = (HostID remote, RmiContext rmiContext, User user) =>
            {
                lock (g_critSec)
                {
                    isLoggedin = true;
                    me = user;
                }
                return true;
            };
        }
        
        ...
        
}

bool isLoggedin 변수를 추가하였습니다. 이를 이용하여 로그인이 되어있는지 여부를 검사합니다.

User me 변수를 추가하였습니다. 서버에서 전달받은 유저의 정보를 저장합니다.

 

NotifyChat( ) 함수는 파라미터로 받은 UserName을 같이 출력하도록 변경하였습니다.

ResponseLogin 요청을 받으면 isLoggedin 변수를 true로 바꾸고 me에 유저 정보를 등록해줍니다.

 

Client 메인 루프 변경

isConnected와 isLoggedin 변수를 적절히 이용하여 클라이언트의 코드가 생각한 대로 실행할 수 있도록 메인 루프를 변경합니다.

 

Client > Program.cs 

        static void Main(string[] args)
        {
            ...
            
            // Connect 될 때 까지 기다립니다.
            while (!isConnected)
                Thread.Sleep(1000);
            Console.Write("UserName: ");
            
            while (keepWorkerThread)
            {
                string userInput = Console.ReadLine();
                if (userInput == "")
                    continue;

                if (isLoggedin)
                {
                    if (userInput == "q")
                        keepWorkerThread = false;
                    else
                        C2SProxy.Chat(HostID.HostID_Server, RmiContext.ReliableSend, userInput);
                }
                else
                    C2SProxy.Login(HostID.HostID_Server, RmiContext.ReliableSend, userInput);
                
            }
            
            ...
            
        }

 

Login 처리

서버에 Dictionary 등록

채팅을 할 때마다 유저의 닉네임을 보내주기 위해서는 접속한 유저의 정보를 저장하고, 조회할 수 있는 자료구조가 필요합니다.

방법은 여러가지가 있겠지만 UserID를 Key값으로 사용하는 Dictionary를 선언해주겠습니다.

public class ServerLauncher
{

	....
    
	public static ConcurrentDictionary<HostID, User> UserList { get; } = new ConcurrentDictionary<HostID, User>();

	....
    
}

 

로그인 로직 추가

static public bool Login(HostID remote, RmiContext rmiContext, string UserName)
{
	string message = string.Format("{0} entered.", UserName);
    
	// 1. 새 유저 객체 추가
	User user = new User(UserName, remote);

	// 2. 유저를 Dictionary에 등록
	ServerLauncher.UserList.TryAdd(remote, user);
    
	// 3. 로그인 처리 및 메세지 전송
	S2CProxy.ResponseLogin(user.HostId, rmiContext, user);
	S2CProxy.SystemChat(ServerLauncher.NetServer.GetClientHostIDs(), RmiContext.ReliableSend, message);

	Console.WriteLine(message);
	return true;
}

로그인 로직은 위와 같이 작성하겠습니다.

 

Chat 로직 변경 ( 유저 검색 )

Chat에서 임시로 만든 User객체를 사용하고 있었습니다. 위에서 만든 Dictionary에서 User 객체를 검색하는 방식으로 변경합니다.

static public bool Chat(HostID remote, RmiContext rmiContext, string str)
{
	ServerLauncher.UserList.TryGetValue(remote, out User user);
	Console.WriteLine("{0}: {1}",user.UserName, str);
	S2CProxy.NotifyChat(ServerLauncher.NetServer.GetClientHostIDs(), rmiContext, user.UserName, str);
	return true;
}

 

 

핸들러 코드 삭제

이제 클라이언트가 Connect 되었을 때와 Disconnect 되었을 때, 시스템 메세지를 보내지 않도록 삭제합니다.

저는 서버 로그로는 남겨두는 편이 좋을 것 같아 Console.WriteLine( ) 부분은 남겨두었습니다.

Server > Handler.cs

public void ClientJoinHandler(NetClientInfo clientInfo)
{
	string message = string.Format("Host{0} connected", clientInfo.hostID);
	Console.WriteLine(message);
}

public void ClientLeaveHandler(NetClientInfo clientInfo, ErrorInfo errorinfo, ByteArray comment)
{
	string message = string.Format("Host{0} disconnected", clientInfo.hostID);
	Console.WriteLine(message);
}

 

실행

이제 서버와 클라이언트를 각각 빌드한 뒤 실행해보겠습니다.

 

유저 로그인

 

 

유저가 로그인 할 때마다 원래 로그인 되어있던 클라이언트에 시스템 메세지가 가는 것을 볼 수 있습니다.

또한 서버 콘솔에서는 유저의 로그인과 유저의 connect가 따로 찍히는 것도 확인할 수 있습니다.

 

 

채팅 입력

 

 

채팅을 입력했을 때, UserName값을 받아서 잘 출력해주는 모습입니다.

 

 

여기까지 작성한 전체 코드는 아래 github 링크에 남겨두겠습니다.

https://github.com/rltjqdl1138/proudnet-chatting-server/tree/%40deploy/simple-chatting

 

GitHub - rltjqdl1138/proudnet-chatting-server

Contribute to rltjqdl1138/proudnet-chatting-server development by creating an account on GitHub.

github.com

 

 

추가 기능

포스트를 다 작성할 때 쯤 되니 로그아웃 기능도 넣어보면 좋을 것 같다는 생각이 듭니다.

PIDL에 Logout 요청을 추가하여 유저가 로그아웃하면 SystemChat을 보내주는 기능을 여러분이 직접 추가해봅시다.

 

마무리

실제 채팅 프로그램을 보면 모든 유저가 이야기 하는 것이 아니라 자신이 속해있는 방에서만 채팅을 주고받습니다.

게임에서도 "외치기" 같은 특수한 기능이 아니라면 자신이 속해있는 지역에서만 채팅을 주고받습니다.

모든 유저에게 정보를 BroadCast하기에는 서버에 부담이 많이 갈 수 있기 때문에 지역을 한정하는 것이죠.

 

다음 포스트에서는 Room 기반의 채팅방을 만들어 자신이 속해있는 채팅방 안에서만 채팅을 주고받을 수 있도록 해보겠습니다.

 

다음 포스트: 채팅 서버 만들기(4) Dictionary 사용해보기 / Room으로 관리하기