본문으로 건너뛰기
Previous
Next
Windows API Winsock | 소켓 네트워크 프로그래밍 완벽 가이드

Windows API Winsock | 소켓 네트워크 프로그래밍 완벽 가이드

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를 다루겠습니다. 💬