오늘의 소켓 통신 단계를 학습하기 이전에 이전 글에서 구현한 채팅 프로그램의 클라이언트 코드 일부를 보자.
while (true)
{
// 1. 키보드로 메시지 입력받기
std::cout << "메시지 입력 (quit 입력 시 종료): ";
std::cin.getline(sendBuffer, sizeof(sendBuffer));
// 'quit'을 입력하면 반복 종료
if (strcmp(sendBuffer, "quit") == 0)
break;
// 2. 서버로 보내기 (말하기)
send(clientSocket, sendBuffer, (int)strlen(sendBuffer), 0);
// 3. 서버가 돌려준 메아리 받기 (듣기)
int bytesReceived = recv(clientSocket, recvBuffer, sizeof(recvBuffer) - 1, 0);
if (bytesReceived <= 0)
{
std::cout << "Server disconnected." << std::endl;
break;
}
recvBuffer[bytesReceived] = '\0';
std::cout << "서버 응답: " << recvBuffer << std::endl;
}
클라이언트의 루프는 다음 순서와 같다.
getline(입력 기다림) →send→recv(응답 기다림) → 반복
여기서 문제는 getline. 키보드 입력을 받기 전까지 프로그램은 이 줄에 멈춰있는 것이다. 그동안 서버가 어떤 텍스트를 보내더라도 클라이언트는 recv 하지 못하는 것. 한 사람이 '입력하기'와 '받기'를 번갈아 하니 한쪽을 하는 동안 다른 쪽이 막히는 것이다.
그렇다면 해결 방법은 무엇일까? 그것은 바로 여러 사람이 일을 하도록 일꾼을 늘리는 것이다. 스레드란 한 프로그램 안에서 동시에 도는 실행 흐름이다. 메인 일꾼은 입력과 전송만 담당하고, 새 일꾼은 수신만 담당하면 된다. 즉, 메인 스레드는 getline과 send만 담당하고, 새 스레드는 recv만 담당하면 된다는 것이다.

클라이언트 코드
이번 단계에서는 서버는 그대로 두고, 클라이언트 쪽 코드만 수정하면 된다.
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <string>
#include <thread> // 스레드를 쓰기 위한 헤더
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
// 수신 전담 함수 — 이 함수가 별도 스레드에서 돌아감
void ReceiveThread(SOCKET sock)
{
char recvBuffer[512];
while (true)
{
int bytesReceived = recv(sock, recvBuffer, sizeof(recvBuffer) - 1, 0);
if (bytesReceived <= 0)
{
std::cout << "\n서버와 연결이 끊어졌습니다." << std::endl;
break;
}
recvBuffer[bytesReceived] = '\0';
std::cout << "\n[서버] " << recvBuffer << std::endl;
}
}
int main()
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
std::cout << "WSAStartup failed" << std::endl;
return 1;
}
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == 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 = inet_addr("127.0.0.1");
serverAddr.sin_port = htons(9000);
if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
std::cout << "connect failed: " << WSAGetLastError() << std::endl;
closesocket(clientSocket);
WSACleanup();
return 1;
}
std::cout << "Client: connected to server!" << std::endl;
// 수신 전담 스레드 시작 (여기서부터 두 갈래로 나뉨)
std::thread recvThread(ReceiveThread, clientSocket);
// 메인 스레드는 입력·전송만 담당
while (true)
{
std::string sendMsg;
std::getline(std::cin, sendMsg);
if (sendMsg == "quit")
break;
send(clientSocket, sendMsg.c_str(), static_cast<int>(sendMsg.size()), 0);
}
// 정리
closesocket(clientSocket);
// 수신 스레드가 끝날 때까지 기다렸다 정리
recvThread.join();
WSACleanup();
return 0;
}
위에 따로 뺀 ReceiveThread 함수가 바로 수신 전담 스레드가 할 일을 작성한 것이다. std::thread로 스레드를 만드는 라인이 실행되는 순간, 코드의 흐름이 두 갈래로 갈라진다. 메인은 아래 while로 내려가 입력을 받고, 새로 생긴 recvThread는 ReceiveThread 안에서 따로 돌기 시작한다. 둘이 동시에 살아 있다고 보면 되는 것이다.
recvThread.join()은 이 스레드가 끝날 때까지 기다린다는 뜻이다. quit으로 메인 루프를 빠져나온 뒤, 수신 일꾼이 마저 정리될 때까지 기다렸다가 프로그램을 끝낸다.
기존 작업과 같아 보이지만, 입력과 출력이 서로 다른 일꾼에서 처리된다는 게 핵심 변화이다. 데이터가 들어올 때까지 블로킹을 하는 recv()나 getline() 함수는 버퍼에 쌓인 것을 내 변수로 꺼내오는 함수다. 블로킹은 프로그램 전체가 아니라 스레드 하나만 멈추게 하므로 recv()에서 멈춰있어도 getline()은 따로 돌아가는 것이다.

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