반응형

 

기존의 우리가 사용하던 send(), recv()함수의 문제점은 바로 블로킹(blocking)소켓이라 데이터를 받을떄까지 무한정 대기하는 문제가 있다. 물론 이걸 쓰레드로 해결하면 문제는 해결된다.

 

하지만... 몇십명의 경우엔 문제가 없겟지만, 만일 1000명이상의 유저가 접속하고 있어야 하는 서버의 경우는 쓰레드를 1000개 만들어야 하게 된다. 한명의 유저에게 하나의 쓰레드를 주게 되면 중규모 이상의 서버라면 참 비효율적인 통신모델이다. 그래서 나온모델중 하나가 select모델이다.


Select Model

 

Select 모델은 select 함수가 핵심이 되는 모델이다.

소켓모드(블로킹, 넌블로킹)에 관계없이 여러 소켓을 한 스레드로 처리 할 수 있다.

select모델의 가장 중요한 기능은 send, recv함수등의 호출시점을 알려주는 것이다.

(Select 모델이 제공하는 기능을 이용해 소켓 함수를 호출해야 할 시점을 알려줌으로써 함수 호출 시 항상 성공하도록 함.)

따라서 소켓 함수 호출 시 조건이 만족되지 않아 생기는 문제를 해결할 수 있다.

 

소켓 모드에 따른 Select 모델의 사용 효과는 다음과 같다.

  • 블로킹 소켓(blocking socket): 소켓 함수 호출 시 조건이 만족되지 않아 블로킹되는 상황을 막을 수 있다.
  • 넌블로킹 소켓(nonblocking socket): 소켓 함수 호출 시 조건이 만족되지 않아 나중에 다시 호출해야하는 상황을 막을 수 있다.

 - 동작원리

  • 세 개의 소켓 셋(Socket set)을 준비 해야 하며 소켓 셋은 소켓 디스크립터의 집합으로써 해당 소켓으로 하고자 하는 작업의 종류를 나타낸다.
    (만약 어떤 소켓의 recv() 함수를 호출해야 할 시점을 알고 싶으면 읽기 셋에 넣고, Send()함수를 호출해야할 시점을 알고 싶다면 쓰기 셋에 넣으면 된다.)

  • 소켓 셋을 준비해서 Select()함수에 전달하면 함수는 소켓 셋에 포함된 소켓에 대해 해당 작업을 할 수 있을 떄 까지 대기한다.

  • 적어도 1개의 소켓이 준비되면 Select()함수를 리턴한다. 이때 소켓 셋에는 준비된 소켓만 남고 나머지는 모두 제거된다.

 

1) 읽기[ ] 쓰기[  ] 예외(OOB)[  ] 관찰 대상 등록
OutOfBand는 send()의 마지막 인자를 MSG_OOB로 보내는 특별한 데이터
받는 쪽에서도 recv OOB 세팅을 해야 읽을 수 있다.

2) select(read, write, exception); -> 관찰 시작

3) 적어도 하나의 소켓이 준비되면 리턴한다. -> 준비되지 않은 애들은 알아서 제거됨.

4) 남은 소켓 체크해서 진행

 

 

select모델에 필요한 변수이다.

 fd_set read;
 fd_set write;
 TIMEVAL time;
fd_Set set;

FD_ZERO(&set);
FD_SET(socket, &set);
FD_CLR(socekt, &set);
FD_ISSET(fd, &set); // fd가 세트(set)되어 있으면 양수 리턴

fd_Set set;
FD_ZERO : 비우다. ex)FD_ZERO(set);
FD_SET : 소켓 s를 넣는다. ex) FD_SET(s,&set);
FD_CLR : 소켓 s를 제거 ex)FD_CLS(s,&set);
FD_ISSET : 소켓 s가 set에 들어있으면 0이 아닌 값을 리턴한다.

 


 

예제 코드

#include "pch.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <Windows.h>
#include <future>
#include "ThreadManager.h"
#include "RefCounting.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 = INVALID_SOCKET;
	char recvBuffer[BUFSIZE] = { };
	int32 recvBytes = 0;
	int32 sendBytes = 0;
};

int main()
{
	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;

	SOCKADDR_IN serverAddr;
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
	serverAddr.sin_port = ::htons(7777);

	if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == INVALID_SOCKET)
		return 0;

	if (::listen(listenSocket, SOMAXCONN) == SOCKET_ERROR)
		return 0;

	cout << "Accept" << endl;

	vector<Session> sessions;
	sessions.reserve(100);

	fd_set reads;
	fd_set writes;

	while (true)
	{
		//소켓 셋 초기화.
		//매 루프 마다 초기활르 해줘야 함.
		//왜냐하면 select를 호출한 다음 반환이 될 때 낙오자는 저절로 삭제된다.
		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);
		}

		int32 retVal = ::select(0, &reads, &writes, nullptr, nullptr);
		if (retVal == SOCKET_ERROR)
			return 0;

		//Listner 소켓 체크
		if (FD_ISSET(listenSocket, &reads))
		{
			SOCKADDR_IN 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 });
			}
		}
		
		//나머지 소켓 체크
		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();
}
반응형