[CPP] Socket Programming(2) : Non-Blocking Socket

2025. 12. 2. 12:23Game/Web and Game Server

※ Reference

- 개복치를 위한 CPP 프로그래밍, 본인 제작(2025/인프런)

- 게임 서버 프로그래밍 교과서, 배현직 지음(2019/길벗)

- Computer Networking : A Top-down Approach, 8th edition by Jim kurose/Keitrh Ross

 

※ 안사항

- 본 게시글을 읽으실때 Computer Network와 CPP에 대한 이해가 충분하지 않으시다면 많이 힘드실 수 있습니다.

- Computer Networking : A Top-down Approach의 경우 PPT가 공개되어 있어 해당 자료를 사용하나, 나머지 자료화면들은 아닌 경우가 있습니다. 물론 제가 만든거면 딱히 뭐라 안합니다만..

- ChatGPT/Gemeni/Claude의 도움을 받으시면 이해하기 더욱 쉬우나, Hallucination에 유의해 주세요. GPT의 경우 CODEX를 이용하시면 더욱 훌륭한 결과물을 얻을 수 있습니다.


어쨋든 Windows에서 사용하므로 Winsock을 사용한다. 다른데서 쓸꺼면 POSIX로 쓰면 되고, 내부 구현만 OS별로 나눠 놓으시라.

 

Non-Blocking Socket을 하기 전에, 일단 아주아주아주 간단한 클라이언트쪽 예제를 한번 만들어 봅시다. 그러기 위해서 위대하신 마이크로소프트 공식 매뉴얼을 찾아보니 - Winsocket을 초기화 -> 소켓생성 -> 바인딩 -> 수신 대기 -> 연결 수락 -> 데이터 이동 과 같은 절차를 밟으라고 명하고 있네요. Connect, Send, Recv와 같은 동작이 필요할 것으로 보이니 헤더에 좀 적어봅시다.

// Socket.h
#include <iostream>
#include <string>
#include <WinSock2.h>
#include <ws2tcpip.h> 
#include <stdlib.h>

#pragma comment(lib, "ws2_32")
#define _WIN32_WINNT 0x0601
#define DEFAULT_PORT "4444" // 프로그램에서 사용할 기초 포트
#define DEFAULT_BUFLEN 512

class ClientSocket {
public:
    struct addrinfo* result = nullptr, * ptr = nullptr, hints; // 주소 정보 등에 관한 구조체 생성 
    SOCKET ConnectSocket;

    ClientSocket(std::string) {};
    virtual void Connect();
    virtual void Send(char*);
    virtual void Recv(char*);
    virtual void end();
};

 

왠지 C언어 구문이 좀 많이 보이는것 같습니다만, CPP를 할줄 알면 Clang은 어느?정도 읽을 수 있으므로 넘어가 주도록 합시다. 이후로는 함수들을 아래와 같이 구현해 주면 됩니다. (동작 하는지는 모르겠는???데 그냥 저런식이구나 하고 이해해 주시면 감사하겠습니다.)

 

#include "Socket.h"

using namespace std;

ClientSocket::ClientSocket(string adress) {

    //addrinfo 개체 생성
    ZeroMemory(&hints, sizeof(hints)); // 메모리 밀기
    hints.ai_family = AF_UNSPEC; // IPv4던 IPv6던 딱히 지정은 안함
    hints.ai_socktype = SOCK_STREAM; // OOB Byte Stream(TCP)
    hints.ai_protocol = IPPROTO_TCP; // 프로토콜은 TCP

    int iresult = getaddrinfo(adress.c_str(), DEFAULT_PORT, &hints, &result); // 주소에서 정보 받아오기
    if (iresult != 0) {
        WSACleanup();
    }

    ConnectSocket = INVALID_SOCKET; // 일단은 비워두기
    ptr = result;
    ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol); // 소켓 생성

    if (ConnectSocket == INVALID_SOCKET) {
        freeaddrinfo(result);
        WSACleanup();
    }
};

void ClientSocket::Connect() {
    int iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen); // 소켓 연결
    if (iResult == SOCKET_ERROR) {
        closesocket(ConnectSocket);
        ConnectSocket = INVALID_SOCKET;
        freeaddrinfo(result);
    }

    if (ConnectSocket = INVALID_SOCKET) {
        WSACleanup();
        cout << "Unable to connect!" << endl;
    }
};

void ClientSocket::Send(char* sendbuf) {
    int iresult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
    if (iresult == SOCKET_ERROR) {
        closesocket(ConnectSocket);
        WSACleanup();
        cout << "Send failed" << endl;
    }
    else cout << "Sent!" << endl;
};

void ClientSocket::end() {
    int iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        closesocket(ConnectSocket);
        WSACleanup();
        cout << "Shoutdown failed" << endl;
    }
}

void ClientSocket::Recv(char* recvbuf) {
    int iResult;
    do {
        iResult = recv(ConnectSocket, recvbuf, 512, 0); // 받은 바이트의 정수값 반환
        if (iResult > 0) cout << "Bytes Received : " << iResult << endl;
        else if (iResult == 0) cout << "Connection Closed" << endl;
        else cout << "recv failed" << endl;
    } while (iResult > 0);
}

 

모든 라이브러리가 대게 그렇지만 - 뭔가 우리가 모르는 Struct들이 많아서 머리가 아플것 같습니다. 하지만 위대한 마이크로소프트는 친히 모든 설명을 메뉴얼에 적어 두었습니다! 그래서 간단하게 Connect/Send/End/Recv 동작을 만들어 보았습니다. 사실 bind()도 만들어서 포트를 따로 묶을까 고민했습니다만.. 뭐 어차피 예시용인데요.

 

쨋든 문제는 위와 같은 상황에서는 블로킹이 난다는 겁니다. 수신하는 쪽에서 송신 속도를 못따라가면 송신 버퍼도 언젠가는 가득 차서 블로킹이 날 수 있겠죠. 그래서 Non-Block Socket API가 있습니다!

 

간단하게 적어주면 됩니다.

논블록으로 소켓을 바꾸면, 리턴값이 좀 기기괴괴해집니다. 이 점은 메뉴얼을 참고해서 넘어가주면, 이제 블로킹이 일어날 상황이면 오류 코드를 띄우고 그냥 넘어가집니다!  그리고 한 스레드에서 소켓 여러개를 다룰 수 있게 됩니다. 루프를 돌면서 - 데이터가 있으면 받아오고, 없으면 그냥 즉시 리턴됩니다. 블로킹 소켓을 쓰는 상황이었으면, 한 소켓이 블로킹 당했으면 그걸 기다리느라 다른 얘들을 처리하지도 못했을 겁니다. 다만 스레드에서 루프를 계속 돌아주는 상황이  나와 CPU가 비명을 지를 수 있습니다. 이때는 polling이라는 훌륭한 수단이 있음을 인지하시고, OS 라이브러리를 찾아보면 함수가 분명 있을겁니다.

 

그리고 송신/수신 할때를 빼고는 논블로킹을 사용하면 좀 거시기 합니다. 이때는 0바이트를 송신해서 이놈이 죽어있나 살아있나 체크해주고 적절한 로직으로 처리해 주면 됩니다.

 

아, 그렇다고 논블로킹 소켓이 (특히 UDP에서) 만능은 아닙니다. 괜히 Async I/O가 있는게 아닙니다. 더불어 이것도 만능은 아니라서 나중에는 IOCP/epoll을 가져다 씁니다. 이에 대해서는 다음 시간에 얘기합시다...

'Game > Web and Game Server' 카테고리의 다른 글

[CPP] Socket Programming(1)  (0) 2025.12.01