메모내용

IOCP Server (직접 구현 코드)

                    
            #include <WinSock2.h>
            #include <Windows.h>
            #include <stdio.h>
            #include <list>
            
            #pragma comment(lib, "ws2_32")
            
            /*	Client 와의 연결에 필요한 정보를 모아놓은 소켓.
                WSARecv 한 데이터는 IOCP 에서 recvBuffer 에 넣어준다. */
            struct Session {
                SOCKET hSocketForClinet;
                char recvBuffer[8192];
            };
            
            void ErrorHandler(const char* msg);
            
            //------------------------------------------------------------------------------------------------------------------------------------------------------//
            
            #define IOCP_THREAD_COUNT 4 // IOCP 에서 WSARecv 를 처리할 Thread 개수, IOCP 모델에서 OS 가 알아서 깨운다.
            HANDLE hIOCP = NULL; // IOCP Handle
            SOCKET hListenSocket = NULL; // ListenSocket
            
            CRITICAL_SECTION cs;
            int ClientCount = 0;
            
            std::list<Session*> SessionList;
            
            //------------------------------------------------------------------------------------------------------------------------------------------------------//
            
            void AddSession(Session* pNewSession)
            {
                EnterCriticalSection(&cs);
                SessionList.push_back(pNewSession);
                ++ClientCount;
                printf("%d 번째Client 가 연결되었습니다.\n", ClientCount);
                LeaveCriticalSection(&cs);
            }
            
            /* Client 와의 Session 을 끊습니다 */
            void CloseSession(SOCKET hSocketForClient)
            {
                std::list<Session*>::iterator SessionIter;
                
                EnterCriticalSection(&cs);
                for (SessionIter = SessionList.begin(); SessionIter != SessionList.end(); ++SessionIter)
                {
                    Session* pSession = *SessionIter;
                    if (pSession->hSocketForClinet == hSocketForClient)
                    {
                        ::closesocket(pSession->hSocketForClinet);
                        delete pSession;
                        SessionList.erase(SessionIter);
                        break;
                    }
                }
                LeaveCriticalSection(&cs);
                puts("Client 와의 Session 종료\n");
            }
            
            /* Listen Server 를 종료합니다. 모든 Client 와의 세션 종료 */
            void CloseServer()
            {
                
                std::list<Session*>::iterator SessionIter;
                
                EnterCriticalSection(&cs);
                for (SessionIter = SessionList.begin(); SessionIter != SessionList.end(); ++SessionIter)
                {
                    Session* pSession = (*SessionIter);
                    ::closesocket(pSession->hSocketForClinet);	// Client 와의 통신소켓 닫기
                    delete pSession;				// 동적할당된 session 메모리 해지
                    SessionList.erase(SessionIter);
                }
                LeaveCriticalSection(&cs);
            
                // ListenSocket 닫기
                ::shutdown(hListenSocket,0);
                ::closesocket(hListenSocket);
            
                // WSA 닫기
                ::WSACleanup();
                
                puts("IOCP Server 종료\n");
            }
            
            void SendMessageToAllClient(const char* msg)
            {
                std::list<Session*>::iterator SessionIter;
            
                char FinalMsg[255] = { 0 };
                sprintf_s(FinalMsg, sizeof(FinalMsg), "Client : %s \n", msg);
            
                EnterCriticalSection(&cs);
                for (SessionIter = SessionList.begin(); SessionIter != SessionList.end(); ++SessionIter)
                {
                    ::send((*SessionIter)->hSocketForClinet, FinalMsg, strlen(FinalMsg), 0);
                }
                LeaveCriticalSection(&cs);
            }
            
            DWORD WINAPI ThreadAcceptLoop(LPVOID pParam)
            {	
                Session* pNewSession = nullptr;
                SOCKET hSocketForClient = NULL;
                SOCKADDR_IN ClientAddr = { 0 };
                int ClientAddrSize = sizeof(ClientAddr);
            
                LPWSAOVERLAPPED lpOverlapped = nullptr;
                WSABUF wsaBuffer = { 0 };
            
                DWORD dwBufferCount, dwFlag = 0;
                DWORD dwNumberOfBytesRecved = 0;
            
                puts("Client 연결을 기다립니다. \n");
                // accept 시작
                while (NULL != hListenSocket)
                {
                    if (INVALID_SOCKET != (hSocketForClient = ::accept(hListenSocket, (SOCKADDR*)&ClientAddr, &ClientAddrSize)))
                    {
                        // Client 연결 완료
            
                        // Session 이랑, WSABUF 할당.
            
                        // 1. Session 초기화
                        pNewSession = new Session;
                        pNewSession->hSocketForClinet = hSocketForClient;
                        memset(pNewSession->recvBuffer, 0, sizeof(pNewSession->recvBuffer));
                        
                        // 4. Session List 에 추가
                        AddSession(pNewSession);
            
                        // 3. OVERLAPPED 초기화
                        lpOverlapped = new WSAOVERLAPPED;
                        ::ZeroMemory(lpOverlapped, sizeof(WSAOVERLAPPED));	// WSAOVERLAPPED 사이즈 만큼 초기화 해야한다. 초기화 하지 않으면 HANDLE Error 발생한다.
            
            
                        // 2. WSABUF 초기화
                        wsaBuffer = { 0 };
                        wsaBuffer.buf = pNewSession->recvBuffer;
                        wsaBuffer.len = sizeof(pNewSession->recvBuffer);
                        dwNumberOfBytesRecved = 0;
                        dwFlag = 0;
            
                        // 3. IOCP Queue 에 감시 Socket 추가
                        // Session 의 주소를 Key로 사용하겠습니다. Key 로 언제든지 Session 에 접근할수 있습니다.
                        ::CreateIoCompletionPort((HANDLE)hSocketForClient, hIOCP, (ULONG_PTR)pNewSession, 0);
            
                        // 4. WSARecv 걸기
                        ::WSARecv(hSocketForClient, &wsaBuffer, 1, &dwNumberOfBytesRecved, &dwFlag, lpOverlapped, NULL);
                        if (!(WSAGetLastError() == WSA_IO_PENDING || WSAGetLastError() == 0)) // PENDING 상태가 아니라면
                        {
                            /*
                                WSAGetLastError () 가
                                0 : 이면 이미 작업이 완료되었음을 의미한다. 아마 Recv를 걸자마자 바로 Recv 받아서 그런것이 아닐까?
                                6 : 이면, 유효하지 않은 핸들이다. OVERLAPPED 초기화, WSA 설정 또한 잘 체크해보자
                                997 : WSA_IO_PENDING 이면 아직 비동기작업이 완료되지 않았을 뿐, 정상이다.
                            */
                            printf("AcceptLoop Thread :: WSARecv Pending 실패 : %d", WSAGetLastError());
                        }
                        puts("Client 의 요청을 대기합니다.\n");
                    }
                    else {
                        puts("Client 연결 실패\n");
                    }
            
                }
                return 0;
            
            }
            
            /* Completion Routine Thread */
            DWORD WINAPI ThreadCompletionRoutine(LPVOID pParam)
            {
                DWORD* lpdwThreadId = (DWORD*)pParam; // main 함수에서 Thread 를 생성하면서 ThreadId 를 넘겼다.
                SOCKET hSocketForClient = NULL;
                LPWSAOVERLAPPED lpOverlapped = nullptr;
                WSABUF wsaBuffer = { 0 };
                Session* pSession = nullptr;
                DWORD dwBufferCount = 0;
                DWORD nNumberOfBytesRecved = 0;	// 수신한 데이터 크기
                DWORD dwGQCS_WaitTime= INFINITE;				// 대기 시간
                DWORD dwFlag = 0;
                
                printf("CompletionRoutine Thread[%d] 시작 \n", (int)*lpdwThreadId);
                
                while (1)
                {
                    printf("CompletionRoutine Thread[%d] 수신대기중... \n", (int)*lpdwThreadId);
            
                    // IOCP Queue 에서 입출력이 완료된 소켓의 상태 가져오기
                    /*
                        GetQueuedCompletionStatus() 가 반환하는경우는 아래와 같이 볼수 있다.
                        변화가 생겼다는건데,
            
                        1. Queue 가 관리하는 Socket 에서 변화가 생겼다.
                        1.1 Socket 이 반환한 데이터가 0 이다 -> Client 에서 Socket 을 종료요청
                        1.2 Socket 이 반환한 데이터가 0 이상이다 -> Client 로부터 데이터 수신한경우
            
                        2. Queue 를 관리하는 IOCP 에 변화가 생겼다.
                        2-1 IOCP 핸들이 유효하지 않은경우, 닫힌경우
                        
                        3. GQCS 가 false 를 반환햇는데, Socket 이 유효하다면, 클라이언트가 비정상 종료했다.
                    */ 
                    bool recvResult = ::GetQueuedCompletionStatus(hIOCP, &nNumberOfBytesRecved, (PULONG_PTR)&pSession, &lpOverlapped, dwGQCS_WaitTime);
            
                    printf("CompletionRoutine Thread[%d] 가 메시지( %d byte )를 수신했습니다.\n", (int)*lpdwThreadId, (int)nNumberOfBytesRecved);
            
                    if (recvResult)
                    {
                        if (nNumberOfBytesRecved == 0)
                        {
                            // FIN 을 받았다.
                            CloseSession(pSession->hSocketForClinet);
                        }
                        else {
            
                            // 정상적인 데이터 수신시.
                            SendMessageToAllClient(pSession->recvBuffer);
            
                            // 데이터 초기화
                            memset(pSession->recvBuffer, 0, sizeof(pSession->recvBuffer));
                            wsaBuffer.buf = pSession->recvBuffer;
                            wsaBuffer.len = sizeof(pSession->recvBuffer);
            
                            nNumberOfBytesRecved = 0;
                            dwBufferCount = 0;
                            dwFlag = 0;
            
                            WSARecv(pSession->hSocketForClinet, &wsaBuffer, 1, &nNumberOfBytesRecved, &dwFlag, lpOverlapped, NULL);
                            if (!(WSAGetLastError() == WSA_IO_PENDING || WSAGetLastError() == 0))
                            {
                                /*
                                    WSAGetLastError () 가 
                                    0 : 이면 이미 작업이 완료되었음을 의미한다. 아마 Recv를 걸자마자 바로 Recv 받아서 그런것이 아닐까?
                                    6 : 이면, 유효하지 않은 핸들이다. OVERLAPPED 초기화, WSA 설정 또한 잘 체크해보자
                                    997 : WSA_IO_PENDING 이면 아직 비동기작업이 완료되지 않았을 뿐, 정상이다.
                                */
                                printf("CompeltionRoutine Thread[%d] 가 WSARecv Pending 실패\n", (int)*lpdwThreadId);
                            }
            
                            // Data Clear
                            lpOverlapped = nullptr;
                            wsaBuffer = { 0 };
                            pSession = nullptr;
                            hSocketForClient = NULL;
                        }
            
                    
                    }
                    else {
                        // 비정상인 경우
            
                        
            
                        // 3. Completion Queue 에서 완료패킷 을 꺼내지 못하고 반환한 경우
                        if (lpOverlapped == NULL)
                        {
                            // 이경우, IOCP 핸들이 닫힌 경우(서버를 종료하는 경우) 도 해당된다.
                            // IOCP 핸들에 해당하는 Queue 에서 먼가 완료되길 기다리고 있엇는데, 완료된것이 아니라 IOCP 가 닫힌경우이다.
                            puts(" GQCS >> IOCP 핸들이 닫혔습니다.\n ");
                            ErrorHandler("GQCS >> 비정상 상태로 인해 서버 종료\n");
                            break;
                            
                        }
                        else {
            
                            if (pSession != NULL) // 비정상 종료인데, 세션이 아직 해지 되지 않았다?
                            {
                                // 4. Client 가 비정상 종료 되었거나, 서버가 먼저 연결을 종료한ㄱ 경우
                                
                                CloseSession(pSession->hSocketForClinet);
                                delete lpOverlapped;
                            }
            
                            puts(" GQCS >> 비정상적으로 Server & Client 세션이 종료 되었습니다.");
                        }
                        
                    }
                }
            
                printf("CompletionRoutine Thread[%d] 종료", (int)*lpdwThreadId);
            
                return 0;
            }
            
            int main()
            {
                InitializeCriticalSection(&cs);
            
                // 1. WSA 초기화
                WSAData wsaData;
                if (0 != ::WSAStartup(MAKEWORD(2, 2), &wsaData))
                {
                    ErrorHandler("WSA 초기화 실패");
                }
                
                // 2. Completion Routine Thread n개 생성
                DWORD dwIOCRThreadId[IOCP_THREAD_COUNT] = {0};
                HANDLE hIOCRThread = 0;
            
                for (int i = 0; i < IOCP_THREAD_COUNT ; ++i)
                {
                    hIOCRThread = 0;
            
                    // WSARecv 를 처리할 CompletionRoutine Thread 생성
                    hIOCRThread = ::CreateThread(
                        NULL,
                        0,
                        ThreadCompletionRoutine,
                        (LPVOID) & (dwIOCRThreadId[i]),
                        0,
                        &(dwIOCRThreadId[i])
                    );
            
                    ::CloseHandle(hIOCRThread); // 미리 닫기, 닫아도 Thread 는 정상 작동
                }
            
                // 3. 새로운 IOCP 생성
                hIOCP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
                if (NULL == hIOCP)
                {
                    puts("ERROR : IOCP 를 생성할 수 없습니다.");
                    return 0;
                }
            
                // 4. Listen Socket 생성 ( OVERLAPPED I/O 사용가능 Socket ), bind , listen
                hListenSocket = ::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
            
                SOCKADDR_IN ListenAddr;
                ListenAddr.sin_family = AF_INET;
                ListenAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
                ListenAddr.sin_port = htons(25000);
            
            
                if (SOCKET_ERROR == (::bind(hListenSocket, (SOCKADDR*)&ListenAddr, sizeof(ListenAddr))))
                {
                    ErrorHandler("Listen Socket 에 IP/Port Bind 에 실패하였습니다.");
                }
            
                if (SOCKET_ERROR == (::listen(hListenSocket, SOMAXCONN)))
                {
                    ErrorHandler("ListenSocket 을 Listen 상태로 전환하는데에 실패하였습니다. ");
                }
            
                // 5. Accept 수행용 Thread 생성
                DWORD dwAcceptThreadId = 0;
                HANDLE hThreadAcceptLoop = ::CreateThread(
                    NULL,
                    0,
                    ThreadAcceptLoop,
                    NULL,
                    0,
                    &dwAcceptThreadId
                );
                ::CloseHandle(hThreadAcceptLoop);
            
                int ServerInput;
                // 6. MainThread 가 종료되지 않도록 설정
                while (1)
                {
                    ServerInput  = getchar();
                    if ('x' == ServerInput)
                    {
                        break;
                    }
                }
            
            
                CloseServer();
            
                DeleteCriticalSection(&cs);
                return 0;
            }
            
            
            void ErrorHandler(const char* msg)
            {
                printf("ERROR >> %s\n", msg);
            
                ::WSACleanup();
                exit(1);
            }