Study/C++ & C#

[C++] Session 다중 접속

BVM 2024. 4. 5. 01:03

이전에 에코서버를 만들어 봤었는데, 그건 세션 당 하나의 소켓만 처리할 수 있었다.

이걸 하나의 세션에서 여러 개의 소켓을 처리할 수 있게 수정했다.

하물며 채팅에서도 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();
			});
	}
};

 

Sessionmake_shared로 바로 넘겨주는 것이 아니라 클래스 내에 shared_ptr로 갖고 있게 해, 시각적으로 클래스가 이러한 포인터를 갖고 있음을 확인할 수 있게 했다.

 

 

2. Session

Session 클래스에 많은 수정이 있었다.

 

  1. 소켓을 갖고 있을 집합
  2. 소켓 전체에 뿌리기 위한 Broadcast 함수
  3. 경쟁 예방을 위한 mutex
  4. 에러 처리
  5. 소켓 정리

등의 기능이 추가됐다.

 

아래는 클래스 헤더이다.

 

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를 활용한 네트워크 처리를 이해하기 위함인데...

계속해 나가면 가능할 것이리라...