지금까지 서버와 클라이언트를 나누어 클라이언트의 메시지를 서버를 통해 다시 클라이언트에게 전달할 수 있도록 만들었다. 하지만 지금까지 만든 건 엄밀히 말하면 채팅이 아닌 1:1 대화다. 서버가 accept()를 딱 한 번만 부르고 끝나기 때문이다.
애초에 이 시리즈를 시작한 목적이 브로드캐스팅이었으니, 오늘은 여러 명을 받고, 한 명의 말을 나머지 전원에게 전달하는 진짜 채팅 서버를 만들어보자.
서버 코드
클라이언트는 이전 글의 코드를 그대로 쓰면 되고, 서버만 고친다.
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <algorithm>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
std::vector<SOCKET> g_clients; // 접속 중인 클라이언트 목록
std::mutex g_mutex; // 목록을 지키는 자물쇠
// 보낸 사람을 제외한 전원에게 전달
void Broadcast(const std::string& message, SOCKET sender) {
std::vector<SOCKET> targets;
{
std::lock_guard<std::mutex> lock(g_mutex);
targets = g_clients; // 복사본만 뜨고 자물쇠는 즉시 반납
}
for (SOCKET target : targets) {
if (target == sender) continue;
send(target, message.c_str(), static_cast<int>(message.size()), 0);
}
}
// 클라이언트 한 명을 전담하는 스레드
void HandleClient(SOCKET clientSocket) {
char buffer[512];
while (true) {
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesReceived <= 0) {
break; // 0이면 정상 종료, 음수면 에러
}
buffer[bytesReceived] = '\0';
std::cout << "[" << clientSocket << "] " << buffer << std::endl;
Broadcast(std::string(buffer, bytesReceived), clientSocket);
}
// 접속이 끊겼으니 목록에서 지운다
{
std::lock_guard<std::mutex> lock(g_mutex);
g_clients.erase(
std::remove(g_clients.begin(), g_clients.end(), clientSocket),
g_clients.end());
}
closesocket(clientSocket);
std::cout << "Client " << clientSocket << " disconnected." << std::endl;
}
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cout << "WSAStartup failed" << std::endl;
return 1;
}
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET) {
std::cout << "socket failed: " << WSAGetLastError() << std::endl;
WSACleanup();
return 1;
}
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(9000);
if (bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
std::cout << "bind failed: " << WSAGetLastError() << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR) {
std::cout << "listen failed: " << WSAGetLastError() << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
std::cout << "Server is listening on port 9000..." << std::endl;
// 손님을 계속 받는다
while (true) {
SOCKADDR_IN clientAddr;
int clientAddrSize = sizeof(clientAddr);
SOCKET clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &clientAddrSize);
if (clientSocket == INVALID_SOCKET) {
std::cout << "accept failed: " << WSAGetLastError() << std::endl;
continue;
}
{
std::lock_guard<std::mutex> lock(g_mutex);
g_clients.push_back(clientSocket);
}
std::cout << "Client " << clientSocket << " connected!" << std::endl;
// 이 손님 전담 직원을 배치하고, 나는 다시 문 앞으로
std::thread(HandleClient, clientSocket).detach();
}
closesocket(listenSocket);
WSACleanup();
return 0;
}
socket → bind → listen까지는 이전 글과 똑같으나, accept 이후부터 main 함수가 달라진 것을 볼 수 있다.
먼저 손님을 계속 받기 위해 accept()를 while 루프로 감싸야 한다. 다만, accept() 이후 바로 recv()를 해버리면 블로킹 시간 동안 문앞에 아무도 없게 된다 (accept()를 받지 못한다). 그 말은 즉슨 두 번째 클라이언트는 들어오지 못한다는 것이다.
해결책은 대기 지점마다 전담 스레드를 붙이는 것이다. 메인 스레드는 accept()만 받고, 클라이언트가 접근할 때마다 전담 스레드를 하나씩 붙여 대화를 맡긴다. detach()는 이 스레드를 나중에 join()으로 기다리지 않겠다는 선언이다.
지금 접속 중인 클라이언트 목록 g_clients에 들어온 클라이언트를 push_back할 때, 내부 배열이 재할당될 때 다른 스레드가 그 배경을 순회하면 흔히 말하는 경쟁 상태(race condition)가 된다. 그래서 g_clients에 손을 대는 사람은 반드시 lock을 걸고 들어간다. lock_guard는 스코프를 벗어날 때나 중간에 예외가 터질 때 자동으로 unlock 되기 때문에 직접 lock()/unlock()을 부르는 것보다 안전하다.
서버를 먼저 켜고, 클라이언트를 두세 개 실행한다. Visual Studio에서 클라이언트 프로젝트를 여러 번 실행하려면, 빌드된 .exe를 탐색기에서 직접 여러 번 더블클릭하는 게 제일 편하다. A 창에서 메시지를 치면 B, C 창에 그 메시지가 뜬다. 드디어 채팅이다!

'프로그래밍 언어 > C++' 카테고리의 다른 글
| [소켓 통신] 멀티스레드 구현 (0) | 2026.07.14 |
|---|---|
| [소켓 통신] 메시지 주고받기 (0) | 2026.07.13 |
| [소켓 통신] 서버에서 클라이언트 접속 받기 (0) | 2026.07.10 |
| [소켓 통신] Winsock 라이브러리 연결 (0) | 2026.07.09 |
| 불안정 정렬과 안정 정렬 (Stable Sort) (1) | 2024.08.30 |
