bdfgdfg

[온라인 서버] Overlapped IO(이벤트 기반) 본문

게임프로그래밍/서버 책

[온라인 서버] Overlapped IO(이벤트 기반)

marmelo12 2021. 12. 29. 16:49
반응형

콜백기반은 단순히 콜백함수를 등록하고, 해당 비동기 io를(정확히는 Overlapped IO, WSARecv,WSASend등의 함수)호출한 쓰레드에 완료된 작업을 APC큐에 넣어둔다.

그리고 해당 쓰레드가 호출이 되어도 되는 순간에 Alertable상태로 만들어 APC큐에 쌓인 모든 일감들을 처리한다.

 -> 단순하게 보면 이벤트기반과 달리 생성과 신호체크가 없고, 콜백함수를 통해 IO완료 처리를 하게 된다.

 

먼저 Overlapped I/O는 비동기 I/O이다. 넌블록킹 + 비동기 I/O의 조합으로서 사용되는 방법인데.

동기를 예로들면, 넌블록킹 소켓일 때 그 작업이 끝나고 나서 결과를 따로 통지하거나 알려주지 않기때문에 따로 사용자가 계속해서 동기 I/O가 완료되었는지 확인을 해야한다.

Overlapped I/O와 같은 비동기 I/O의 경우는 비동기로 I/O를 요청하고 요청한 I/O가 완료되면 이벤트 혹은 콜백방식을 통해 I/O완료를 체크할 수 있다. 

 

설명 잘되어있는 블로그

https://chfhrqnfrhc.tistory.com/entry/Overlapped-IO

 

간단하니 이벤트 기반만 보도록한다.

enum class IO_TYPE
{
	IO_RECV,
	IO_SEND
};

// 워커쓰레드 모델을 채택.
// 각각 접속을 처리하는 쓰레드, 일하는 쓰레드(IO처리)

// Overlapped 구조체를 확장하여 사용.
struct OverlappedEx
{
	WSAOVERLAPPED m_wsaOverlapped;
	int			  m_index;
	WSABUF		  m_wsaBuf;
	char		  m_dataBuffer[MAX_SOCKBUF];
	IO_TYPE		  m_ioType;
};

// 클라이언트 정보를 담기위한 구조체
struct ClientInfo
{
	SOCKET			m_socketClient[WSA_MAXIMUM_WAIT_EVENTS]; // 64
	WSAEVENT		m_eventHandle[WSA_MAXIMUM_WAIT_EVENTS];
	OverlappedEx	m_overlappedEx[WSA_MAXIMUM_WAIT_EVENTS];
};

먼저 데이터를 송수신할 때 이 타입이 무엇인지 알기위해 enum class IO_TYPE을 정의.

 

OverlappedEx는 Ovrelapped구조체를 확장하여 사용한다.

즉. WSARecv나 WSASend에 Overlapped를 건넬 때, 확장된 버전의 변수를 넘긴다(물론 캐스팅하여 넘겨야한다)

 -> 이게 가능한 이유는 확장한 구조체의 첫번째 offset을 WSAOVERLAPPED로 해두었기 때문.

 -> 확장한 멤버는 WSABUF, IO_TYPE, 데이터 버퍼(이건 WSABUF의 buf에 등록하기 위함)등

 

이제 ClientInfo는 서버와 연결된 클라이언트 소켓들의 정보를 담는다.

 -> 여기서 이벤트 인덱스와 클라이언트 인덱스를 1:1로 맞추는데 이벤트 셀렉트와 같은 방식이다.

 -> 클라이언트들은 자기만의 OverlappedEx(확장버전)을 가진다.

 -> 즉 클라이언트 소켓 하나의 정보를 가져올 때, OvrelappedEx의 자기 index, wsaOverlapped등의 정보를 손쉽게 가져오는 것.

 

class OverlappedEvent
{
public:
	OverlappedEvent();
	virtual ~OverlappedEvent();
public:
	//------서버 클라이언트 공통함수-------//
	//소켓을 초기화하는 함수
	bool InitSocket();
	//소켓의 연결을 종료 시킨다.
	void CloseSocket(SOCKET socketClose, bool bIsForce = false);

