1. 컴플리션 포트 (Completion Port)
I/O 작업의 완료를 신호화한다.
파일 핸들을 포트와 연결하고 작업이 완료되면 큐에 대기시킨다.
(함수의 완료 신호를 큐에 저장한다)
- 워커 스레드
이벤트가 완료되면 큐에서 꺼내어 이어서 작업을 처리하도록 한다.
2. IOCP 모델
WSARecv, WSASend 등의 비동기 함수를 사용한다.
이전에 사용했던 overlapped 구조체를 사용한다.
(1) 컴플리션 포트, 워커 스레드
struct Session
{
SOCKET socket = INVALID_SOCKET;
char recvBuffer[BUF_SIZE] = {};
int32 recvBytes = 0;
};
enum IO_TYPE
{
READ,
WRITE,
ACCEPT,
CONNECT
};
struct OverlappedEx
{
WSAOVERLAPPED overlapped = {};
int32 type = 0;
// 아래로 데이터 추가
};
overlapped 모델에서는 overlapped가 세션 내에 있었지만, 이제는 별도의 구조체를 만들어서 관리한다.
입출력 상태를 표현하는 IO_TYPE을 정의하고 OverlappedEx 구조체에서 type으로 관리한다.
리슨까지는 이전의 코드와 동일하다. 하지만 iocp 모델은 논블로킹으로 생성이 되므로, 논블로킹 소켓을 만들어주는 아래 코드를 삭제한다.
// 논 블로킹 소켓
u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
return 0;
이제 IOCP 큐(컴플리션 포트)를 만든다.
HANDLE iocpHandle = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
이어서 컴플리션 포트의 작업을 처리하는 워커 스레드를 만든다.
// 워커 스레드
for (int32 i = 0; i < 5; i++)
GThreadManager->Launch([=]() {WorkerThreadMain(iocpHandle); });
워커 스레드의 역할은 나중에 구현하도록 한다.
void WorkerThreadMain(HANDLE iocpHandle)
{
}
(2) 세션 관리
연결이 완료된 소켓을 받아 세션으로 저장한다.
세션들을 임시적으로 관리하기 위해 Listen 이후에 아래 동적 배열을 생성한다.
// 세션 관리(임시)
vector<Session*> sessionManager;
while (true)
{
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET)
return 0;
// 소켓을 세션에 저장
Session* session = new Session();
session->socket = clientSocket;
sessionManager.push_back(session);
cout << "클라이언트가 연결됨." << endl;
(3) 컴플리션 포트에 소켓 추가
이제 큐에 소켓을 저장하는 단계이다.
::CreateIoCompletionPort((HANDLE)clientSocket, iocpHandle, (ULONG_PTR)session, 0);
해당 소켓을 iocpHandle로 관찰한다.
이 함수의 세번째 매개변수는 Key이다. key값을 세션의 주소로 설정한다.
(4) 버퍼 생성
이제 버퍼를 생성해서 세션의 버퍼를 넘겨주는 작업을 한다.
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUF_SIZE;
(5) overlapped 활용
이전 overlapped 모델에서 overlapped 매개 변수를 사용하여 데이터에 접근하는 것 처럼, OverlappedEx 구조체를 활용하여 값을 받는다.
OverlappedEx* overlappedEx = new OverlappedEx();
overlappedEx->type = IO_TYPE::READ;
DWORD recvLen = 0;
DWORD flags = 0;
::WSARecv(clientSocket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);
이제 클라이언트의 작업이 완료되었다면 컴플리션 포트에 저장되고, 해당 이벤트를 탐지하기 위해 워커 스레드가 작동하게 된다.
어떤 용도로 쓰기 위함(Read, Write 등등)인지 알기 위하여 확장한 OverlappedEx 구조체를 활용한다.
여기에서의 overlapped는 이전의 Key였던 session과 같이 유저레벨에서 사용이 가능하게 된다.
(6) 워커 스레드
void WorkerThreadMain(HANDLE iocpHandle)
{
while (true)
{
// TODO
// GQCS
DWORD bytesTransferred = 0;
Session* session = nullptr;
OverlappedEx* overlappedEx = nullptr;
bool ret = ::GetQueuedCompletionStatus(iocpHandle, &bytesTransferred, (ULONG_PTR*)&session, (LPOVERLAPPED*)&overlappedEx, INFINITE);
이제 비동기 I/O 작업이 완료되었는지 확인하는 함수 GetQueuedCompletionStatus를 사용한다.
(핸들, 전송된 바이트, 세션 주소, overlapped 구조체, 완료 이벤트가 발생할 때까지 대기하는 시간)
이어서 각 세션에 대해 작업이 완료되지 않았거나 전달된 바이트가 0이면 다른 세션에 대해 검사한다.
// 작업 완료 후
if (ret == false || bytesTransferred == 0)
continue;
WSABUF wsaBuf;
wsaBuf.buf = session->recvBuffer;
wsaBuf.len = BUF_SIZE;
DWORD recvLen = 0;
DWORD flags = 0;
::WSARecv(session->socket, &wsaBuf, 1, &recvLen, &flags, &overlappedEx->overlapped, NULL);
}
3. 주의
(1) 메모리 오염
session을 key로, overlapped를 사용하는데 만약 해당 세션이 종료된 이후에 해당 값에 접근하면 메모리 오염이 발생할 수 있다(댕글링 포인터).
따라서 스마트 포인터를 사용하거나 참조 횟수를 사용하는 것이 안전하다.
(2) 워커 스레드 코드에서 WSARecv를 한번 더 하는 이유는?
한 번 더 실행하지 않으면, 다음번에 클라이언트가 전송한 데이터를 받을 수 없게 된다.
따라서 데이터를 받은 후에 처리를 했으면 다시 한 번 예약을 해야한다.
(3) 단일 세션에 여러 스레드가 접근할 수 있을까?
WSARecv를 실행하면 처리하는 스레드가 하나임이 보장이 되기 때문에 Thread-Safe 하다고 할 수 있다.
따로 락을 걸어줄 필요는 없다. 다만 데이터를 쓸 때(Write)는 문제가 될 수 있다.
4. IOCP 서버의 작동 방식 정리
(1) CreateIoCompletionPort 함수
해당 함수로 iocpHandle 이름을 가지는 컴플리션 포트를 생성한다.
(2) Accept 이후 소켓을 세션에 저장
리슨 이후 억셉트가 발생하면 소켓을 세션에 저장한다.
(3) CreateIoCompletionPort 함수
해당 세션을 핸들로 감시한다.
이후에 해당 세션이 완료되는 이벤트가 발생하면 큐에 저장한다.
(4) WSARecv 등의 비동기 함수
큐를 감시하다가 완료되는 이벤트가 들어오면 작업을 실시한다.
(5) 워커 스레드 작동
각 스레드에서 컴플리션 포트를 매개변수로 삼는 WorkerThreadMain 함수를 실행한다.
GetQueuedCompletionStatus 함수를 사용해 큐에 완료 이벤트가 발생했는지 확인한다.
true라면 이제 recv 또는 send 등의 작업을 실시해준다.
(6) 워커 스레드 내의 비동기 함수
이제 다시 이벤트를 기다리기 위해 스레드의 작업 함수 마지막에 WSA 함수를 작동시킨다.
'서버 프로그래밍 > 네트워크' 카테고리의 다른 글
2-8. 멀티 스레드 환경에서 발생하는 상황들 (0) | 2023.12.04 |
---|---|
2-6. 소켓 입출력 모델 (2) - Overlapped 모델 (0) | 2023.12.01 |
2-5. 소켓 입출력 모델 (1) - Select 모델 (Select, WSAEventSelect) (0) | 2023.11.30 |
2-4. 소켓 옵션 설정, 논-블로킹 소켓(Non-Blocking Socket) (0) | 2023.11.30 |
2-3. TCP와 UDP (0) | 2023.11.29 |