C++ 기반 다중 클라이언트 통신 시스템 구현

1. 시스템 개요

본 문서는 C++로 구현된 다중 클라이언트 통신 시스템에 대한 기술 노트입니다. 이 시스템은 TCP 소켓을 기반으로 하는 클라이언트-서버 아키텍처를 채택하며, 다중 클라이언트의 동시 접속과 메시지 브로드캐스트 기능을 제공합니다. 멀티스레딩 기법을 활용하여 동시성을 처리합니다.

2. 시스템 아키텍처

2.1 파일 구조


├── Server.h
├── Server.cpp
├── Client.h
├── Client.cpp
├── main.cpp
└── Makefile

2.2 클래스 설계

  1. Server 클래스: 서버 소켓을 관리하고, 클라이언트 연결을 수락하며, 메시지 브로드캐스트를 담당합니다.
  2. 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. 확장 고려 사항

  • 프로토콜: 사용자 정의 메시지 형식 추가 가능
  • 보안: 암호화 및 인증 매커니즘 통합
  • 성능: 스레드 풀 도입, 메시지 큐 버퍼링
  • 안정성: 하트비트, 재연결 로직 구현

태그: C++ socket 멀티스레드 tcp 네트워크프로그래밍

6월 2일 19:45에 게시됨