251029

lililllilillll·2025년 10월 29일

개발 일지

목록 보기
339/350

✅ 한 것들


  • 윤성우의 열혈 TCP/IP 소켓 프로그래밍


📖 윤성우의 열혈 TCP/IP 소켓 프로그래밍


Chapter 22 Overlapped IO 모델

22-1 Overlapped IO 모델의 이해

Overlapped IO : 비동기로 여러 IO 동시 진행

  • WSASocket() : 마지막 인자에 WSA_FLAG_OVERLAPPED를 전달하면 비동기 API 사용 가능한 소켓 된다. (Non-blocking은 아님.)
  • WSASend() : Overlapped IO에 사용되는 데이터 출력 함수
    • __WSABUF : 두 번째 인자 구조체. 전송 데이터 버퍼와 크기 정보 저장
    • __WSAOVERLAPPED : 여섯 번째 인자 구조체
      • Overlapped IO 진행하려면 NULL 아니라 유효한 구조체 넣어야 한다.
      • WSASend()로 동시에 둘 이상 영역에 데이터 전송 시 구조체 변수를 별도로 구성해야 한다
  • WSARecv() : 데이터 수신

WSASend()의 인자 lpNumberOfBytesSent에는 전송 데이터 크기 저장됨

  • Q. 호출되자마자 반환하는데, 어떻게 전송 데이터 크기 반환?
  • A. 출력 버퍼가 비어있고, 전송 데이터 크기 작다면, 함수 호출과 동시에 전송 완료 가능
    • 이땐 WSASend()가 0을 반환하고, 데이터 크기 정보도 저장
  • 반환 후에도 전송 이뤄진다면, WSASend()는 SOCKET_ERROR 반환.
    • WSAGetLastError() 호출해서 WSA_IO_PENDING 확인 가능.
    • 이땐 WSAGetOverllapedResult()로 전송 데이터 크기 확인 (이 함수는 수신 결과 확인도 가능)

22-2 Overlapped IO에서의 입출력 완료의 확인

Overlapped IO 입출력 완료 및 결과 확인 방법

  • WSASend(), WSARecv() 6번째 인자 활용. Event 오브젝트 기반
  • WSASend(), WSARecv() 7번째 인자 활용. Completion Routine 기반

Event 오브젝트 사용하기

    if(connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr))==SOCKET_ERROR)
        ErrorHandling("connect() error!");

    evObj = WSACreateEvent();
    memset(&overlapped, 0, sizeof(overlapped));
    overlapped.hEvent = evObj; // Event 오브젝트 등록
    dataBuf.len = strlen(msg) + 1;
    dataBuf.buf = msg;

    // 바로 반환되는데, 에러 뜨면 아직 남아있는 건지 확인하고 Event로 기다리기
    if(WSASend(hSocket, &dataBuf, 1, &sendBytes, 0, &overlapped, NULL) == SOCKET_ERROR)
    {
        if(WSAGetLastError() == WSA_IO_PENDING)
        {
            puts("Background data send");
            WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
            WSAGetOverlappedResult(hSocket, &overlapped, &sendBytes, FALSE, NULL);
        }
        else
        {
            ErrorHandling("WSASend() error");
        }
    }

Overlapped Send 예제

    // 역시 바로 반환되는데, ERROR 뜨면 PENDING인지 확인. PENDING이면 이벤트 기다리기.
    if(WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, NULL) == SOCKET_ERROR)
    {
        if(WSAGetLastError()==WSA_IO_PENDING)
        {
            puts("Background data receive");
            WSAWaitForMultipleEvents(1, &evObj, TRUE, WSA_INFINITE, FALSE);
            WSAGetOverlappedResult(hRecvSock, &overlapped, &recvBytes, FALSE, NULL);
        }
        else
        {
            ErrorHandling("WSARecv() error");
        }
    }

Overlapped Recv 예제

Completion Routine 사용하기

Completion Routine 등록

  • WSASend(), WSARecv() 마지막 인자 통해 함수 등록
  • Pending된 IO가 완료되면, 이 함수를 호출해라
  • 중요한 작업 중 CR 호출되면 안되니까, IO 요청 쓰레드가 alertable wait일 때만 CR 호출

