1. 시스템 개요
본 문서는 C++로 구현된 다중 클라이언트 통신 시스템에 대한 기술 노트입니다. 이 시스템은 TCP 소켓을 기반으로 하는 클라이언트-서버 아키텍처를 채택하며, 다중 클라이언트의 동시 접속과 메시지 브로드캐스트 기능을 제공합니다. 멀티스레딩 기법을 활용하여 동시성을 처리합니다.
2. 시스템 아키텍처
2.1 파일 구조
├── Server.h
├── Server.cpp
├── Client.h
├── Client.cpp
├── main.cpp
└── Makefile
2.2 클래스 설계
- Server 클래스: 서버 소켓을 관리하고, 클라이언트 연결을 수락하며, 메시지 브로드캐스트를 담당합니다.
- Client 클래스: 서버에 연결하고, 메시지를 송수신하며, 연결 상태를 유지합니다.
3. 핵심 기능 구현
3.1 서버 구현
Server.h
#ifndef SERVER_H
#define SERVER_H
#include <string>
#include <thread>
#include <mutex>
#include <vector>
#include <map>
#include <atomic>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class Server {
public:
Server(int port);
~Server();
bool Start();
void Stop();
void BroadcastMessage(const std::string& message);
bool SendMessage(int clientSocket, const std::string& message);
private:
void HandleClient(int clientSocket);
void ReceiveMessages(int clientSocket);
void CleanupDisconnectedClients();
int port_;
int serverSocket_;
std::atomic<bool> running_;
std::thread acceptThread_;
std::mutex clientsMutex_;
std::map<int, std::thread> clientThreads_;
std::vector<int> clients_;
};
#endif // SERVER_H
Server.cpp
#include "Server.h"
#include <iostream>
#include <cstring>
Server::Server(int port) : port_(port), running_(false) {
serverSocket_ = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket_ < 0) {
std::cerr << "Error creating socket" << std::endl;
}
}
Server::~Server() {
Stop();
if (serverSocket_ >= 0) close(serverSocket_);
}
bool Server::Start() {
if (serverSocket_ < 0) return false;
int opt = 1;
setsockopt(serverSocket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port_);
if (bind(serverSocket_, (struct sockaddr*)&address, sizeof(address)) < 0) return false;
if (listen(serverSocket_, 5) < 0) return false;
running_ = true;
std::cout << "Server started on port " << port_ << std::endl;
acceptThread_ = std::thread([this]() {
while (running_) {
struct sockaddr_in clientAddr;
socklen_t clientLen = sizeof(clientAddr);
int clientSocket = accept(serverSocket_, (struct sockaddr*)&clientAddr, &clientLen);
if (clientSocket < 0) {
if (running_) std::cerr << "Accept failed" << std::endl;
continue;
}
std::cout << "New client from " << inet_ntoa(clientAddr.sin_addr) << std::endl;
{
std::lock_guard<std::mutex> lock(clientsMutex_);
clients_.push_back(clientSocket);
}
clientThreads_[clientSocket] = std::thread(&Server::HandleClient, this, clientSocket);
}
});
return true;
}
void Server::Stop() {
running_ = false;
{
std::lock_guard<std::mutex> lock(clientsMutex_);
for (int sock : clients_) close(sock);
clients_.clear();
}
for (auto& [sock, thread] : clientThreads_) {
if (thread.joinable()) thread.join();
}
clientThreads_.clear();
if (acceptThread_.joinable()) acceptThread_.join();
std::cout << "Server stopped" << std::endl;
}
void Server::HandleClient(int clientSocket) {
ReceiveMessages(clientSocket);
CleanupDisconnectedClients();
}
void Server::ReceiveMessages(int clientSocket) {
char buffer[1024];
while (running_) {
memset(buffer, 0, sizeof(buffer));
int bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesRead <= 0) break;
std::string message(buffer);
std::cout << "Message from client " << clientSocket << ": " << message << std::endl;
BroadcastMessage(message);
}
}
void Server::BroadcastMessage(const std::string& message) {
std::lock_guard<std::mutex> lock(clientsMutex_);
for (int clientSocket : clients_) SendMessage(clientSocket, message);
}
bool Server::SendMessage(int clientSocket, const std::string& message) {
return send(clientSocket, message.c_str(), message.length(), 0) >= 0;
}
void Server::CleanupDisconnectedClients() {
std::lock_guard<std::mutex> lock(clientsMutex_);
auto it = std::remove_if(clients_.begin(), clients_.end(), [](int sock) {
char buffer;
return recv(sock, &buffer, 1, MSG_PEEK) == 0;
});
clients_.erase(it, clients_.end());
}
3.2 클라이언트 구현
Client.h
#ifndef CLIENT_H
#define CLIENT_H
#include <string>
#include <thread>
#include <atomic>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
class Client {
public:
Client(const std::string& host, int port);
~Client();
bool Connect();
void Disconnect();
bool SendMessage(const std::string& message);
bool IsConnected() const;
private:
void ReceiveMessages();
std::string host_;
int port_;
int clientSocket_;
std::atomic<bool> running_;
std::thread receiveThread_;
};
#endif // CLIENT_H
Client.cpp
#include "Client.h"
#include <iostream>
#include <cstring>
Client::Client(const std::string& host, int port) : host_(host), port_(port), clientSocket_(-1), running_(false) {}
Client::~Client() { Disconnect(); }
bool Client::Connect() {
clientSocket_ = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket_ < 0) { std::cerr << "Socket creation failed" << std::endl; return false; }
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port_);
if (inet_pton(AF_INET, host_.c_str(), &serverAddr.sin_addr) <= 0) {
std::cerr << "Invalid address" << std::endl; close(clientSocket_); return false;
}
if (connect(clientSocket_, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cerr << "Connection failed" << std::endl; close(clientSocket_); return false;
}
running_ = true;
std::cout << "Connected to " << host_ << ":" << port_ << std::endl;
receiveThread_ = std::thread(&Client::ReceiveMessages, this);
return true;
}
void Client::Disconnect() {
running_ = false;
if (clientSocket_ >= 0) { close(clientSocket_); clientSocket_ = -1; }
if (receiveThread_.joinable()) receiveThread_.join();
std::cout << "Disconnected" << std::endl;
}
bool Client::SendMessage(const std::string& message) {
if (clientSocket_ < 0) { std::cerr << "Not connected" << std::endl; return false; }
return send(clientSocket_, message.c_str(), message.length(), 0) >= 0;
}
void Client::ReceiveMessages() {
char buffer[1024];
while (running_) {
memset(buffer, 0, sizeof(buffer));
int bytesRead = recv(clientSocket_, buffer, sizeof(buffer) - 1, 0);
if (bytesRead <= 0) break;
std::cout << "Server: " << buffer << std::endl;
}
}
bool Client::IsConnected() const { return clientSocket_ >= 0 && running_; }
3.3 main.cpp
#include "Server.h"
#include "Client.h"
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
void RunServer(int port) {
Server srv(port);
if (!srv.Start()) { std::cerr << "Server start failed" << std::endl; return; }
std::cout << "Server on port " << port << " (Enter to stop)" << std::endl;
std::cin.get();
srv.Stop();
}
void RunClient(const std::string& host, int port) {
Client cli(host, port);
if (!cli.Connect()) { std::cerr << "Connection failed" << std::endl; return; }
std::string msg;
while (cli.IsConnected()) {
std::cout << "Message (quit to exit): ";
std::getline(std::cin, msg);
if (msg == "quit") break;
cli.SendMessage(msg);
}
cli.Disconnect();
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " <server port | client host port>" << std::endl;
return 1;
}
std::string mode = argv[1];
if (mode == "server" && argc == 3) {
RunServer(std::stoi(argv[2]));
} else if (mode == "client" && argc == 4) {
RunClient(argv[2], std::stoi(argv[3]));
} else {
std::cout << "Invalid arguments" << std::endl;
}
return 0;
}
3.4 Makefile
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -pthread
TARGETS = server client
all: $(TARGETS)
server: server.cpp main.cpp
$(CXX) $(CXXFLAGS) -o $@ $^
client: client.cpp main.cpp
$(CXX) $(CXXFLAGS) -o $@ $^
clean:
rm -f $(TARGETS)
.PHONY: all clean
4. 주요 기술 포인트
- 멀티스레딩: 서버는 연결 수락과 각 클라이언트 메시지 처리를 별도 스레드로 처리합니다.
std::mutex로 공유 자원을 보호합니다. - TCP 소켓: 신뢰성 있는 데이터 전송을 위해 TCP를 사용합니다.
- 연결 관리:
CleanupDisconnectedClients를 통해 끊긴 연결을 정리합니다.
5. 컴파일 및 실행
make # 컴파일
./server 8080 # 서버 실행
./client 127.0.0.1 8080 # 클라이언트 실행
6. 확장 고려 사항
- 프로토콜: 사용자 정의 메시지 형식 추가 가능
- 보안: 암호화 및 인증 매커니즘 통합
- 성능: 스레드 풀 도입, 메시지 큐 버퍼링
- 안정성: 하트비트, 재연결 로직 구현