I'm FanJae.

[C++ Server, C# Client] Day 6. Client Event 구현 II, Server 동기화 처리 본문

Toy Project/Multi Room Cheating Server

[C++ Server, C# Client] Day 6. Client Event 구현 II, Server 동기화 처리

FanJae 2024. 10. 1. 19:35

1. Client Event 구현

 

1-1. Chatting Room 레이아웃 설계

- 채팅창 레이아웃은 다음과 같이 설계하였다.

- 원래라면 누군가 입장했다는 정보나 ID 등도 추가해야겠지만, 이번 토이 프로젝트에서는 넣지 않았다.

- **님이 입장하였습니다.가 없다. (다음 토이 프로젝트에서는 좀 넣어서 깔끔하게 만들어보려 한다.)

 

1-2. 메시지 수신 처리

        private void Receive()
        {
            try
            {
                while (isRunning)
                {
                    byte[] buffer = new byte[4096];
                    int recv_length = 0;

                    try
                    {
                        recv_length = socket.Receive(buffer);
                    }
                    catch (SocketException ex)
                    {
                        if (ex.SocketErrorCode == SocketError.TimedOut)
                        {
                            continue;
                        }
                        else
                        {
                            throw;
                        }
                    }
                    if (recv_length > 0)
                    {
                        string response = Encoding.UTF8.GetString(buffer, 0, recv_length);


                        Invoke(new Action(() =>
                        {
                            chattingRoomTextBox.AppendText(response);
                            chattingRoomTextBox.AppendText("\r\n");

                        }));
                    }
                }
            }
            catch (SocketException ex)
            {
                if (isRunning)
                {
                    MessageBox.Show("서버와의 연결이 끊어졌습니다. " + ex.Message);
                }
            }
            catch (ObjectDisposedException)
            {

            }
            catch (InvalidOperationException)
            {

            }
        }

- 처음에 이 부분 처리에서 상당히 애먹었다.

C#의 경우 보통 Main Thread만이 UI에 대한 Control을 조작할 수 있게 함 (Thread Affinitiy)
- 응용 프로그램이 실행될 경우 기본적으로 하나의 Thread가 발생 : Main Thread
- Thread는 Main Form의 Event 처리 및 Main Form의 각종 Control들에 대한 읽기 / 쓰기 작업 수행
- Main Form에서 다른 Form을 띄울 경우에도 기본적으로 Main Thread가 자식 Form의 Control까지 모두 소유

이 때, Main Thread 외의 Thread에서 UI 상의 Control(Label, Textbox 등)에 접근할 경우 Cross Thread Error 발생

출처 :https://youngseong.tistory.com/365

 

- Invoke를 사용해서 다른 Thread에서 직접 접근할 수 없는 Control에 대한 작업을 Main Thread에 위임 가능하다고 한다.

- 앞으로 C# Multi Thread를 이용한 UI 갱신을 위해서라도 잘 알아둬야 할 것 같다.

 

1-3. 메시지 보내기 기능 구현

private void SendButton_Click(object sender, EventArgs e)
{
    if (IsSocketConnected(socket))
    {
        string request = sendTextBox.Text;
        byte[] buffer = Encoding.UTF8.GetBytes(request);
        socket.Send(buffer);

        sendTextBox.Text = "";
    }
}

private void sendTextBox_KeyDown(object sender, KeyEventArgs e)
{
    if (e.KeyCode == Keys.Enter)
    {
        e.SuppressKeyPress = true;
        this.SendButton_Click(sender, e);
    }
}

- 보내기 버튼을 누르면 일반 메시지를 보내기 처리한다.

- sendTextBox_KeyDown 버튼의 경우는 사실 크게 필요한 기능은 아니다. (엔터 누르면 채팅 보내지는 기능이다.)

 

private void ExitButton_Click(object sender, EventArgs e)
{
    this.Close();
}

private void ChattingRoom_FormClosing(object sender, FormClosingEventArgs e)
{
    if (IsSocketConnected(socket))
    {
        string request = "/Exit_Room";
        byte[] buffer = Encoding.UTF8.GetBytes(request);
        socket.Send(buffer);
    }

    isRunning = false;
    if (receiveThread != null && receiveThread.IsAlive)
    {
        receiveThread.Join();
    }
}

- Windows Form 닫기 기능이다. 기본적으로 /Exit_Room 이라는 명령을 보내서 해당 방에서 떠났음을 표시한다.

- 또한, 기존 이 폼에서 메시지를 받는 Thread를 종료하게 만든 이후 나가도록 처리한다.

 

1-4. 상수값 처리

public static class Constants
{
    public const string CLOSE_SOCKET = "/Close_Socket";
    public const string COMPLETE_CREATE_ROOM = "/Complete_Create_Room";
    public const string EXIST_ROOM = "/Exist_Room";
    public const string NOT_EXIST_ROOM = "/Not_Exist_Room";
    public const string NO_ROOM = "/No_Room";
    public const string EXIT_ROOM = "/Exit_Room";
    public const string GET_CHATTING_ROOM = "/Get_Chatting_Room";
    public const string CREATE_CHATTING_ROOM = "/Create_Chatting_Room ";
    public const string JOIN_CHATTING_ROOM = "/Join_Chatting_Room ";
}

- 메시지에 사용되는 값들은 이와 같이 처리하여 사용해준다.

- 앞서 서버에서 다뤘던 Client_Handle.h와 동일한 형태이다.


2. Server

2-1. 동기화 문제

- 사실 이 Toy project에서 동기화 처리를 해줄 만한 부분은 그렇게 많지 않다.

- 방 관련 해서만 처리해주면 된다. 각 쓰레드가 공유 자원으로 쓰는것은 오직 방 관련한 접근 뿐이다.

※ Lock을 걸때는 문제가 되는 부분에 걸어준다. (중요도에 따라서 Lock을 걸지 않는 부분도 있다.)

 

① 일반 메시지 전송 - Lock 미필요

std::cout << "[Log] : roomName : " << this->roomName << std::endl;
for (SOCKET target_socket : chatRooms[this->roomName]) 
{
	send(target_socket, message.c_str(), message.length(), 0);
}

 

- chatRooms 값이 변하는 순간은 방에서 누가 들어오거나 나가는 상황이다.

- 데이터 일관성이 깨질 수 있지만, 일반 메시지 전송에서 데이터 일관성이 깨지는건 Critical 한 문제는 아니다.

 

방 리스트 얻어오기 - Lock 미필요 

void ClientEventHandler::Handle_Get_Chatting_Room()
{
	std::string room_List = "";
	std::cout << "[Log] : " << chatRooms.size() << std::endl;
	if (chatRooms.empty())
	{
		room_List += NO_ROOM;
	}
	else
	{
		for (auto& room : chatRooms)
		{
			room_List += room.first + "\n";
		}
	}
	send(socket, room_List.c_str(), room_List.length(), 0);
}

- 방의 리스트를 불러오는 부분에서는 락을 걸지 않았다.

- 방 리스트를 불러오려는 순간 방이 만들어지거나 파괴(아무도 없어서 삭제) 되는 경우는 있겠지만 중요도가 낮다.

 

③ 방 나가기 - Lock 필요

void ClientEventHandler::Handle_Exit_Room()
{
	std::cout << "[Log] : " << "Handle_exit_Room" << std::endl;
	{
            const std::lock_guard<std::mutex> lock(chatRoom_mutex);
            if (chatRooms.find(roomName) != chatRooms.end())
            {
                send_message = EXIST_ROOM;
            }
            else
            {
                std::cout << "[Log] : Client Create Room : " << message << std::endl;
                chatRooms[message].insert(socket);
                send_message = roomName;
            }
	}
}

- 방을 입장하는 상황과 방을 삭제하는 것은 데이터 일관성이 중요하다.

- 방을 단순히 나가는 것뿐만 아니라, 삭제될 방에 입장한다는건 불가능하기 때문이다.

 

④ 방 입장 - Lock 필요

void ClientEventHandler::Handle_Join_Chatting_Room(const std::string& message)
{
	this->roomName = message;
	std::string send_message = "";
	{
		const std::lock_guard<std::mutex> lock(chatRoom_mutex);
		if (chatRooms.find(roomName) != chatRooms.end())
		{
			send_message = roomName;
			chatRooms[message].insert(socket);
		}
		else
		{
			send_message = NOT_EXIST_ROOM;
		}
	}
	send(socket, send_message.c_str(), send_message.length(), 0);
}

- 방을 입장할때는 Lock()을 처리해야 한다. 삭제될 방 처리와 겹쳐서 처리하면 안되기 때문이다.


3. TODO

1) Client

- 필요시 일부 코드 개선

 

2) Server

- 구조도 작성

Comments