Select 모델
Select 모델을 사용하면 소켓모드(블로킹, 넌블로킹)에 관계없이 여러 소켓을 한 스레드로 처리하는 모델이다.
Select 모델 동작원리
소켓 함수를 호출해야 할 시점을 알려줌으로써 함수 호출 시 항상 성공하도록 하는 것이다.
효과는 다음과 같다.
* 블로킹 소켓 : 소켓 함수 호출 시 조건이 만족되지 않아 블로킹되는 상황을 막을 수 있다.
* 넌블로킹 소켓 : 소켓 함수 호출 시 조건이 만족되지 않아 다시 호출해야 하는 상황을 막을 수 있다.
3가지의 소켓 셋을 준비한다.
* 읽기 셋
클라이언트가 접속했으므로 accept() 함수를 호출한다.
데이터를 받았으므로 recv(), recvfrom()등의 함수를 호출한다.
연결이 종료되었으므로 recv(), recvfrom()등의 함수를 호출한다. 이때 리턴 값은 0 or SOCKET_ERROR이다.
* 쓰기 셋
송신 버퍼에 데이터를 쓰고, send(), sendto()등의 함수를 호출하여 데이터를 보낼 수 있다.
넌블로킹 소켓을 사용한 connect()함수 호출이 성공하였음을 확인할 수 있다.
* 예외 셋
OOB(Out-Of-Band) 데이터가 도착했으므로, recv(), recvfrom()등의 함수를 호출하여 OOB 데이터를 받을 수 있다.
넌블로킹 소켓을 사용한 connect()함수 호출이 실패하였음을 확인할 수 있다.
OOB(Out-Of-Band) 데이터란 send() 함수의 마지막 인자로 MSG_OOB를 사용하여 보내는 데이터를 말한다. 이 경우 recv()함수의 마지막 인자 MSG_OOB를 사용해야만 데이터를 수신 할 수 있다. 원래 OOB 데이터는 긴급한 상황을 알리는 용도로 사용하는데, TCP/IP에서는 1바이트 이상을 보낼 수 없다는 특징때문에 잘 사용하지 않는다.
Select() 함수는
int Select(
int nfds, // 유닉스와 호환성을 위해 존재하며, 윈도우는 사용하지 않는다.
fd_set* readfds, // 읽기 셋
fd_set* writefds, // 쓰기 셋
fd_set* exceptfds, // 예외 셋
const struct timeval* timeout // 초와 마이크로초 단위로 타임아웃을 나타낸다.
// NULL : 적어도 한개 소켓이 조건을 만족 할 때까지 무한히 기다린다. 리턴값은 조건을 만족하는 소켓의 개수가 된다.
// {0, 0} : 소켓 셋에 포함된 모든 소켓을 검사한 수 곧바로 리턴한다.
// 양수 : 적어도 한 소켓이 조건을 만족할 때까지 기다리되, 타임아웃으로 지정한 시간이 지나면 리턴한다.
);
절차는 다음과 같다.
1. 소켓 셋을 비운다.
2. 소켓 셋에 소켓을 넣는다. 최대 넣을 수 있는 개수는 FD_SETSIZE(64)로 정의되어 있다.
3. select() 함수를 호출한다. select() 함수는 블로킹 함수로 동작
4. select() 함수가 리턴한 후 소켓 셋에 남아있는 모든 소켓에 대해 적절한 소켓 함수를 호출하여 처리한다.
5. 1 - 4과정을 반복한다.
소켓 셋을 다루기 위한 매크로
FD_CLR(SOCKET s, fd_set* set) : 셋에서 소켓 s를 제거한다.
FD_ISSET(SOCKET s, fd_set* set) : 소켓 s가 셋에 들어있으면 0이 아닌 값을 리턴, 그렇지 않으면 0을 리턴한다.
FD_SET(SOCKET s, fd_set* set) : 셋에 소켓 s를 넣는다.
FD_ZERO(fd_set* set) : 셋을 비운다.
예제
#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>
#define MAX_BUFFER_SIZE 512
// 소켓 정보 저장을 위한 구조체
typedef struct
{
SOCKET sock;
char buffer[MAX_BUFFER_SIZE + 1];
int recvBytes;
int sendBytes;
} SOCKET_INFO;
int g_nTotalSockets = 0;
SOCKET_INFO* g_aSocketInfoArray[FD_SETSIZE];
// 소켓 관리 함수
BOOL AddSocketInfo(SOCKET clientSocket);
void RemoveSocketInfo(int nIndex);
// 오류 출력 함수
void err_quit(char* msg);
void err_display(char* msg);
int _tmain(int argc, _TCHAR* argv[])
{
int retValue;
// 윈속 초기화
WSADATA wsa;
if(WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return -1;
// socket()
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, 0);
if(listenSocket == INVALID_SOCKET)
err_quit("socket()");
// bind()
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(5001);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
retValue = bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
if(retValue == SOCKET_ERROR)
err_quit("bind()");
//listen()
retValue = listen(listenSocket, SOMAXCONN);
if(retValue == SOCKET_ERROR)
err_quit("listen()");
// 넌블로킹 소켓으로 전환
unsigned long on = TRUE;
retValue = ioctlsocket(listenSocket, FIONBIO, &on);
if(retValue == SOCKET_ERROR)
err_display("ioctlsocket()");
// 통신에 사용할 변수
FD_SET rset;
FD_SET wset;
SOCKET clientSocket;
SOCKADDR_IN clientAddr;
int nAddrLength;
while(1)
{
// 소켓 셋 초기화
FD_ZERO(&rset);
FD_ZERO(&wset);
// 소켓 셋 지정
FD_SET(listenSocket, &rset);
for(int i = 0; i < g_nTotalSockets; ++i)
{
if(g_aSocketInfoArray[i]->recvBytes > g_aSocketInfoArray[i]->sendBytes)
FD_SET(g_aSocketInfoArray[i]->sock, &wset);
else
FD_SET(g_aSocketInfoArray[i]->sock, &rset);
}
// select()
retValue = select(0, &rset, &wset, NULL, NULL);
if(retValue == SOCKET_ERROR)
err_quit("select()");
// 소켓 셋 검사 #1 : 클라이언트 접속
if(FD_ISSET(listenSocket, &rset))
{
nAddrLength = sizeof(clientAddr);
clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &nAddrLength);
if(clientSocket == INVALID_SOCKET)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
err_display("accept()");
}
else
{
printf("[TCP 서버] 클라이언트 접속: IP 주소 = %s, 포트번호 = %d\n",
inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
if(AddSocketInfo(clientSocket) == FALSE)
{
printf("[TCP 서버] 클라이언트 접속을 해제 합니다n");
closesocket(clientSocket);
}
}
}
// 소켓 셋 검사 #2 : 데이터 통신
for(int i = 0; i < g_nTotalSockets; ++i)
{
SOCKET_INFO* pInfo = g_aSocketInfoArray[i];
if(FD_ISSET(pInfo->sock, &rset))
{
// 데이터받기
retValue = recv(pInfo->sock, pInfo->buffer, MAX_BUFFER_SIZE, 0);
if(retValue == SOCKET_ERROR)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
err_display("recv()");
RemoveSocketInfo(i);
continue;
}
}
else if(retValue == 0)
{
RemoveSocketInfo(i);
continue;
}
else
{
pInfo->recvBytes = retValue;
// 받은 데이터 출력
SOCKADDR_IN sockAddr;
int nAddrLength = sizeof(sockAddr);
getpeername(pInfo->sock, (SOCKADDR*)&sockAddr, &nAddrLength);
pInfo->buffer[retValue] = '\0';
printf("[TCP/%s:%d] %s\n",
inet_ntoa(sockAddr.sin_addr), ntohs(sockAddr.sin_port), pInfo->buffer);
}
}
if(FD_ISSET(pInfo->sock, &wset))
{
// 데이터보내기
retValue = send(pInfo->sock, pInfo->buffer + pInfo->sendBytes,
pInfo->recvBytes - pInfo->sendBytes, 0);
if(retValue == SOCKET_ERROR)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
err_display("send()");
RemoveSocketInfo(i);
continue;
}
}
pInfo->sendBytes += retValue;
if(pInfo->recvBytes == pInfo->sendBytes)
pInfo->recvBytes = pInfo->sendBytes = 0;
}
}
}
return 0;
}
BOOL AddSocketInfo(SOCKET clientSocket)
{
if(g_nTotalSockets >= (FD_SETSIZE - 1))
{
printf("[오류] 소켓 정보를 추가할 수 없습니다.\n");
return FALSE;
}
SOCKET_INFO* pSocketInfo = new SOCKET_INFO;
if(pSocketInfo == NULL)
{
printf("[오류] 메모리가 부족합니다.\n");
return FALSE;
}
pSocketInfo->sock = clientSocket;
pSocketInfo->recvBytes = 0;
pSocketInfo->sendBytes = 0;
g_aSocketInfoArray[g_nTotalSockets++] = pSocketInfo;
return TRUE;
}
void RemoveSocketInfo(int nIndex)
{
SOCKET_INFO* pInfo = g_aSocketInfoArray[nIndex];
// 클라이언트 정보 얻기
SOCKADDR_IN socketAddr;
int nAddrLength = sizeof(socketAddr);
getpeername(pInfo->sock, (SOCKADDR*)&socketAddr, &nAddrLength);
printf("[TCP 서버] 클라이언트 종료: IP 주소 = %s, 포트번호 = %d\n",
inet_ntoa(socketAddr.sin_addr), ntohs(socketAddr.sin_port));
closesocket(pInfo->sock);
delete pInfo;
for(int i = nIndex; i < g_nTotalSockets; ++i)
g_aSocketInfoArray[i] = g_aSocketInfoArray[i + 1];
g_nTotalSockets--;
}
void err_quit(char* msg)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
LocalFree(lpMsgBuf);
exit(-1);
}
void err_display(char* msg) [출처] 소켓의 입출력 모델(Select - 배열사용) (정문수 개인 카페) |작성자 유빈아빠
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
printf("[%s] %s\n", msg, (LPCTSTR)lpMsgBuf);
LocalFree(lpMsgBuf);
}
댓글 없음:
댓글 쓰기