	//------서버용 함수-------//
	//서버의 주소정보를 소켓과 연결시키고 접속 요청을 받기 위해 그 소켓을 등록하는 함수
	bool BindandListen(int nBindPort);
	//접속 요청을 수락하고 메세지를 받아서 처리하는 함수
	bool StartServer();

	//Overlapped I/O작업을 위한 쓰레드를 생성
	bool CreateWokerThread();
	//accept요청을 처리하는 쓰레드 생성
	bool CreateAccepterThread();

	//사용되지 않은 index반환
	int GetEmptyIndex();

	//WSARecv Overlapped I/O 작업을 시킨다.
	bool BindRecv(int nIdx);

	//WSASend Overlapped I/O작업을 시킨다.
	bool SendMsg(int nIdx, char* pMsg, int nLen);

	//Overlapped I/O작업에 대한 완료 통보를 받아 
	//그에 해당하는 처리를 하는 함수
	void WokerThread();
	//사용자의 접속을 받는 쓰레드
	void AccepterThread();

	//Overlapped I/O 완료에 대한 결과 처리
	void OverlappedResult(int nIdx);

	//생성되어있는 쓰레드를 파괴한다.
	void DestroyThread();

private:
	//클라이언트 정보 저장 구조체
	ClientInfo	m_clientInfo;

	//접속 되어있는 클라이언트 수
	int			m_clientCnt;
	//메인 윈도우 포인터

	//작업 쓰레드 핸들
	HANDLE		m_hWorkerThread;
	//접속 쓰레드 핸들
	HANDLE		m_hAccepterThread;
	//작업 쓰레드 동작 플래그
	bool		m_bWorkerRun;
	//접속 쓰레드 동작 플래그
	bool		m_bAccepterRun;

	//소켓 버퍼
	char		m_socketBuf[MAX_SOCKBUF];
};

 

이제 Overlapped IO 이벤트 기반을 적용한 서버를 위한 클래스.

 

중요한 함수는 밑과 같다. 

CreateWokerThread

CreateAccepterThread

BindRecv

SendMsg

WokerThread

AccepterThread

OverlappedResult

 

물론 나머지가 중요하지 않다는건 아니니 서버의 흐름대로 본다.

1. InitSocket

2. BindAndListen

3. StartServer

bool OverlappedEvent::InitSocket()
{
    WSADATA wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        return false;
    }

    // 연결지향형 TCP, Overlapped I/O 소켓을 생성(리슨 소켓)
    m_clientInfo.m_socketClient[0] = ::WSASocket(AF_INET, SOCK_STREAM, 0, 0, 0, WSA_FLAG_OVERLAPPED);
    if (m_clientInfo.m_socketClient[0] == INVALID_SOCKET)
    {
        return false;
    }

    std::cout << "소켓 초기화 성공" << std::endl;

    return true;
}

참고로 WSASocket함수의 마지막 인자로 WSA_FLAG_OVERLAPPED를 넣어야 Overlapped I/O가 가능해진다고 한다.

일반적인 socket함수는 저런 인자값을 넣지않아도 가능한데 기본적으로 Overlapped I/O가 가능한 소켓으로 만들어진다고 한다.

