반응형
실시간 채팅 애플리케이션 기능 구현
이전 단계에서 사용자 목록 관리 및 연결/해제 알림 기능을 서버에 추가했습니다. 이번 단계에서는 실시간 채팅 기능을 구현하여, 사용자들이 메시지를 주고받을 수 있도록 하겠습니다.
기능 요구사항
- 메시지 전송: 사용자가 메시지를 입력하면 서버를 통해 다른 모든 사용자에게 전송됩니다.
- 메시지 수신: 서버로부터 메시지를 수신하고 이를 사용자에게 표시합니다.
- JSON 메시지 형식: 메시지는 JSON 형식으로 전송되며, 사용자 이름과 메시지를 포함합니다.
서버 코드 업데이트
ChatServer.h
#ifndef CHATSERVER_H
#define CHATSERVER_H
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <memory>
#include <unordered_set>
#include <unordered_map>
#include <string>
#include <nlohmann/json.hpp>
namespace net = boost::asio;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;
class ClientSession;
class ChatServer : public std::enable_shared_from_this<ChatServer> {
public:
ChatServer(net::io_context& ioc, tcp::endpoint endpoint)
: acceptor_(ioc), socket_(ioc) {
acceptor_.open(endpoint.protocol());
acceptor_.set_option(net::socket_base::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen();
do_accept();
}
void broadcast(const nlohmann::json& message);
void join(std::shared_ptr<ClientSession> session, const std::string& username);
void leave(std::shared_ptr<ClientSession> session);
void updateUserList();
private:
void do_accept();
tcp::acceptor acceptor_;
tcp::socket socket_;
std::unordered_set<std::shared_ptr<ClientSession>> sessions_;
std::unordered_map<std::string, std::shared_ptr<ClientSession>> user_sessions_;
};
#endif // CHATSERVER_H
ClientSession.h
#ifndef CLIENTSESSION_H
#define CLIENTSESSION_H
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <memory>
#include "ChatServer.h"
namespace net = boost::asio;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;
class ClientSession : public std::enable_shared_from_this<ClientSession> {
public:
ClientSession(tcp::socket socket, ChatServer& server)
: ws_(std::move(socket)), server_(server) {}
void start();
void send(const nlohmann::json& message);
private:
void do_read();
void do_write();
void handle_message(const std::string& message);
websocket::stream<tcp::socket> ws_;
ChatServer& server_;
boost::beast::flat_buffer buffer_;
std::vector<std::string> write_msgs_;
std::string username_;
};
#endif // CLIENTSESSION_H
ChatServer.cpp
#include "ChatServer.h"
#include "ClientSession.h"
void ChatServer::broadcast(const nlohmann::json& message) {
for (auto& session : sessions_) {
session->send(message);
}
}
void ChatServer::join(std::shared_ptr<ClientSession> session, const std::string& username) {
sessions_.insert(session);
user_sessions_[username] = session;
// 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
nlohmann::json notification;
notification["type"] = "join";
notification["username"] = username;
broadcast(notification);
updateUserList();
}
void ChatServer::leave(std::shared_ptr<ClientSession> session) {
sessions_.erase(session);
for (auto it = user_sessions_.begin(); it != user_sessions_.end(); ++it) {
if (it->second == session) {
// 알림 메시지를 생성하여 모든 사용자에게 브로드캐스트
nlohmann::json notification;
notification["type"] = "leave";
notification["username"] = it->first;
broadcast(notification);
user_sessions_.erase(it);
break;
}
}
updateUserList();
}
void ChatServer::updateUserList() {
nlohmann::json user_list_message;
user_list_message["type"] = "user_list";
for (const auto& user_session : user_sessions_) {
user_list_message["users"].push_back(user_session.first);
}
broadcast(user_list_message);
}
void ChatServer::do_accept() {
acceptor_.async_accept(socket_, [this](boost::system::error_code ec) {
if (!ec) {
auto session = std::make_shared<ClientSession>(std::move(socket_), *this);
session->start();
}
do_accept();
});
}
ClientSession.cpp
#include "ClientSession.h"
#include <nlohmann/json.hpp>
void ClientSession::start() {
do_read();
}
void ClientSession::send(const nlohmann::json& message) {
auto self(shared_from_this());
net::post(ws_.get_executor(), [this, self, message]() {
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(message.dump());
if (!write_in_progress) {
do_write();
}
});
}
void ClientSession::do_read() {
auto self(shared_from_this());
ws_.async_read(buffer_, [this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
auto message = boost::beast::buffers_to_string(buffer_.data());
handle_message(message);
buffer_.consume(length);
do_read();
} else {
server_.leave(shared_from_this());
}
});
}
void ClientSession::do_write() {
auto self(shared_from_this());
ws_.async_write(net::buffer(write_msgs_.front()), [this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
write_msgs_.erase(write_msgs_.begin());
if (!write_msgs_.empty()) {
do_write();
}
} else {
server_.leave(shared_from_this());
}
});
}
void ClientSession::handle_message(const std::string& message) {
auto json_message = nlohmann::json::parse(message);
if (json_message.contains("username")) {
username_ = json_message["username"].get<std::string>();
server_.join(shared_from_this(), username_);
}
if (json_message.contains("message")) {
nlohmann::json broadcast_message;
broadcast_message["type"] = "message";
broadcast_message["username"] = username_;
broadcast_message["message"] = json_message["message"];
server_.broadcast(broadcast_message);
}
}
클라이언트 코드 업데이트
ChatClient.h
#ifndef CHATCLIENT_H
#define CHATCLIENT_H
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include <memory>
#include <string>
#include <queue>
#include <iostream>
namespace net = boost::asio;
namespace websocket = boost::beast::websocket;
using tcp = net::ip::tcp;
class ChatClient : public std::enable_shared_from_this<ChatClient> {
public:
ChatClient(net::io_context& ioc, const tcp::resolver::results_type& endpoints)
: resolver_(ioc), ws_(ioc), endpoints_(endpoints) {}
void connect();
void disconnect();
void sendMessage(const std::string& message);
void setUsername(const std::string& username);
private:
void doRead();
void doWrite();
std::string formatMessage(const std::string& message);
tcp::resolver resolver_;
websocket::stream<tcp::socket> ws_;
tcp::resolver::results_type endpoints_;
boost::beast::flat_buffer buffer_;
std::queue<std::string> writeMessages_;
std::string username_;
};
#endif // CHATCLIENT_H
ChatClient.cpp
#include "ChatClient.h"
#include <nlohmann/json.hpp>
void ChatClient::connect() {
auto self(shared_from_this());
net::async_connect(ws_.next_layer(), endpoints_,
[this, self](boost::system::error_code ec, tcp::endpoint) {
if (!ec) {
ws_.async_handshake("localhost", "/",
[this, self](boost::system::error_code ec) {
if (!ec) {
doRead();
} else {
std::cerr << "Handshake failed: " << ec.message() << std::endl;
}
});
} else {
std::cerr << "Connect failed: " << ec.message() << std::endl;
}
});
}
void ChatClient::disconnect() {
auto self(shared_from_this());
ws_.async_close(websocket::close_code::normal,
[this, self](boost::system::error_code ec) {
if (ec) {
std::cerr << "Close failed: " << ec.message() << std::endl;
}
});
}
void ChatClient::sendMessage(const std::string& message) {
auto self(shared_from_this());
net::post(ws_.get_executor(), [
this, self, message]() {
bool writeInProgress = !writeMessages_.empty();
writeMessages_.push(formatMessage(message));
if (!writeInProgress) {
doWrite();
}
});
}
void ChatClient::setUsername(const std::string& username) {
username_ = username;
}
std::string ChatClient::formatMessage(const std::string& message) {
nlohmann::json jsonMessage;
jsonMessage["username"] = username_;
jsonMessage["message"] = message;
return jsonMessage.dump();
}
void ChatClient::doRead() {
auto self(shared_from_this());
ws_.async_read(buffer_,
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
std::cout << "Received: " << boost::beast::buffers_to_string(buffer_.data()) << std::endl;
buffer_.consume(length);
doRead();
} else {
std::cerr << "Read failed: " << ec.message() << std::endl;
disconnect();
}
});
}
void ChatClient::doWrite() {
auto self(shared_from_this());
ws_.async_write(net::buffer(writeMessages_.front()),
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
writeMessages_.pop();
if (!writeMessages_.empty()) {
doWrite();
}
} else {
std::cerr << "Write failed: " << ec.message() << std::endl;
disconnect();
}
});
}
main.cpp
#include <boost/asio.hpp>
#include "ChatClient.h"
int main() {
try {
net::io_context ioc;
tcp::resolver resolver(ioc);
auto endpoints = resolver.resolve("localhost", "12345");
auto client = std::make_shared<ChatClient>(ioc, endpoints);
std::cout << "Enter your username: ";
std::string username;
std::getline(std::cin, username);
client->setUsername(username);
client->connect();
std::thread t([&ioc]() { ioc.run(); });
std::string message;
while (std::getline(std::cin, message)) {
client->sendMessage(message);
}
client->disconnect();
t.join();
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
설명
위의 코드는 실시간 채팅 애플리케이션의 서버와 클라이언트의 기능을 완성한 예제입니다. 클라이언트는 사용자의 입력을 받아 서버로 메시지를 전송하고, 서버는 이를 다른 모든 클라이언트에게 중계합니다.
- ChatServer 클래스:
broadcast()
메서드는 모든 클라이언트에게 메시지를 전송합니다.join()
메서드는 클라이언트가 서버에 연결될 때 호출되며, 사용자 이름을 관리하고, 연결 알림을 브로드캐스트합니다.leave()
메서드는 클라이언트가 서버에서 연결을 종료할 때 호출되며, 연결 해제 알림을 브로드캐스트합니다.updateUserList()
메서드는 현재 연결된 사용자 목록을 업데이트하고, 모든 사용자에게 전송합니다.
- ClientSession 클래스:
start()
메서드는 세션을 시작합니다.send()
메서드는 서버에서 클라이언트로 메시지를 전송합니다.do_read()
메서드는 클라이언트로부터 메시지를 읽습니다.do_write()
메서드는 클라이언트에게 메시지를 씁니다.handle_message()
메서드는 수신된 메시지를 처리합니다. 사용자 이름과 메시지를 구분하여 처리합니다.
- ChatClient 클래스:
connect()
메서드는 서버에 연결을 설정합니다.disconnect()
메서드는 서버와의 연결을 종료합니다.sendMessage()
메서드는 사용자의 메시지를 서버로 전송합니다.setUsername()
메서드는 사용자의 이름을 설정합니다.formatMessage()
메서드는 메시지를 JSON 형식으로 변환합니다.doRead()
메서드는 서버로부터 메시지를 읽습니다.doWrite()
메서드는 서버로 메시지를 씁니다.
이제 스물일곱 번째 날의 학습을 마쳤습니다. 실시간 채팅 애플리케이션의 서버와 클라이언트를 완성하는 방법을 학습했습니다.
질문이나 피드백이 있으면 언제든지 댓글로 남겨 주세요. 내일은 "프로젝트: 보안 기능 추가"에 대해 학습하겠습니다.
반응형
'-----ETC----- > C++ 네트워크 프로그래밍 시리즈' 카테고리의 다른 글
[C++ 네트워크 프로그래밍] Day 29: 프로젝트: 최적화 및 테스트 (0) | 2024.08.01 |
---|---|
[C++ 네트워크 프로그래밍] Day 30: 프로젝트: 배포 및 유지보수 (0) | 2024.08.01 |
[C++ 네트워크 프로그래밍] Day 28: 프로젝트: 보안 기능 추가 (0) | 2024.08.01 |
[C++ 네트워크 프로그래밍] Day 25: 프로젝트: 서버 개발 (1) (0) | 2024.08.01 |
[C++ 네트워크 프로그래밍] Day 26: 프로젝트: 서버 개발 (2) (0) | 2024.08.01 |