[C++] ChatRoom 구현

2024. 4. 9. 20:58·Study/C++ & C#

본격적으로 채팅 시스템을 만들어 보기 위한 기초 작업이라고 봐도 되겠다.

이전과의 차이점은 아래와 같다.

 

  1. Session 정의 변경
     - 이제 세션은 서버와 클라이언트 간의 연결 상태를 나타낸다.
     - 기존 Room처럼 쓸 수 없고 단일 연결을 나타낸다.
  2. ChatServer 구현
     - io_context를 가지고 전체 서버를 관리할 객체
     - 이후에 io_context를 활용하는 객체들은 전부 이 객체로부터 레퍼런스를 받는다.
  3. ChatRoom 구현
     - ChatServer 아래에서 실제 채팅방의 역할을 할 객체
  4. 핸들러 구현
     - 메세지 핸들링을 분리해 코드 관리를 용이하게 할 목적으로 별도 클래스로 구현

 

1. Session

#ifndef SESSION_H_
#define SESSION_H_

#include <boost/asio.hpp>
#include <memory>
#include <deque>
#include <string>

#include "MessageHandler.h"

class ChatServer;
class ChatRoom;

class Session : public std::enable_shared_from_this<Session> {
public:
	Session(asio::io_context& io_context, ip::tcp::socket& socket, std::shared_ptr<ChatServer> server);
	void Init();

	void Start();
	void Deliver(const std::string& message);
	void Join(short& roomNumber);
	void Leave();
	void Disconnect();
	std::shared_ptr<ChatRoom> GetCurrentRoom() const { return current_room_; }

private:
	void DoRead();
	void DoWrite();

	asio::io_context& io_context_;
	ip::tcp::socket socket_;
	asio::io_context::strand strand_;
	std::shared_ptr<ChatServer> server_;
	std::shared_ptr<ChatRoom> current_room_;
	std::unique_ptr<MessageHandler> message_handler_;

	enum { kMaxLength = 1024 };
	char data_[kMaxLength];
	std::deque<std::string> write_messages_;
};

#endif // SESSION_H_

 

 

public 함수들은 Room 연결에 관한 것들이 대부분이다.

이외의 함수들은 다음의 기능을 가진다.

 

  1. Start()
     - Accept 시 새로운 Session을 생성하며 호출하게 된다.
     - Init()과 DoRead()를 호출하며 사이클을 시작한다.
  2. Init()
     - MessageHandler 멤버의 초기화를 위한 함수.
     - 생성자 내에서 shared_from_this() 사용을 할 수 없기 때문.
  3. Deliver()
     - 세션에 메세지를 쓰기 위한 함수.
     - 구현은 아래와 같다.
void Session::Deliver(const std::string& message) {
	asio::post(strand_, [this, message]() {
		bool write_in_progress = !write_messages_.empty();
		write_messages_.emplace_back(message);
		if (!write_in_progress) {
			DoWrite();
		}
		});
}

 

bind 한 객체를 wrap으로 strand_에 넘겨주는 것이 아닌 post로 바로 보내고 있다.

이건 이후 개선하며 바뀔 여지가 있긴 하나, 스레드 경합이 일어나는 곳은 아니므로 당장의 문제는 없을 것이다.

 

 

2. ChatServer

#ifndef CHATSERVER_H_
#define CHATSERVER_H_

#include <boost/asio.hpp>
#include <unordered_map>
#include <memory>
#include "ChatRoom.h"

class ChatServer : public std::enable_shared_from_this<ChatServer> {
public:
	ChatServer();
	virtual ~ChatServer();

	void StartAccept(const short port);
	void AcceptConnection(std::shared_ptr<ip::tcp::acceptor> acceptor);
	void RemoveSession(const std::shared_ptr<Session>& session);
	void Start(const short port);
	void Stop();

	asio::io_context& GetIoContext();

	void AddChatRoom(const short& roomNumber, const std::string roomName);
	void RemoveChatRoom(const short& roomNumber);
	std::shared_ptr<ChatRoom> GetChatRoom(const short& roomNumber);
	void PrintChatRoomStatus();

private:
	asio::io_context io_context_;
	asio::io_context::strand strand_;
	std::unique_ptr<asio::io_context::work> work_;
	std::unordered_map<short, std::shared_ptr<ChatRoom>> chat_rooms_;
	std::unordered_set<std::shared_ptr<Session>> sessions_;
};

#endif // CHATSERVER_H_

 

Sapphire 프로젝트의 Hive 클래스를 따라 했다.

따라한 것 치고 많이 다르긴 한데...

 

여하튼 이 클래스에서 io_context를 선언하고 전체 서버에서 이를 활용하게 된다.