bool OverlappedEvent::BindandListen(int nBindPort)
{
    SOCKADDR_IN serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
    serverAddr.sin_port = ::htons(PORT_NUM);

    if (::bind(m_clientInfo.m_socketClient[0], (SOCKADDR*)&serverAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
        return false;

    if (::listen(m_clientInfo.m_socketClient[0], 5) == SOCKET_ERROR)
        return false;

    std::cout << "bind And Listen 성공! " << std::endl;
    return true;
}

m_socketClient[0]과 m_eventHandle[0]은 각각 리슨소켓과 리슨소켓과 대응되는 이벤트 커널 객체이다.

bool OverlappedEvent::StartServer()
{
    // 접속된 클라이언트 주소 정보를 저장할 구조체.
    bool bRet = CreateWokerThread(); // 워커 쓰레드 생성.
    if (bRet == false)
        return false;
    bRet = CreateAccepterThread();
    if (bRet == false)
        return false;

    // 정보 갱신용 이벤트 생성.
    m_clientInfo.m_eventHandle[0] = ::WSACreateEvent();

    return true;
}

StartServer함수 호출 시, 각각 WorkerThread, AccepterThread를 하나씩 호출한다.

그리고 리슨소켓의 이벤트를 등록.

그리고 메인 쓰레드는 이후 무한루프에 빠진다. (IO처리 클라연결요청처리는 각 쓰레드가 알아서 처리한다)

 

먼저 흐름대로 CreateWorkerThread를 보자.

bool OverlappedEvent::CreateWokerThread()
{
    UINT threadId = 0;

    // 보통 인자로 자기자신을 넘겨준다. 그렇기에 이 클래스는 Singleton으로 관리되는게 좋음.
    //CREATE_SUSPENDED는 ResumeThread함수를 호출하기전까지 쓰레드는 생성만되고 함수 진입안함.
    m_hWorkerThread = (HANDLE)_beginthreadex(NULL, 0, CallWorkerThread, this, CREATE_SUSPENDED, &threadId);
    if (m_hWorkerThread == NULL)
        return false;


    ::ResumeThread(m_hWorkerThread);
    return true;
}

beginthreadex함수의 인자로 (정확히는 쓰레드 함수가 호출될 때 넘겨주는 인자) this를 넘기는데.

그 이유는 쓰레드 함수는 멤버함수가 아닌 전역함수이기에 this를 넘겨주는 것.

(참고로 CREATE_SUSPENDED는 당장은 쓰레드만 생성하고 쓰레드 함수에 진입하지 말아란 의미)

(그렇기에 밑에서 ResumThread를 호출하는 모습)

 

WorkerThread의 함수를 처리하는 쓰레드 함수를 보자.(전역함수)

// 비동기 IO처리 (OVERLAPPED IO). 비동기 IO와 OVERLAPPED IO는 거의 같은말. 
// 결과 확인을 이벤트,콜백방식을 채택한게 OVERLAPPED IO
unsigned int WINAPI CallWorkerThread(LPVOID p)
{
    OverlappedEvent* pOverlappedEvent = (OverlappedEvent*)p;
    pOverlappedEvent->WokerThread();
    return 0;
}

this로 넘겨준 자기자신을 다시 캐스팅한다.

(this의 클래스는 OverlappedEvent클래스)

그리고 멤버함수인 WorkerThread함수를 호출한다. 이어서 보자.

//일하는 쓰레드. 즉 Overlapped I/O처리 
void OverlappedEvent::WokerThread()
{
    while (m_bWorkerRun)
    {
        // 요청한  Overlapped I/O 작업이 완료되었는지 이벤트를 기다린다.
        // WSASend,WSARecv - 비동기 IO의 요청이 완료되면 이벤트 커널 객체는 signaled상태가 된다.
        DWORD objIdx = ::WSAWaitForMultipleEvents(WSA_MAXIMUM_WAIT_EVENTS,
            m_clientInfo.m_eventHandle, FALSE, INFINITE, FALSE);

        objIdx -= WSA_WAIT_EVENT_0;
        // 에러 발생
        if (objIdx == WSA_WAIT_FAILED)
            break;

        // 이벤트를 리셋. (manual-reset모드이기에 다시 닫아줘야함)
        ::WSAResetEvent(m_clientInfo.m_eventHandle[objIdx]); // Non-signaled상태로 만든다. (Set은 signaled상태로 만듬)

        // 접속이 들어왔다.
        if (objIdx == WSA_WAIT_EVENT_0)
            continue;

        //Overlapped IO에 대한 결과처리
        OverlappedResult(objIdx);
    }
}

 

여기서 중요한거. 이 서버클래스는 Overlapped IO 이벤트기반이지만

방식이 약간 이벤트 셀렉트 모델과 비슷하다.

 

WSAWaitForMultipleEvents함수는 이벤트 핸들의 배열을 넘겨주면 이벤트가 발생한 첫번째 인덱스를 반환한다.

(굳이 WSA_WAIT_EVENT는 안빼줘도 되긴함)

현재 우리는 이벤트 셀렉트 방식처럼 소켓배열,이벤트배열을 만들고 인덱스를 1:1로 맞춰줬다.

그렇기에 이 인덱스를 이용한다.

 

현재 WSACreateEvent로 이벤트 객체를 만들었음 (manual-reset모드 + non-signaled상태)

그렇기에 다시 WSAResetEvent를 호출해야 한다.

 -> 그렇지 않으면 계속 signaled상태가 된다. (이렇거면 그냥 auto-reset도 괜찮을듯하긴 하다)

 

그리고 objidx == WSA_WAIT_EVENT_0이다. (사실 이건 그냥 0으로 박아놓는게 좋을 듯하다)

WSA_WAIT_EVENT_0은 매크로이며 그냥 정수 0이다.

즉 리슨소켓과 1대1연결된 이벤트 객체의 signaled라면 무시한다. (AccepterThread가 별도로 처리함)

 

이어서 OverlappedResult함수를 보자.

void OverlappedEvent::OverlappedResult(int nIdx)
{
    DWORD transfer = 0; // 송수신받은 바이트
    DWORD flags = 0; // flag 쓸일없으므로 0으로 한다.

    bool ret = ::WSAGetOverlappedResult(m_clientInfo.m_socketClient[nIdx],
        (LPWSAOVERLAPPED)&m_clientInfo.m_overlappedEx[nIdx], &transfer, FALSE, &flags);
    if (ret && transfer == 0) 
        return;

    // 연결해제 요청
    if (transfer == 0)
    {
        ::closesocket(m_clientInfo.m_socketClient[nIdx]);
        --m_clientCnt;
        return;
    }

    // OverlappedEx 추출.
    OverlappedEx* pOverlappedEx = &m_clientInfo.m_overlappedEx[nIdx];
    switch (pOverlappedEx->m_ioType)
    {
        // WSARECV로 Overlapped I/O가 완료된 경우
    case IO_TYPE::IO_RECV:
        pOverlappedEx->m_dataBuffer[transfer] = '\0';
        std::cout << "[수신] bytes : " << transfer << " msg : " << pOverlappedEx->m_dataBuffer << std::endl;

        //클라이언트에게 에코
        SendMsg(nIdx, pOverlappedEx->m_dataBuffer, transfer);
        break;
    case IO_TYPE::IO_SEND:
        pOverlappedEx->m_dataBuffer[transfer] = '\0';
        std::cout << "[송신] bytes : " << transfer << " msg : " << pOverlappedEx->m_dataBuffer << std::endl;
        BindRecv(nIdx); // 다시 Recv걸어준다. 
        break;

    default:
        break;
    }
}

먼저 가장 중요한 함수. 이벤트가 signaled가 되었다면(상태확인) 이제 결과를 확인해야 한다.

바로 WSAGetOverlappedResult함수. 우리가 선언한 지역변수 transfer(송수신)의 값을 얻어온다.

 -> 더 중요한건 WSARecv,WSASend등의 비동기 I/O를 호출할 때 넘겨준 Overlapped구조체의 주소를 얻어온다.★

우리는 확장버전의 OverlappedEx구조체를 넘겨줬다. 

 -> 즉 내부 멤버의 IO_TYPE과(이게 SEND였는지 RECV였는지) 버퍼내용, 인덱스등의 정보를 다시 얻어올 수 있다.

 

건네줬던 overlapped의 IO_TYPE을 확인해보고 RECV였다면 에코니 다시 SEND해준다.

SEND였다면 에코니 다시 RECV를 해야한다.

 

먼저 SendMsg부터 본다.

bool OverlappedEvent::SendMsg(int nIdx, char* pMsg, int nLen)
{
    DWORD recvNumBytes = 0;

    // 에코서버니 수신받은 메시지를 복사한다.
    ::CopyMemory(m_clientInfo.m_overlappedEx[nIdx].m_dataBuffer, pMsg, nLen);

    // Overlapped I/O를 위한 셋팅
    m_clientInfo.m_overlappedEx[nIdx].m_wsaOverlapped.hEvent = m_clientInfo.m_eventHandle[nIdx];
    m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf.len = nLen;
    m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf.buf = m_clientInfo.m_overlappedEx[nIdx].m_dataBuffer;
    m_clientInfo.m_overlappedEx[nIdx].m_index = nIdx;
    m_clientInfo.m_overlappedEx[nIdx].m_ioType = IO_TYPE::IO_SEND;

    int ret = ::WSASend(m_clientInfo.m_socketClient[nIdx], &m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf,
        1, &recvNumBytes, 0, (LPWSAOVERLAPPED)&m_clientInfo.m_overlappedEx[nIdx], nullptr);

    // 에러인데 PENDING까지 아니라면 탈출. (연결이 끊긴걸로 판단)
    if (ret == SOCKET_ERROR && ::WSAGetLastError() != WSA_IO_PENDING)
    {
        return false;
    }

    return true;
}

서버입장에서 Send를 한다는것은 먼저 데이터를 받았다는 의미이다.

(현재 에코구조는 클라가 먼저 send 서버가 recv)

즉 현재 버퍼에 pMsg 인자로 넘어온 데이터로 넘겨준다.

 -> 사실이거는 이럴필요는 없는게 이미 소켓의 dataBuffer에 내용이 저장이 되어있음.

 -> 그래도 OverlappedResult함수에서 송수신된 transfer가 중요하니. 저렇게 구조를 잡은듯하다.

 DWORD transfer = 0; // 송수신받은 바이트
 DWORD flags = 0;

 // PENDING일때 체크하는 함수이긴함.
 bool ret = ::WSAGetOverlappedResult(m_clientInfo.m_socketClient[nIdx],
     (LPWSAOVERLAPPED)&m_clientInfo.m_overlappedEx[nIdx], &transfer, FALSE, &flags);

그리고 Overlapped I/O를 위한 셋팅을 한다.

 

현재 클라이언트 정보들중에서 데이터를 보낼 연결된 클라이언트 소켓의 OverlappedEx멤버를 채워주고.

WSASend를 걸어준다.

 

 

이제 IO_TYPE::SEND였을 때를 보자.

위에서 WSASEND를 걸었고, overlappedEx의 멤버로 IO_TYPE::IO_SEND로 해주었음.

case IO_TYPE::IO_SEND:
    pOverlappedEx->m_dataBuffer[transfer] = '\0';
    std::cout << "[송신] bytes : " << transfer << " msg : " << pOverlappedEx->m_dataBuffer << std::endl;
    BindRecv(nIdx); // 다시 Recv걸어준다. 
    break;

즉 다음에는 위의 코드가 실행이 된다는 의미.

에코서버라서 다시 Recv를 걸어주는 코드인데.

나중에 IOCP일 때도 이게 좀 중요한 키워드인게 처음에 클라이언트와 연결된 소켓을 생성하고 나서.

무조건 한번 Recv를 걸어줘야한다! -> 언제 패킷이 올지도 모르지만 cp객체는 우리가 일을 요청하지않으면 일을 안함.

 

BindRecv함수를 보자. 이 함수가 중요하다고 생각이 든다.

bool OverlappedEvent::BindRecv(int nIdx)
{
    DWORD flag = 0;
    DWORD recvNumBytes = 0;

    m_clientInfo.m_eventHandle[nIdx] = ::WSACreateEvent();

    //Overlapped I/O 
    m_clientInfo.m_overlappedEx[nIdx].m_wsaOverlapped.hEvent = m_clientInfo.m_eventHandle[nIdx];
    m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf.len = MAX_SOCKBUF;
    m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf.buf = m_clientInfo.m_overlappedEx[nIdx].m_dataBuffer;
    m_clientInfo.m_overlappedEx[nIdx].m_index = nIdx;
    m_clientInfo.m_overlappedEx[nIdx].m_ioType = IO_TYPE::IO_RECV;

    // 바로 작업이 완료 될수도 있고(recvNumBytes에 수신 데이터 담긴다) 아닐수도있다(PENDING)
    int ret = ::WSARecv(m_clientInfo.m_socketClient[nIdx], &m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf,
        1, &recvNumBytes, &flag, (LPWSAOVERLAPPED)&m_clientInfo.m_overlappedEx[nIdx], nullptr);
    
    
    
    // 에러인데 PENDING까지 아니라면 탈출. (연결이 끊긴걸로 판단)
    if (ret == SOCKET_ERROR && ::WSAGetLastError() != WSA_IO_PENDING)
    {
        return false;
    }
    // 바로 반환되는것과 바로 반환이 안되면 PENDNG처리가 따로되어야하지만, 위에서는 그냥 바로 false때림.
    

    return true;
}

먼저 항상중요한거.

WSASend나 WSARecv나 바로 요청한게 반환이 될 수 있기에 transfer. 즉 송수신한 바이트의 수를 반환했다면

바로 처리하면 되긴한다.

 -> 하지만 여기 서버코드는 이벤트 감지를 통해서 결과를 처리하는 루틴.

 -> 또 현재 TCP 서버이기때문에 데이터가 1바이트라도 읽어들인다음 이벤트가 signaled상태가 될수도 있다.

     즉 나중에는 패킷 클래스를 정의하고 패킷크기를 확인해 크기가 다르다면 다시 WSARecv를 거는등의 처리가 필요.

 

이제 Overlapped I/O를 세팅해주고, (여기서 CreateEvent를 해주는데 Accept에서 BindRecv를 미리걸어두기 때문(호출))

 -> 이 코드는 이전의 이벤트 객체를 덮어버리기에 내부적으로 이벤트 객체를 이미 가지고있었다면 무시하는게 좋을듯하다.

m_clientInfo.m_overlappedEx[nIdx].m_wsaOverlapped.hEvent = m_clientInfo.m_eventHandle[nIdx];
m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf.len = MAX_SOCKBUF;
m_clientInfo.m_overlappedEx[nIdx].m_wsaBuf.buf = m_clientInfo.m_overlappedEx[nIdx].m_dataBuffer;
m_clientInfo.m_overlappedEx[nIdx].m_index = nIdx;
m_clientInfo.m_overlappedEx[nIdx].m_ioType = IO_TYPE::IO_RECV;

도 마찬가지. 처음에 Accept일 때, 한번 WSARecv를 걸기위한 세팅.

우리는 지금 SEND하고나서 Recv를 다시 하는 케이스이므로

 

굳이 다시 이렇게 등록할 필요는 없다.

 

이게 워커쓰레드의 동작방식

이제 Accepter를 처리하는 쓰레드를 본다.

// 사용자의 접속을 처리하는 쓰레드 함수
void OverlappedEvent::AccepterThread()
{
    SOCKADDR_IN clientAddr;
    int addrLen = sizeof(SOCKADDR_IN);

    while (m_bAccepterRun)
    {
        // 접속을 받을 구조체의 인덱스를 얻어온다.
        int idx = GetEmptyIndex(); // 빈 인덱스. 즉 배열에서 빈 부분.

        if (idx == -1) // 가득찬 상황(64개 오버)
        {
            return;
        }

        // 클라이언트 접속 요청이 들어올때까지 대기.(동기방식)
        m_clientInfo.m_socketClient[idx] = ::accept(m_clientInfo.m_socketClient[0],
            (SOCKADDR*)&clientAddr, &addrLen);

        if (m_clientInfo.m_socketClient[idx] == INVALID_SOCKET)
            return;

        // 새로 생성된 소켓 이벤트 생성해준 후,(이벤트 셀렉트랑 비슷함 결국 소켓 == 이벤트 1대1연결구조)
        // 그리고 WSARecv를 미리 걸어둠. (해당 소켓의 입력버퍼에 내용 있으면 읽어들임)
        bool ret = BindRecv(idx); 
        if (ret == false)
            return;

        // 클라이언트 개수 증가
        ++m_clientCnt;

        // 클라이어트가 접속되었으므로 WorkerThread에게 이벤트로 알린다.
        ::WSASetEvent(m_clientInfo.m_eventHandle[0]); // 리슨소켓의 이벤트이지만, 워커쓰레드용 이벤트이기도함.

    }
}

비교적 간단하다.

GetEmptyIndex는 현재 소켓 배열, 이벤트 배열에서 다음 빈공간을 이어서 저장하기 위해 인덱스를 얻어오는 함수다.

 

그리고 클라이언트의 접속요청이 올때까지 accept대기를 한다.

연결이 되었다면 BindRecv함수를 호출 -> 미리 Recv대기.

 

그리고 연결이 완료되었으니 리슨소켓과 1대1연결된 이벤트를 signaled로 켜준다.

 -> 물론 Worker쓰레드에서는 이벤트 배열의 0번 이벤트 객체는 무시한다.

 

결과

 

반응형
Comments