Deep Studying

[프라우드넷] 채팅 서버 만들기(4) Dictionary 사용해보기 / Room으로 관리하기 본문

게임서버

[프라우드넷] 채팅 서버 만들기(4) Dictionary 사용해보기 / Room으로 관리하기

miniSeop 2022. 1. 13. 23:08

이전 포스트에서는 유저 클래스를 등록하고 마샬링하여 주고 받는 방법을 추가해봤습니다.

 하지만 채팅은 모든 유저들과 이야기를 주고 받기 보다는 들어가있는 방이나 자신의 캐릭터가 존재하는 맵에서 같은 공간에 있는 사람들끼리 주고받는 경우가 더 많습니다. 이러한 기능을 추가해보도록 하겠습니다.

 

+ 제대로된 Room 기능을 구현하는 것 보다는 Dictionary 자료구조를 설명하는 데에 더 포커스가 맞춰졌습니다.

 

 

RoomNumber추가

유저 클래스에 RoomNumber라는 멤버 변수를 추가해줍니다.

RoomNumber가 0이면 아무 곳에도 속해있지 않고, 다른 숫자이면 해당 방에 들어가 있는 것이라고 생각하겠습니다.

Common > Common.cs

public class User
{
	static int UserId = 0;
	public HostID HostId { get; set; }
	public string UserName { get; set; }
	public int UserID { get; set; }
	public int RoomNumber { get; set; }

	public User(string UserName, HostID HostId)
	{
		UserID = ++UserId;
		this.UserName = UserName;
		this.HostId = HostId;
		RoomNumber = 0;
	}
	public User()
	{
		UserName = "Unknown";
		RoomNumber = 0;
	}
}

 

PIDL 수정

C2S.PIDL

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

Enter Room과 LeaveRoom 함수를 추가하였습니다.

S2C.PIDL에는 따로 추가할 내용이 없어 수정하지 않았습니다.

 

클라이언트 메인 루프 변경

Client > program.cs

...

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 if(me.RoomNumber == 0)
		{
			try
			{
				int RoomNumber = Int32.Parse(userInput);
				C2SProxy.EnterRoom(HostID.HostID_Server, RmiContext.ReliableSend, RoomNumber);
				me.RoomNumber = RoomNumber;
			}
			catch(FormatException ex)
			{

			}
		}
		else
		{
			Console.Write("Chat to {0}\n", me.RoomNumber);
			C2SProxy.Chat(HostID.HostID_Server, RmiContext.ReliableSend, userInput);
		}
	}
	else {
		Console.WriteLine("Login...");
		C2SProxy.Login(HostID.HostID_Server, RmiContext.ReliableSend, userInput);
	}
}

 

 자신의 RoomNumber가 0이면 방에 접속하지 않은 것으로, 다른 숫자면 해당 방에 들어가 있는 것으로 생각합니다.

 

클라이언트 부분에는 신경을 안쓰다보니 약간 지저분한 감이 없잖아 있는 느낌입니다.

 콘솔환경에서 클라이언트를 사용하는 것에 한계가 있다보니 조만간 MFC나 Unity등 GUI로 클라이언트 예제를 만들어볼 생각이라 우선은 이정도로 넘어가도록 하겠습니다.

 

 

 

이전에 했던 것 처럼 빈 Stub함수를 만들고 등록해주는 것으로 시작하겠습니다.

Server > process/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;
            C2SStub.EnterRoom = EnterRoom;
            C2SStub.LeaveRoom = LeaveRoom;

            ServerLauncher.NetServer.AttachProxy(S2CProxy);
            ServerLauncher.NetServer.AttachStub(C2SStub);
        }
        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;
        }
        static public bool Login(HostID remote, RmiContext rmiContext, string UserName)
        {
            string message = string.Format("{0} entered.", UserName);
            User user = new User(UserName, remote);

            ServerLauncher.UserList.TryAdd(remote, user);
            
            S2CProxy.ResponseLogin(user.HostId, rmiContext, user);
            S2CProxy.SystemChat(ServerLauncher.NetServer.GetClientHostIDs(), RmiContext.ReliableSend, message);
            
            Console.WriteLine(message);
            return true;
        }
        
        // * 추가 * //
        static public bool EnterRoom(HostID remote, RmiContext rmiContext, int RoomNumber)
        {
            return true;
        }
        static public bool LeaveRoom(HostID remote, RmiContext rmiContext)
        {
            return true;
        }
        
        public void SystemChat(string str)
        {
            S2CProxy.SystemChat(ServerLauncher.NetServer.GetClientHostIDs(), RmiContext.ReliableSend, str);
        }
    }
}

 

 