스레드 경합이 일어날 만한 곳들은 이를 받아 생성된 각각의 strand를 통해 처리할 수 있으니,

굳이 io_context를 늘려 관리를 어렵게 할 필요가 없다.

 

chat_rooms_는 map이고, sessions_는 set으로 갖고 있게 했다.

각 룸의 고유 번호를 키로 하고 그 값으로 룸의 포인터를 넘겨주는 게 효율적이라고 생각했다.

룸의 포인터 안에서 고유의 번호를 찾는 것보단 Key를 통해 찾는 게 더 빠르다고 판단했다.

세션은 단순히 수명관리를 위해 포인터만이 필요했으므로 set으로 갖고 있게 했다.

 

 

3. ChatRoom

#ifndef CHATROOM_H_
#define CHATROOM_H_

#include <unordered_set>
#include <memory>
#include <string>

class Session;

class ChatRoom {
public:
	ChatRoom(const short& num, const std::string& name);

	void Join(std::shared_ptr<Session> session);
	void Leave(std::shared_ptr<Session> session);
	void Broadcast(const std::string& message, std::shared_ptr<Session> sender);

	size_t GetParticipantCount() const;
	std::string GetRoomName();

private:
	short roomNumber_;
	std::string name_;
	std::unordered_set<std::shared_ptr<Session>> sessions_;
};

#endif // CHATROOM_H_

 

실제로 채팅방으로서의 기능을 수행할 클래스.

들어오고(Join), 나가고(Leave), 뿌리고(Broadcast)의 기능만 수행한다.

심플하게 구성됐다.

 

void ChatRoom::Broadcast(const std::string& message, std::shared_ptr<Session> sender) {
	for (const auto& session : sessions_) {
		if (session != sender) {
			session->Deliver(message);
		}
	}
}

 

Broadcast()는 갖고 있는 세션을 순회하며 메세지를 각 세션의 Deliver()에 전달해 주는 식으로 구현했다.

 

 

4. MessageHandler

#ifndef MESSAGEHANDLER_H_
#define MESSAGEHANDLER_H_

#include <string>
#include <memory>

class Session;

class MessageHandler {
public:
	MessageHandler(std::shared_ptr<Session> session);

	void HandleMessage(const std::string& message);

private:
	std::shared_ptr<Session> session_;
};

#endif // MESSAGEHANDLER_H_

 

받은 메세지를 처리하는 로직이 DoRead() 안에 있으면 관리가 번거로워지고 단일 함수의 책임이 커진다.

따라서 별도 클래스로 분리했다.

 

아래는 HandleMessage()의 구현부이다.

void MessageHandler::HandleMessage(const std::string& message) {
	if (message.substr(0, 5) == "/join") {
		try {
			short roomNumber = std::stoi(message.substr(6));
			session_->Join(roomNumber);
		}
		catch (const std::invalid_argument& e) {
			std::cerr << "Invalid room number: " << e.what() << "\n";
			session_->Deliver("Invalid room number");
		}
		catch (const std::out_of_range& e) {
			std::cerr << "Room number out of range: " << e.what() << "\n";
			session_->Deliver("Invalid room number");
		}
	}
	else if (message == "/leave") {
		session_->Leave();
	}
	else {
		auto room = session_->GetCurrentRoom();
		if (room) {
			room->Broadcast(message, session_);
		}
	}
}

 

예외 처리 부분은 임시로 둔 것에 가깝기 때문에 추후에 수정이 필요하다.

대충 저러한 것들을 처리할 것이라고 사실상 의사코드를 넣어둔 것에 준한다.

 

 

5. 실행 결과

 

채팅 룸 구분도 되고 있고, 한글 송/송수신도 문제없다.

남은 건 각종 예외처리 보강과 구색은 갖춘 채팅 클라이언트를 만드는 것.

'Study > C++ & C#' 카테고리의 다른 글

[C++] 채팅서버에 DB 실장  (0) 2024.05.14
[C#] ###Clicker 동작 개선  (0) 2024.05.05
[C++] Session 다중 접속  (0) 2024.04.05
[C++] Boost.Asio 에코 서버  (0) 2024.03.29
[C#] ###Clicker 개선판  (0) 2024.03.18
'Study/C++ & C#' 카테고리의 다른 글
  • [C++] 채팅서버에 DB 실장
  • [C#] ###Clicker 동작 개선
  • [C++] Session 다중 접속
  • [C++] Boost.Asio 에코 서버
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)
  • 블로그 메뉴

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

  • 공지사항

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

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
[C++] ChatRoom 구현
상단으로

티스토리툴바