이전에 에코서버를 만들어 봤었는데, 그건 세션 당 하나의 소켓만 처리할 수 있었다.
이걸 하나의 세션에서 여러 개의 소켓을 처리할 수 있게 수정했다.
하물며 채팅에서도 1:1 채팅만 하는 것은 아니지 않은가.
1. Server
Server
클래스 자체는 거의 원형을 유지하고 있다.
class Server {
public:
Server(boost::asio::io_context& io_context, short port)
: io_context_(io_context),
acceptor_(io_context, tcp::endpoint(tcp::v4(), port)),
session_(std::make_shared<Session>(io_context)) {
DoAccept();
}
private:
boost::asio::io_context& io_context_;
tcp::acceptor acceptor_;
std::shared_ptr<Session> session_;
void DoAccept() {
acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
session_->AddSocket(std::move(socket));
}
DoAccept();
});
}
};
Session
을 make_shared
로 바로 넘겨주는 것이 아니라 클래스 내에 shared_ptr
로 갖고 있게 해, 시각적으로 클래스가 이러한 포인터를 갖고 있음을 확인할 수 있게 했다.
2. Session
Session
클래스에 많은 수정이 있었다.
- 소켓을 갖고 있을 집합
- 소켓 전체에 뿌리기 위한
Broadcast
함수 - 경쟁 예방을 위한
mutex
- 에러 처리
- 소켓 정리
등의 기능이 추가됐다.
아래는 클래스 헤더이다.
class Session : public std::enable_shared_from_this<Session> {
public:
explicit Session(boost::asio::io_context& io_context)
: io_context_(io_context),
data_{ 0 } {
}
void AddSocket(tcp::socket socket) {
std::lock_guard<std::mutex> lock(sockets_mutex_);
sockets_.push_back(std::make_shared<tcp::socket>(std::move(socket)));
DoRead(sockets_.back());
}
int GetCount() {
return sockets_.size();
}
private:
boost::asio::io_context& io_context_;
std::vector<std::shared_ptr<tcp::socket>> sockets_;
std::mutex sockets_mutex_;
enum { kMaxLength = 1024 };
std::array<char, kMaxLength> data_;
void DoRead(std::shared_ptr<tcp::socket> socket);
void Broadcast(std::size_t length, std::shared_ptr<tcp::socket> sender);
void DoWrite(std::shared_ptr<tcp::socket> socket, std::size_t length);
void HandleError(const boost::system::error_code& ec,
std::shared_ptr<tcp::socket> socket);
void CloseSocket(std::shared_ptr<tcp::socket> socket);
};
DoRead/Write
에 추가된 건 새로 추가된 함수들로 예외처리를 한 것뿐이니 넘어가고...
새 함수들을 보자.
1. Broadcast()
void Broadcast(std::size_t length, std::shared_ptr<tcp::socket> sender) {
std::lock_guard<std::mutex> lock(sockets_mutex_);
for (auto& socket_ptr : sockets_) {
if (socket_ptr != sender) {
DoWrite(socket_ptr, length);
}
}
}
어떤 소켓으로부터 받은 데이터를 세션에 있는 전체 소켓에 뿌린다.
현재의 서버 코드는 여러 스레드에 의한 경합 상태가 벌어질 일이 전혀 없지만 mutex
로 락을 걸어줬다.
미래엔 어떻게 코드를 바꿔나갈지 알 수 없고, 결국 다중 스레드를 활용하는 방향으로 갈 것이기 때문에 습관을 들이는 겸 해서 추가했다.
안일하게 있다가 문제가 발생하는 것 보단 낫지 않겠는가.
2. HandleError()
void HandleError(const boost::system::error_code& ec,
std::shared_ptr<tcp::socket> socket) {
if (ec == boost::asio::error::eof) {
std::cout << "Successfully Disconnected\n";
}
else {
std::cerr << "Disconnected with Error: " << ec.message() << "\n";
}
CloseSocket(socket);
}
클라이언트가 연결을 끊거나 비동기 읽기/쓰기 과정에서 문제가 있을 때 호출하도록 했다.
eof
이외의 에러들에 대해서 이유를 출력하고 문제가 생긴 소켓을 닫는다.
3. CloseSocket()
void CloseSocket(std::shared_ptr<tcp::socket> socket) {
std::lock_guard<std::mutex> lock(sockets_mutex_);
auto it = std::find_if(sockets_.begin(), sockets_.end(),
[&socket](const std::shared_ptr<tcp::socket>& s) {
return s == socket;
});
if (it != sockets_.end()) {
socket->close();
sockets_.erase(it);
}
}
컨테이너를 순회하며 해당 소켓을 찾고 닫는다.
일단 이렇게 하나씩 추가하며 채팅 서버를 완성해 보려고 한다.
Sapphire에 사용된 boost.asio를 활용한 네트워크 처리를 이해하기 위함인데...
계속해 나가면 가능할 것이리라...
'Study > C++ & C#' 카테고리의 다른 글
[C#] ###Clicker 동작 개선 (0) | 2024.05.05 |
---|---|
[C++] ChatRoom 구현 (0) | 2024.04.09 |
[C++] Boost.Asio 에코 서버 (0) | 2024.03.29 |
[C#] ###Clicker 개선판 (0) | 2024.03.18 |
[C#] 심플한 게임 런처 (0) | 2024.03.06 |