Dictionary 기능 활용하기

이번 포스트의 핵심 내용은 특정 방에 속해있는 유저들의 데이터를 가져오는 것입니다.

아래 함수를 통해 그 기능을 추가해줍니다.

 

Server > process/Process.cs

internal class CommonProcess
{

	...

	static public HostID[] GetHostIDsInRoom(int RoomNumber)
	{
		HostID[] users = ServerLauncher.UserList
			.Where(p => p.Value.RoomNumber == RoomNumber)
			.Select(p => p.Value.HostId)
			.ToArray( );

		return users;
	}
    
}

 

ServerLauncher.UserList( 설명 글에서는 UserList라고 하겠습니다 )는 ServerLauncher.cs에서 만든 Dictionary입니다.

앞으로 응용할 곳이 많아 보여 내용을 정리하고 가는 것이 좋아보이네요.

 

데이터 추가하기

 

void Add ( TKey key, TValue value )

ServerLauncher.UserList.Add( HostID, user );

Add 함수를 사용하면 해당 Key를 가진 Element가 이미 있었던 없었던 상관없이 Value를 넣습니다.

위에 직접 추가하는 것과 다른 점은 Value에는 null이 들어갈 수 없다는 점입니다.

 

bool TryAdd ( TKey key, TValue value )

ServerLauncher.UserList.Add( HostID, user );

TryAdd 함수를 사용하면 해당 Key를 가진 Element가 없을 경우에만 Value를 Dictionary에 추가합니다.

추가하는데 성공한다면 true를 리턴하고, 이미 Element가 있어서 실패한다면 false를 추가합니다.

 

데이터 삭제하기

bool Remove ( TKey )

ServerLauncher.UserList.Remove( HostID );

Remove 함수를 사용하면 해당 Key를 가진 Element를 삭제합니다.

데이터가 있었는데 성공적으로 삭제가 되었다면 true를 리턴하고 원래 데이터가 없었다면 false를 리턴합니다.

 

단일 아이템 가져오기

직접 가져오기

User user = ServerLauncher.UserList[ HostID ];

map과 같이 Key-Value 구조로 이루어져있어 Key를 통해 바로 Value를 가져오는 것도 가능합니다.

ServerLauncher.UserList[ HostID ]이 Null이 될 수 있다는 점을 알고 작성해야합니다.

 

bool TryGetValue( TKey key, out TValue value )

User user = ServerLauncher.UserList.TryGetValue( HostID, out User user );

TryGetValue를 사용하면 해당 값이 있을때 값을 가져오고 user 변수에 저장합니다.

값을 가져오는 데 성공했다면 true를, 실패했다면 false를 리턴하는 Boolean 타입의 메서드입니다.

이전 포스트부터 설명 없이 사용했습니다만 이러한 기능을 가지고 있습니다.

 

리스트로 가져오기

IEnumerable<T> Select( )

ServerLauncher.UserList.Select( p => p.Value );

 

우선 아직 IEnumerable 인터페이스에 대해 완전히 이해하지 못하여 아래 링크를 첨부합니다.

 

https://docs.microsoft.com/en-us/dotnet/api/system.collections.ienumerable?view=net-6.0 

 

IEnumerable Interface (System.Collections)

Exposes an enumerator, which supports a simple iteration over a non-generic collection.

docs.microsoft.com

결과와 추측 ( IEnumerable에 대해 완벽히 숙지하면 부가 설명을 추가하겠습니다. )

 

결과만 설명드리자면 Select를 실행하면 기존 Dictionary에서 새로운 자료구조를 만들어냅니다.

인자로 람다식을 넣는 이유는 다음과 같습니다.

 기존 Dictionary는 자료 구조의 단위가 User 가 아닌 KeyValuePair<Key, Value> 입니다.

Select에 람다식을 인자로 넣으면 우리가 원하는 값을 추출하여 IEnumerable 자료구조를 생성합니다. 위 코드에서는 KeyValuePair인 p 에서 Value값만 추출하여 자료구조를 생성합니다.

 

 

일부만 가져오기

IEnumerable<T> Where( )

ServerLauncher.UserList.Where( p => p.Value.UserName == "user123" );

 

결과와 추측 ( IEnumerable에 대해 완벽히 숙지하면 부가 설명을 추가하겠습니다. )

 

Where도 마찬가지로 IEnumerable을 리턴합니다.

인자로 람다식을 넣어 람다식의 리턴 값이 True인 값만 추출하여 IEnumerable 자료구조에 추가합니다. 위 코드에서는 Value의 UserName이 "user123" 인 값만 추출하여 자료구조를 생성합니다. 이 때, T값은 똑같이 KeyValuePair입니다. 

 

