Select 모델 (동기방식)
Select 모델은 여러 소켓에 대한 입출력 이벤트를 감지하고 처리할 수 있도록 도와줍니다.
Select 모델은 여러 소켓의 상태를 감지하고, 해당 소켓들 중에서 입력이나 출력 준비가 된 소켓을 선택하여 처리할 수 있도록 해줍니다. 이는 비동기 소켓 프로그래밍의 복잡성을 줄이면서도 다중 클라이언트를 처리하는 데 효과적입니다. select() 함수를 사용하여 소켓의 상태를 검사하고, 필요한 작업을 수행합니다.
Select 모델은 단일 스레드에서 여러 소켓을 처리하기 때문에 비교적 간단하게 구현할 수 있으며, 블로킹 소켓을 사용하더라도 다중 클라이언트를 동시에 처리할 수 있습니다. 그러나 매우 많은 수의 소켓을 다루는 경우에는 비효율적일 수 있습니다. 이런 경우에는 다른 모델이나 비동기 소켓을 고려해야 할 수도 있습니다.
Select모델의 핵심은 소켓 함수 호출이 성공할 시점을 미리 알 수 있다는 것이다.
예를 들어 수신버퍼에 데이터가 없는데 read를 할 때, 송신버퍼가 꽉 찼는데 write를 할 때 블로킹이라면 대기를 하게 된다.
Select모델을 블로킹 소켓에 적용한다면 조건이 만족되지 않아서 블로킹되는 상황을 예방할 수 있고
논블로킹 소켓에서는 조건이 만족되지 않아서 불필요하게 반복 체크하는 상황을 예방할 수 있다.
1. Select모델은 읽기, 쓰기, 예외에 대한 관찰 대상을 등록한다.
2. select(readSet, writeSet, exceptSet);
3. 적어도 하나의 소켓이 준비되면 리턴하고 낙오자는 알아서 제거된다.
4. 남은 소켓 체크해서 진행
select() 함수와 함께 사용되는 매크로에는 FD_ZERO, FD_SET, FD_CLR, FD_ISSET 가 있다. 이들은 파일 디스크립터 세트를 초기화, 설정, 해제 및 테스트하는 데 사용됩니다.
- FD_ZERO(fd_set *set): 주어진 파일 디스크립터 세트(set)를 초기화합니다. 이는 모든 파일 디스크립터를 비활성화하는 데 사용됩니다.
- FD_SET(int fd, fd_set *set): 주어진 파일 디스크립터를 파일 디스크립터 세트(set)에 추가합니다. 이는 해당 파일 디스크립터가 활성화되었음을 나타냅니다.
- FD_CLR(int fd, fd_set *set): 주어진 파일 디스크립터를 파일 디스크립터 세트(set)에서 제거합니다. 이는 해당 파일 디스크립터를 비활성화하는 데 사용됩니다.
- FD_ISSET(int fd, fd_set *set): 주어진 파일 디스크립터가 파일 디스크립터 세트(set)에 있는지 여부를 확인합니다. 이 함수는 주어진 파일 디스크립터가 활성화되었는지를 테스트하는 데 사용됩니다.
#include <iostream>
#include <windows.h>
#include <WinSock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
void HandleError(const char* cause)
{
int32 errCode = ::WSAGetLastError();
cout << cause << " ErrorCode" << errCode << endl;
}
const int32 BUFSIZE = 1000;
struct Session
{
SOCKET socket;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
int32 sendBytes = 0;
};
int main()
{
//윈소켓 초기화(w2_32 라이브러리 초기화)
//관련정보가 wsaData에 채워짐
WSADATA wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_SOCKET)
return 0;
//논블로킹을 하기위해
u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
return 0;
//나의 주소 : IP주소 + PORT
SOCKADDR_IN serverAddr;//IPv4
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);//아이피 알아서 설정
serverAddr.sin_port = ::htons(7777);//PORT
//socket과 주소 연동
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
//서버오픈
if (::listen(listenSocket, 10) == SOCKET_ERROR)//10은 대기열 이걸넘으면 못들어옴
return 0;
cout << "Accept" << endl;
//fd_set set;
//FD_ZERO(set) : 초기화(비운다)
//FD_SET(s, &set) : 소켓 s를 넣는다
//FD_CLR(s, &set) : 소켓 s를 제거
//FD_ISSET : 소켓 s가 set에 들어있으면 0이 아닌 값을 리턴한다.
vector<Session> sessions;
sessions.reserve(100);
fd_set reads;
fd_set writes;
while (true)
{
//소켓 셋 초기화
FD_ZERO(&reads);
FD_ZERO(&writes);
//ListenSocket 등록
FD_SET(listenSocket, &reads);
//소켓 등록
for (Session& s : sessions)
{
if (s.recvBytes <= s.sendBytes)
FD_SET(s.socket, &reads);
else
FD_SET(s.socket, &writes);
}
//옵션 마지막 timeout 인자 설정 가능
/* timeval timeout;
timeout.tv_sec;
timeout.tv_usec;*/
int32 retVal = ::select(0, &reads, &writes, nullptr, nullptr);//관찰시작 낙오자 제거
if (retVal == SOCKET_ERROR)
break;
//Listener 소켓 체크
if (FD_ISSET(listenSocket, &reads))
{
SOCKADDR_IN clientAddr;//IPv4
::memset(&clientAddr, 0, sizeof(clientAddr));
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
cout << "client connected" << endl;
sessions.push_back(Session{ clientSocket });
}
}
//나머지 소켓 체크 read나 write
for (Session& s : sessions )
{
//read 체크
if (FD_ISSET(s.socket, &reads))
{
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
if (recvLen <= 0)
{
//TODO : SESSIONS 제거
continue;
}
s.recvBytes = recvLen;
}
//write 체크
if (FD_ISSET(s.socket, &writes))
{
int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes],s.recvBytes - s.sendBytes, 0);
if (sendLen == SOCKET_ERROR)
{
//TODO : SESSIONS 제거
continue;
}
s.sendBytes += sendLen;
if (s.recvBytes == s.sendBytes)
{
s.recvBytes = 0;
s.sendBytes = 0;
}
}
}
}
::WSACleanup();
}
WASEventSelect ( 비동기 방식 )
Select 모델과 다르게 전체리 셋을 매번 해줄 필요가 없다는 장점이 있다.
WSAEventSelect함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환된다.
accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다. 따라서 clientsocket은 FD_READ, FD_WRITE 등을 다시 등록할 필요가 있다.
드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리 필요하다.
중요)
- 이벤트 발생 시적 절한 소켓 함수 호출해야 함. 아니면 다음번에는 동일 네트워크 이벤트가 발생하지 않는다.
EX) FD_READ이벤트 떴으면 recv() 호출해야 하고 안 하면 FD_READ 두 번 다시 X
1. WSAEventSelect
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);
- 특정 소켓에서 발생하는 네트워크 이벤트(FD_READ, FD_WRITE, FD_CONNECT 등)를 비동기적으로 감지합니다.
- 지정된 이벤트가 발생하면 연관된 이벤트 객체가 신호 상태로 설정됩니다.
- 소켓 개수 만큼 이벤트 객체에 연동해야 한다.
FD_ACCEPT : 접속한 클라이언트가 있는지 accept호출
FD_READ : 데이터 수신가능 recv, recvfrom
FD_WRITE : 데이터 송신 가능 send, sendto
FD_CONNECT : 통신을 위한 연결 절차 완료 확인
2. WSACreateEvent(생성)
WSAEVENT WSACreateEvent(void);
- 새로운 이벤트 객체를 생성합니다.
- 이 객체는 이후에 소켓과 연관 지어 네트워크 이벤트를 모니터링하는 데 사용됩니다.
- 이벤트 객체는 초기 상태가 비신호 상태이며, 특정 이벤트가 발생하면 신호 상태로 변경됩니다.
3. WSACloseEvent(삭제)
BOOL WSACloseEvent(WSAEVENT hEvent);
- 사용이 끝난 이벤트 객체를 닫고 자원을 해제합니다.
- 메모리 누수를 방지하고 시스템 자원을 효율적으로 관리할 수 있게 합니다.
- 다른 함수들(WSEventSelect, WSAWaitForMultipleEvents 등)과 연동되어 동작하는 이벤트 객체를 적절하게 정리할 수 있습니다.
4. WSAWaitForMultipleEvents(신호 상태 감지)
DWORD WSAWaitForMultipleEvents(DWORD cEvents, const WSAEVENT* lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable);
- 다수의 이벤트 객체가 신호 상태가 될 때까지 대기합니다.
- fWaitAll 매개변수를 통해 모든 이벤트가 신호 상태가 될 때까지 대기하거나, 하나의 이벤트가 신호 상태가 되면 반환할 수 있습니다.
- dwTimeout을 통해 대기 시간을 설정할 수 있어 무한 대기 또는 제한된 시간 동안만 대기하도록 제어할 수 있습니다.
5. WSAEnumNetworkEvents(구체적인 네트워크 이벤트 알아내기)
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);
- 특정 소켓과 연관된 네트워크 이벤트들을 열거합니다.
- 발생한 이벤트의 종류를 확인하고 해당 이벤트에 대해 적절한 처리를 할 수 있도록 합니다.
- 이벤트 객체의 상태를 리셋하여 다음 이벤트를 감지할 준비를 합니다.
- WSAEventSelect: 소켓 이벤트를 비동기적으로 감지하고 이벤트 객체를 신호 상태로 설정합니다.
- WSACreateEvent: 비동기 이벤트 처리를 위한 새로운 이벤트 객체를 생성합니다.
- WSACloseEvent: 더 이상 필요하지 않은 이벤트 객체를 닫아 자원을 해제합니다.
- WSAWaitForMultipleEvents: 여러 이벤트 객체 중 하나 또는 모두가 신호 상태가 될 때까지 대기합니다.
- WSAEnumNetworkEvents: 특정 소켓과 연관된 발생한 네트워크 이벤트를 확인하고 처리합니다.
#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <atomic>
#include <mutex>
#include <windows.h>
#include <future>
#include "ThreadManager.h"
#include <WinSock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
void HandleError(const char* cause)
{
int32 errCode = ::WSAGetLastError();
cout << cause << " ErrorCode" << errCode << endl;
}
const int32 BUFSIZE = 1000;
struct Session
{
SOCKET socket;
char recvBuffer[BUFSIZE] = {};
int32 recvBytes = 0;
int32 sendBytes = 0;
};
int main()
{
//윈소켓 초기화(w2_32 라이브러리 초기화)
//관련정보가 wsaData에 채워짐
WSADATA wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_SOCKET)
return 0;
//논블로킹을 하기위해
u_long on = 1;
if (::ioctlsocket(listenSocket, FIONBIO, &on) == INVALID_SOCKET)
return 0;
//나의 주소 : IP주소 + PORT
SOCKADDR_IN serverAddr;//IPv4
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);//아이피 알아서 설정
serverAddr.sin_port = ::htons(7777);//PORT
//socket과 주소 연동
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
return 0;
//서버오픈
if (::listen(listenSocket, 10) == SOCKET_ERROR)//10은 대기열 이걸넘으면 못들어옴
return 0;
cout << "Accept" << endl;
//WASEventSelect = 소켓과 관련된 네트워크 이벤트 객체를 통해 감지(비동기)
//이벤트 객체 관련 함수
//생성 : WSACreateEvent( 수동리셋 Manual-Reset + Non-Signaled 상태 시작)
//삭제 : WSACloseEvent
//신호 상태 감지 : WSAWaitForMultipleEvents)
//구체적인 네트워크 이벤트 알아내기 : WSAEnumNetworkEvents
//소켓 개수 만큼 이벤트 객체 연동해야함.
//WSAEventSelect(socket, event, networkEvents);
//-관심있는 네트워크 이벤트
//FD_ACCEPT : 접속한 클라이언트가 있는지 accept호출
//FD_READ : 데이터 수신가능 recv, recvfrom
//FD_WRITE : 데이터 송신 가능 send, sendto
//FD_CONNECT : 통신을 위한 연결 절차 완료 확인
//주의사항
//WSAEventSelect함수를 호출하면 해당 소켓은 자동으로 넌블로킹 모드로 전환
//accept() 함수가 리턴하는 소켓은 listenSocket과 동일한 속성을 갖는다. 따라서 clientsocket은 FD_READ, FD_WRITE등을 다시 등록할 필요가 있다.
//드물게 WSAEWOULDBLOCK 오류가 뜰 수 있으니 예외 처리 필요
//중요)
//- 이벤트 발생 시적절한 소켓 함수 호출해야함.
//- 아니면 다음 번에는 동일 네트워크 이벤트가 발생하지 않는다.
//EX) FD_READ이벤트 떴으면 recv() 호출해야하고 안하면 FD_READ 두 번 다시 X
//WSAWaitForMultipleEvent
// 1) count, event
// 2) waitAll : 모두 기다리거나 하나만 완료되어도 빠져나간다.
// 3) timeout : 타임아웃
// 4) 지금은 false
// 5) return : 완료된 첫번째 인덱스
vector<WSAEVENT> wsaEvents;
vector<Session> sessions;
sessions.reserve(100);
WSAEVENT listenEvent = ::WSACreateEvent();//이벤트생성
wsaEvents.push_back(listenEvent);
sessions.push_back(Session{ listenSocket });//인덱스 맞추려고
if (::WSAEventSelect(listenSocket, listenEvent, FD_ACCEPT | FD_CLOSE) == SOCKET_ERROR)//이벤트와 소켓 연결
return 0;
while (true)
{
int32 index = ::WSAWaitForMultipleEvents(wsaEvents.size(), &wsaEvents[0], FALSE, WSA_INFINITE, FALSE);//3번쨰인자 하나만 완료되어도 빠져나가게 설정
if (index == WSA_WAIT_FAILED)
continue;
index -= WSA_WAIT_EVENT_0;
//::WSAResetEvent(wsaEvents[index]);//WSAEnumNetworkEvents를 사용하면 자동으로 reset해줌
WSANETWORKEVENTS networkEvents;//WSAEnumNetworkEvents의 결과물을 받을 구조체
if (::WSAEnumNetworkEvents(sessions[index].socket, wsaEvents[index], &networkEvents) == SOCKET_ERROR)//관찰하려는 이벤트
continue;
//Listener 소켓 체크
if (networkEvents.lNetworkEvents & FD_ACCEPT)
{
//Error-check
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
continue;
SOCKADDR_IN clientAddr;
int32 addrLen = sizeof(clientAddr);
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket != INVALID_SOCKET)
{
cout << "client Connected" << endl;
WSAEVENT clientEvent = ::WSACreateEvent();
wsaEvents.push_back(clientEvent);
sessions.push_back(Session{ clientSocket });
if (::WSAEventSelect(clientSocket, clientEvent, FD_READ | FD_WRITE | FD_CLOSE) == SOCKET_ERROR)//이벤트와 소켓 연결
return 0;
}
}
//Client 소켓 체크
if (networkEvents.lNetworkEvents & FD_READ || networkEvents.lNetworkEvents & FD_WRITE)
{
//Error-check
if ((networkEvents.lNetworkEvents & FD_READ) && (networkEvents.iErrorCode[FD_READ_BIT] != 0))
continue;
//Error-check
if ((networkEvents.lNetworkEvents & FD_WRITE) && (networkEvents.iErrorCode[FD_WRITE_BIT] != 0))
continue;
Session& s = sessions[index];
//read
if (s.recvBytes == 0)
{
int32 recvLen = ::recv(s.socket, s.recvBuffer, BUFSIZE, 0);
if (recvLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
{
//TODO : 오류 SESSIONS 제거
continue;
}
s.recvBytes = recvLen;
cout << "recv data = " << recvLen << endl;
}
if (s.recvBytes > s.sendBytes)
{
int32 sendLen = ::send(s.socket, &s.recvBuffer[s.sendBytes], s.recvBytes - s.sendBytes, 0);
if (sendLen == SOCKET_ERROR && ::WSAGetLastError() != WSAEWOULDBLOCK)
{
//TODO : 오류 SESSIONS 제거
continue;
}
s.sendBytes += sendLen;
if (s.recvBytes == s.sendBytes)
{
s.recvBytes = 0;
s.sendBytes = 0;
}
cout << "send data = " << sendLen << endl;
}
}
//FD_CLOSE 처리
if (networkEvents.lNetworkEvents & FD_CLOSE)
{
//TODO : SESSION 제거
}
}
::WSACleanup();
}
'VisualStudio > C++서버' 카테고리의 다른 글
[C++서버] IOCP(Completion Port) 모델 (0) | 2024.05.24 |
---|---|
[C++] Overlapped모델 (비동기 + 논블로킹) (0) | 2024.05.24 |
[C++] setsockopt와 Socket 설정 (0) | 2024.05.23 |
[C++] 블로킹방식 TCP/IP 소스 (0) | 2024.05.23 |
[C++서버] 멀티 스레드 프로그래밍 개념 총 정리 (0) | 2023.11.22 |