본격적으로 채팅 시스템을 만들어 보기 위한 기초 작업이라고 봐도 되겠다.
이전과의 차이점은 아래와 같다.
- Session 정의 변경
- 이제 세션은 서버와 클라이언트 간의 연결 상태를 나타낸다.
- 기존 Room처럼 쓸 수 없고 단일 연결을 나타낸다. - ChatServer 구현
-io_context
를 가지고 전체 서버를 관리할 객체
- 이후에io_context
를 활용하는 객체들은 전부 이 객체로부터 레퍼런스를 받는다. - ChatRoom 구현
- ChatServer 아래에서 실제 채팅방의 역할을 할 객체 - 핸들러 구현
- 메세지 핸들링을 분리해 코드 관리를 용이하게 할 목적으로 별도 클래스로 구현
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
연결에 관한 것들이 대부분이다.
이외의 함수들은 다음의 기능을 가진다.
- Start()
-Accept
시 새로운Session
을 생성하며 호출하게 된다.
-Init()
과DoRead()
를 호출하며 사이클을 시작한다. - Init()
-MessageHandler
멤버의 초기화를 위한 함수.
- 생성자 내에서shared_from_this()
사용을 할 수 없기 때문. - 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 |