리스트로 변환하기

T ToArray( )

ServerLauncher.UserList.Select( p => p.Value ).ToArray( );
ServerLauncher.UserList.Where( p => p.Value.UserName == "user123" ).ToArray( );

// Where 먼저 실행
ServerLauncher.UserList
        .Where( p => p.Value.UserName == "user123" )
        .Select( p => p.Value )
        .ToArray( );
// Select 먼저 실행
ServerLauncher.UserList
        .Select( p => p.Value )
        .Where( p => p.UserName == "user123" )
        .ToArray( );

 

ToArray 함수를 실행하면 IEnumerable에서 Array로 값을 변환해줍니다. 이 때, Sorting이 되어있지는 않습니다.

Select 뒤에 써도 되고 Where 뒤에 써도 되고, 둘을 섞어 사용해도 문제 없습니다. 단 Select를 실행했을 때, 자료형이 변경된다는 점은 유념해주세요. 위 코드를 보고 람다식이 어떻게 달라졌는지 확인해주세요

 

 

Stub 메서드 로직 수정

Chat 메서드

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

위에 선언한 GetHostIDsInRoom( ) 함수를 통해 같은 RoomNumber를 가진 유저의 HostID만 찾아냅니다.

NotifyChat의 인자를 해당 값으로 변경하여 같은 RoomNumber를 가진 유저에게만 채팅 메세지를 보냅니다.

 

Login 메서드

static public bool Login(HostID remote, RmiContext rmiContext, string UserName)
        {
            string message = string.Format("{0} entered.", UserName);
            User user = new User(UserName, remote);

            // 유저 등록
            ServerLauncher.UserList.TryAdd(remote, user);
            
            S2CProxy.ResponseLogin(user.HostId, rmiContext, user);
            
            Console.WriteLine(message);
            return true;
        }

 

 방에 들어왔을 때, 방 안에 있는 유저들에게만 시스템 메세지를 보내는 것이 좋아보입니다.

따라서 로그인 했을 때 시스템 메세지를 보내는 부분을 삭제했습니다.

 

EnterRoom 메서드

static public bool EnterRoom(HostID remote, RmiContext rmiContext, int RoomNumber)
        {
            ServerLauncher.UserList.TryGetValue(remote, out User user);
            user.RoomNumber = RoomNumber;
            ServerLauncher.UserList.Add(remote, user);

            string message = string.Format("{0} entered to Room {1}", user.UserName, user.RoomNumber);

            S2CProxy.SystemChat(GetHostIDsInRoom(RoomNumber), RmiContext.ReliableSend, message);
            Console.WriteLine(message);
            return true;
        }

유저의 RoomNumber를 파라미터 값으로 변경해줍니다.

 

LeaveRoom 메서드

static public bool LeaveRoom(HostID remote, RmiContext rmiContext)
        {
            ServerLauncher.UserList.TryGetValue(remote, out User user);
            user.RoomNumber = 0;
            ServerLauncher.UserList.TryAdd(remote, user);
            return true;
        }

유저의 RoomNumber를 0으로 변경해줍니다.

 

 

마무리

일반적인 Room 구조를 가진 어플리케이션이라면 방에 접속할 때,

 

  1. EnterRoom을 실행했을 때, 클라이언트는 서버의 Response를 기다린다.

  2. 서버에서 Room에 들어가는 요청이 올바른지 확인한다. ( 방 생성, 방 인원제한 등 체크 )

  3. 방에 접속할 수 있으면 요청을 처리하고 방 정보를 Response에 넣어 클라이언트에 보내준다.

  4. 클라이언트는 Block을 풀고 해당 방 정보를 바탕으로 UI에 반영한다. ( 혹은 실패 처리를 한다. )

 

 위와 같은 수순으로 진행되어야 올바른 방 접속 과정이라고 볼 수 있겠습니다. 또한 방에 있는 유저를 관리하고 기능을 수행해줄 클래스도 필요할 것 같아 보입니다.

 하지만 Dictionary 구조를 설명하는 것에 중점을 둔 포스트라서 위와같은 과정을 모두 구현하는 것은 글이 너무 길어질 것 같아 삭제하였습니다. 별 기능을 기획해두지도 않아서 막상 구현해두어도 와닿지 않을 것 같기도 하고요

 

 그래서 채팅 서버에 대한 내용은 여기서 마치고 본격적인 Room 구조를 설명하기 위해서 간단한 원카드 게임 서버를 구현해보려고 합니다. 준비가되면 이어서 포스트하도록 하겠습니다.

 

감사합니다.

 

 

다음 포스트: 원카드 서버 만들기(1) 프로젝트 시작하기