alertable wait 상태 만드는 함수

  • WaitForSingleObjectEx()
  • WaitForMultipleObjectsEx()
  • WSAWaitForMultipleEvents()
  • SleepEx()`
  • 전부 마지막 인자에 TRUE 전달하면 alertable wait 상태 된다
  • ex 붙은 건 원래 함수랑 똑같은데 마지막 인자 추가
    memset(&overlapped, 0, sizeof(overlapped));
    dataBuf.len = BUF_SIZE;
    dataBuf.buf = buf;
    evObj = WSACreateEvent(); // Dummy event object

    // 마지막 인자에 콜백 함수 등록
    if(WSARecv(hRecvSock, &dataBuf, 1, &recvBytes, &flags, &overlapped, CompRoutine)==SOCKET_ERROR)
    {
        if(WSAGetLastError()==WSA_IO_PENDING)
            puts("Background data receive");
    }

    // 등록한 콜백 함수 실행되도록 alert wait 상태로 넘어가기
    idx = WSAWaitForMultipleEvents(1, &evObj, FALSE, WSA_INFINITE, TRUE);
    if(idx==WAIT_IO_COMPLETION) // IO가 정상 완료 했다면
        puts("Overlapped I/O Completed");
    else
        ErrorHandling("WSARecv() error");

    WSACloseEvent(evObj);
    closesocket(hRecvSock);
    closesocket(hLisnSock);
    WSACleanup();
    return 0;
}

// CALLBACK 의미 : 콜백 함수는 운영체제가 호출하므로, 호출 규약을 알려주는 것
void CALLBACK CompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    if(dwError!=0)
    {
        ErrorHandling("CompRoutine error");
    }
    else
    {
        recvBytes = szRecvBytes;
        printf("Received message: %s \n", buf);
    }
}
  • CR을 이용한다 하더라도, WSAOVERLAPPED 구조체 주소값은 반드시 전달해줘야 한다.
    • hEvent 초기화 위해 Event 오브젝트 생성할 필요는 없다,.
  • 책과는 다르게, recvBytes랑 flags는 DWORD로 선언해야 컴파일 해줬다. 더 깐깐해진거인듯.

Chapter 23 IOCP (Input Output Completion Port)

23-1 Overlapped IO를 기반으로 IOCP 이해하기

ioctlsocket(hLisnSock, FIONBIO, &mode)

  • 넌-블로킹 모드로 소켓 속성 변경
  • 소켓 입출력모드(FIONBIO)를 mode에 저장된 값으로 변경
  • mode가 0이면 블로킹, 0 아니면 넌-블로킹

넌-블로킹 모드 소켓

  • 넌-블로킹 모드로 입출력
  • 클라 요청 없을 때 accept() 호출되면 INVALID_SOCKET 바로 반환
    • 이어서 WSAGetLastError() 호출하면 WSAEWOULDBLOCK 반환
  • accept()로 새로 생성되는 소켓 역시 넌-블로킹

Overlapped IO만으로 에코 서버 구현

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUF_SIZE 1024
void CALLBACK ReadCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void CALLBACK WriteCompRoutine(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
void ErrorHandling(char *message);

typedef struct
{
    SOCKET hClntSock;
    char buf[BUF_SIZE];
    WSABUF wsaBuf;
} PER_IO_DATA, *LPPER_IO_DATA;

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hLisnSock, hRecvSock;
    SOCKADDR_IN lisnAdr, recvAdr;
    LPWSAOVERLAPPED lpOvLp;
    DWORD recvBytes;
    LPPER_IO_DATA hbInfo;
    int mode = 1, recvAdrSz, flagInfo = 0;

    if(argc!=2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    if(WSAStartup(MAKEWORD(2,2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");

    // 마지막 인자로 비동기 API 가능한 소켓 생성
    hLisnSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    // Non-blocking 모드로 바꾼다
    ioctlsocket(hLisnSock, FIONBIO, &mode);

    memset(&lisnAdr, 0, sizeof(lisnAdr));
    lisnAdr.sin_family = AF_INET;
    lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);
    lisnAdr.sin_port = htons(atoi(argv[1]));

    if(bind(hLisnSock, (SOCKADDR*) &lisnAdr, sizeof(lisnAdr))==SOCKET_ERROR)
        ErrorHandling("bind() error");
    if(listen(hLisnSock, 5)==SOCKET_ERROR)
        ErrorHandling("listen() error");

    recvAdrSz = sizeof(recvAdr);
    while(1)
    {
        SleepEx(100, TRUE); // for alertable wait state
        hRecvSock = accept(hLisnSock, (SOCKADDR *)&recvAdr, &recvAdrSz);
        // 호출 대상 소켓이 Non-blocking 모드이므로, accept() 정말 받은건지 아닌지 확인 필요.
        if (hRecvSock == INVALID_SOCKET)
        {
            if (WSAGetLastError() == WSAEWOULDBLOCK)
                continue;
            else
                ErrorHandling("aceept() error");
        }
        puts("Client connected...");

        // accept()된 클라에게 WSAOVERLAPPED 구조체 할당할 준비
        lpOvLp = (LPWSAOVERLAPPED)malloc(sizeof(WSAOVERLAPPED));
        memset(lpOvLp, 0, sizeof(WSAOVERLAPPED));

        hbInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
        hbInfo->hClntSock = (DWORD)hRecvSock; // 소켓 핸들 정보 저장
        (hbInfo->wsaBuf).buf = hbInfo->buf;
        (hbInfo->wsaBuf).len = BUF_SIZE;

        lpOvLp->hEvent = (HANDLE)hbInfo; // CR 기반은 event 필요 없으므로, event 오브젝트에 다른 정보 넣어도 된다
        WSARecv(hRecvSock, &(hbInfo->wsaBuf), 1, &recvBytes, &flagInfo, lpOvLp, ReadCompRoutine);
    }
    closesocket(hRecvSock);
    closesocket(hLisnSock);
    WSACleanup();
    return 0;
}

// 이 함수가 호출됐다는 건, 데이터 입력이 완료됐다는 뜻
// WSARecv() 6번째 인자로 넘긴걸, 7번째 인자 함수에서 3번째 인자로 쓴다는게 고정
void CALLBACK ReadCompRoutine(DWORD dwError, DWORD szRecvBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
    SOCKET hSock = hbInfo->hClntSock; // 아까 event 대신 넣어놨던 소켓 정보 활용
    LPWSABUF bufInfo = &(hbInfo->wsaBuf);
    DWORD sentBytes;

    if(szRecvBytes==0)
    {
        closesocket(hSock);
        free(lpOverlapped->hEvent);
        free(lpOverlapped);
        puts("Client disconnectd....");
    }
    else // echo!
    {
        bufInfo->len = szRecvBytes;
        // 다 받았으면, 다음 alertable wait 때 WriteCompRoutine 실행
        WSASend(hSock, bufInfo, 1, &sentBytes, 0, lpOverlapped, WriteCompRoutine);
    }
}

void CALLBACK WriteCompRoutine(DWORD dwError, DWORD szSendBytes, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
    LPPER_IO_DATA hbInfo = (LPPER_IO_DATA)(lpOverlapped->hEvent);
    SOCKET hSock = hbInfo->hClntSock;
    LPWSABUF bufInfo = &(hbInfo->wsaBuf);
    DWORD recvBytes;
    int flagInfo = 0;
    // 다 보냈으면, 다음 alertable wait 때 ReadCompRoutine 실행
    WSARecv(hSock, bufInfo, 1, &recvBytes, &flagInfo, lpOverlapped, ReadCompRoutine);
}

void ErrorHandling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

CR 기반 에코 서버

    memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_addr.s_addr = inet_addr(argv[1]);
    servAdr.sin_port = htons(atoi(argv[2]));

    if(connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
        ErrorHandling("connect() error");
    else
        puts("Connected.....");
    
    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        strLen = strlen(message);
        send(hSocket, message, strLen, 0);
        readLen = 0;
        while(1)
        {
            readLen += recv(hSocket, &message[readLen], BUF_SIZE - 1, 0);
            if(readLen >= strLen)
                break;
        }
        message[strLen] = 0;
        printf("Message from server: %s", message);
    }

CR 기반 에코 서버와 문제 없이 돌아가는 클라

위의 Overlapped IO 모델의 에코 서버가 지니는 단점

  • 넌-블로킹 모드 accept()가 계속 호출됨
  • alertable wait 상태를 위한 SleepEx()가 계속 호출됨

해결 방법

  • accept()는 main 쓰레드에서 처리
  • 클라와의 입출력 담당하는 별도 쓰레드 생성
  • 이것이 바로 IOCP.
    • IO를 담당하는 쓰레드 별도 생성하고, 그 쓰레드가 모든 클라 대상으로 IO 진행.
    • IO만 담당은 아니고, IO의 전후 과정을 전부 담당하는 쓰레드를 하나 이상 생성

중요한 것

  • 쓰레드 생성은 중요한 게 아니다
  • 입출력이 넌-블로킹인가?
  • 넌-블로킹으로 진행된 입출력 완료 어케 확인하는가?
profile
너 정말 **핵심**을 찔렀어

0개의 댓글