#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);
}