Windows API Winsock | 소켓 네트워크 프로그래밍 완벽 가이드
이 글의 핵심
Winsock은 Windows 소켓 API입니다. WSAStartup으로 초기화하고, socket으로 소켓을 생성합니다. 서버는 bind-listen-accept, 클라이언트는 connect로 연결합니다. send/recv로 데이터를 주고받고, closesocket으로 닫습니다.
들어가며
Winsock(Windows Sockets)은 1990년대 초반 Windows에서 네트워크 프로그래밍을 위해 도입된 API입니다. Berkeley Socket을 기반으로 Windows에 맞게 확장되었습니다. TCP/IP, UDP 통신을 구현할 수 있으며, 웹 서버, 채팅 프로그램, 게임 서버 등에 사용됩니다.
// 가장 간단한 TCP 클라이언트
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
inet_pton(AF_INET, "93.184.216.34", &addr.sin_addr); // example.com
connect(sock, (sockaddr*)&addr, sizeof(addr));
send(sock, "GET / HTTP/1.1\r\n\r\n", 18, 0);
char buf[1024];
recv(sock, buf, sizeof(buf), 0);
closesocket(sock);
WSACleanup();
1. Winsock 초기화
1.1 WSAStartup과 WSACleanup
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
// Winsock 초기화
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup 실패: %d\n", result);
return 1;
}
printf("Winsock 버전: %d.%d\n",
LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion));
// 소켓 작업...
// Winsock 정리
WSACleanup();
return 0;
}
2. TCP 서버
2.1 기본 TCP 서버
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 1. 소켓 생성
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSock == INVALID_SOCKET) {
printf("socket 실패: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 2. 바인드 (주소 할당)
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000); // 포트 9000
addr.sin_addr.s_addr = INADDR_ANY; // 모든 IP
if (bind(listenSock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
printf("bind 실패: %d\n", WSAGetLastError());
closesocket(listenSock);
WSACleanup();
return 1;
}
// 3. 리슨 (연결 대기)
if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR) {
printf("listen 실패: %d\n", WSAGetLastError());
closesocket(listenSock);
WSACleanup();
return 1;
}
printf("서버 시작: 포트 9000\n");
// 4. 클라이언트 연결 수락
while (true) {
sockaddr_in clientAddr = {};
int clientAddrSize = sizeof(clientAddr);
SOCKET clientSock = accept(listenSock, (sockaddr*)&clientAddr, &clientAddrSize);
if (clientSock == INVALID_SOCKET) {
printf("accept 실패: %d\n", WSAGetLastError());
continue;
}
char clientIP[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP));
printf("클라이언트 연결: %s:%d\n", clientIP, ntohs(clientAddr.sin_port));
// 5. 데이터 수신
char buf[1024];
int bytesReceived = recv(clientSock, buf, sizeof(buf) - 1, 0);
if (bytesReceived > 0) {
buf[bytesReceived] = '\0';
printf("받은 데이터: %s\n", buf);
// 6. 응답 전송
const char* response = "Hello from server!";
send(clientSock, response, (int)strlen(response), 0);
}
// 7. 소켓 닫기
closesocket(clientSock);
}
closesocket(listenSock);
WSACleanup();
return 0;
}
2.2 멀티스레드 서버
DWORD WINAPI ClientThread(LPVOID lpParam)
{
SOCKET clientSock = (SOCKET)lpParam;
char buf[1024];
while (true) {
int bytesReceived = recv(clientSock, buf, sizeof(buf) - 1, 0);
if (bytesReceived <= 0) {
printf("클라이언트 연결 종료\n");
break;
}
buf[bytesReceived] = '\0';
printf("받음: %s\n", buf);
// 에코백
send(clientSock, buf, bytesReceived, 0);
}
closesocket(clientSock);
return 0;
}
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(listenSock, (sockaddr*)&addr, sizeof(addr));
listen(listenSock, SOMAXCONN);
printf("멀티스레드 서버 시작\n");
while (true) {
SOCKET clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
printf("새 클라이언트 연결\n");
// 스레드 생성
CreateThread(NULL, 0, ClientThread, (LPVOID)clientSock, 0, NULL);
}
}
closesocket(listenSock);
WSACleanup();
return 0;
}
3. TCP 클라이언트
3.1 기본 TCP 클라이언트
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 소켓 생성
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
printf("socket 실패: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 서버 주소 설정
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
// 연결
if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
printf("connect 실패: %d\n", WSAGetLastError());
closesocket(sock);
WSACleanup();
return 1;
}
printf("서버에 연결됨\n");
// 데이터 전송
const char* message = "Hello, Server!";
send(sock, message, (int)strlen(message), 0);
printf("전송: %s\n", message);
// 응답 수신
char buf[1024];
int bytesReceived = recv(sock, buf, sizeof(buf) - 1, 0);
if (bytesReceived > 0) {
buf[bytesReceived] = '\0';
printf("받음: %s\n", buf);
}
closesocket(sock);
WSACleanup();
return 0;
}
4. UDP 소켓
4.1 UDP 서버
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// UDP 소켓 생성
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (sockaddr*)&addr, sizeof(addr));
printf("UDP 서버 시작: 포트 9000\n");
while (true) {
char buf[1024];
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
// 데이터 수신
int bytesReceived = recvfrom(sock, buf, sizeof(buf) - 1, 0,
(sockaddr*)&clientAddr, &clientAddrSize);
if (bytesReceived > 0) {
buf[bytesReceived] = '\0';
char clientIP[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP));
printf("받음 [%s:%d]: %s\n", clientIP, ntohs(clientAddr.sin_port), buf);
// 응답 전송
const char* response = "UDP Response";
sendto(sock, response, (int)strlen(response), 0,
(sockaddr*)&clientAddr, clientAddrSize);
}
}
closesocket(sock);
WSACleanup();
return 0;
}
4.2 UDP 클라이언트
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in serverAddr = {};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(9000);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
// 데이터 전송
const char* message = "Hello UDP!";
sendto(sock, message, (int)strlen(message), 0,
(sockaddr*)&serverAddr, sizeof(serverAddr));
printf("전송: %s\n", message);
// 응답 수신
char buf[1024];
int serverAddrSize = sizeof(serverAddr);
int bytesReceived = recvfrom(sock, buf, sizeof(buf) - 1, 0,
(sockaddr*)&serverAddr, &serverAddrSize);
if (bytesReceived > 0) {
buf[bytesReceived] = '\0';
printf("받음: %s\n", buf);
}
closesocket(sock);
WSACleanup();
return 0;
}
5. 논블로킹 소켓과 select
5.1 논블로킹 소켓
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 논블로킹 모드 설정
u_long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
// connect는 즉시 반환 (WSAEWOULDBLOCK)
int result = connect(sock, (sockaddr*)&addr, sizeof(addr));
if (result == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) {
printf("연결 진행 중...\n");
// select로 연결 완료 대기
fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(sock, &writeSet);
timeval timeout = {5, 0}; // 5초
if (select(0, NULL, &writeSet, NULL, &timeout) > 0) {
printf("연결 완료!\n");
} else {
printf("연결 타임아웃\n");
}
}
}
5.2 select로 여러 소켓 모니터링
#include <vector>
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(listenSock, (sockaddr*)&addr, sizeof(addr));
listen(listenSock, SOMAXCONN);
// 논블로킹 설정
u_long mode = 1;
ioctlsocket(listenSock, FIONBIO, &mode);
std::vector<SOCKET> clients;
printf("select 서버 시작\n");
while (true) {
fd_set readSet;
FD_ZERO(&readSet);
// listenSock 추가
FD_SET(listenSock, &readSet);
// 모든 클라이언트 소켓 추가
for (SOCKET client : clients) {
FD_SET(client, &readSet);
}
timeval timeout = {1, 0}; // 1초
int count = select(0, &readSet, NULL, NULL, &timeout);
if (count > 0) {
// 새 연결 확인
if (FD_ISSET(listenSock, &readSet)) {
SOCKET clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
clients.push_back(clientSock);
printf("새 클라이언트 연결 (총 %zu개)\n", clients.size());
}
}
// 클라이언트 데이터 확인
for (auto it = clients.begin(); it != clients.end();) {
SOCKET client = *it;
if (FD_ISSET(client, &readSet)) {
char buf[1024];
int bytesReceived = recv(client, buf, sizeof(buf) - 1, 0);
if (bytesReceived <= 0) {
printf("클라이언트 연결 종료\n");
closesocket(client);
it = clients.erase(it);
} else {
buf[bytesReceived] = '\0';
printf("받음: %s\n", buf);
// 에코백
send(client, buf, bytesReceived, 0);
++it;
}
} else {
++it;
}
}
}
}
closesocket(listenSock);
WSACleanup();
return 0;
}
6. 비동기 소켓 (WSAAsyncSelect)
6.1 Windows 메시지 기반 비동기
#define WM_SOCKET (WM_USER + 1)
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
static SOCKET listenSock = INVALID_SOCKET;
static std::vector<SOCKET> clients;
switch (msg) {
case WM_CREATE: {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(listenSock, (sockaddr*)&addr, sizeof(addr));
listen(listenSock, SOMAXCONN);
// 비동기 이벤트 등록
WSAAsyncSelect(listenSock, hwnd, WM_SOCKET, FD_ACCEPT);
MessageBox(hwnd, L"서버 시작", L"알림", MB_OK);
return 0;
}
case WM_SOCKET: {
SOCKET sock = (SOCKET)wParam;
int event = WSAGETSELECTEVENT(lParam);
int error = WSAGETSELECTERROR(lParam);
if (error != 0) {
MessageBox(hwnd, L"소켓 에러!", L"오류", MB_ICONERROR);
return 0;
}
if (sock == listenSock && event == FD_ACCEPT) {
// 새 연결
SOCKET clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
clients.push_back(clientSock);
// 클라이언트 소켓도 비동기 등록
WSAAsyncSelect(clientSock, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
wchar_t buf[128];
swprintf_s(buf, L"클라이언트 연결 (총 %zu개)", clients.size());
SetWindowText(hwnd, buf);
}
} else if (event == FD_READ) {
// 데이터 수신
char buf[1024];
int bytesReceived = recv(sock, buf, sizeof(buf) - 1, 0);
if (bytesReceived > 0) {
buf[bytesReceived] = '\0';
MessageBoxA(hwnd, buf, "받은 메시지", MB_OK);
// 에코백
send(sock, buf, bytesReceived, 0);
}
} else if (event == FD_CLOSE) {
// 연결 종료
closesocket(sock);
auto it = std::find(clients.begin(), clients.end(), sock);
if (it != clients.end()) {
clients.erase(it);
}
wchar_t buf[128];
swprintf_s(buf, L"클라이언트 종료 (남은: %zu개)", clients.size());
SetWindowText(hwnd, buf);
}
return 0;
}
case WM_DESTROY:
for (SOCKET client : clients) {
closesocket(client);
}
if (listenSock != INVALID_SOCKET) {
closesocket(listenSock);
}
WSACleanup();
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
7. 실전 예제: 간단한 채팅 서버
#include <winsock2.h>
#include <ws2tcpip.h>
#include <vector>
#include <string>
#include <algorithm>
#pragma comment(lib, "ws2_32.lib")
struct Client {
SOCKET sock;
std::string name;
};
std::vector<Client> g_clients;
void BroadcastMessage(const char* message, SOCKET exceptSock = INVALID_SOCKET)
{
for (const auto& client : g_clients) {
if (client.sock != exceptSock) {
send(client.sock, message, (int)strlen(message), 0);
}
}
}
DWORD WINAPI ClientThread(LPVOID lpParam)
{
Client* pClient = (Client*)lpParam;
// 입장 메시지
char welcomeMsg[256];
sprintf_s(welcomeMsg, "[%s] 님이 입장했습니다.\n", pClient->name.c_str());
BroadcastMessage(welcomeMsg);
printf("%s", welcomeMsg);
char buf[1024];
while (true) {
int bytesReceived = recv(pClient->sock, buf, sizeof(buf) - 1, 0);
if (bytesReceived <= 0) {
break;
}
buf[bytesReceived] = '\0';
// 메시지 브로드캐스트
char chatMsg[1280];
sprintf_s(chatMsg, "[%s] %s", pClient->name.c_str(), buf);
BroadcastMessage(chatMsg, pClient->sock);
printf("%s", chatMsg);
}
// 퇴장 메시지
char leaveMsg[256];
sprintf_s(leaveMsg, "[%s] 님이 퇴장했습니다.\n", pClient->name.c_str());
BroadcastMessage(leaveMsg);
printf("%s", leaveMsg);
// 클라이언트 제거
auto it = std::find_if(g_clients.begin(), g_clients.end(),
[pClient](const Client& c) { return c.sock == pClient->sock; });
if (it != g_clients.end()) {
g_clients.erase(it);
}
closesocket(pClient->sock);
delete pClient;
return 0;
}
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(listenSock, (sockaddr*)&addr, sizeof(addr));
listen(listenSock, SOMAXCONN);
printf("채팅 서버 시작: 포트 9000\n");
while (true) {
SOCKET clientSock = accept(listenSock, NULL, NULL);
if (clientSock != INVALID_SOCKET) {
// 이름 받기
char nameBuf[64];
int bytesReceived = recv(clientSock, nameBuf, sizeof(nameBuf) - 1, 0);
nameBuf[bytesReceived] = '\0';
Client* pClient = new Client;
pClient->sock = clientSock;
pClient->name = nameBuf;
g_clients.push_back(*pClient);
CreateThread(NULL, 0, ClientThread, pClient, 0, NULL);
}
}
closesocket(listenSock);
WSACleanup();
return 0;
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Windows API 멀티스레딩 | CreateThread·동기화 완벽 가이드
- Windows API 파일 I/O·레지스트리 | 파일 처리 완벽 가이드
- C++ 멀티스레딩 완벽 가이드 | thread·mutex·condition_variable
이 글이 도움이 되셨나요? Windows Winsock 네트워크 프로그래밍을 마스터하는 데 도움이 되었기를 바랍니다!
다음 글에서는 MFC 다이얼로그와 DDX/DDV를 다루겠습니다. 💬