본문 바로가기
CS 기초/Network

논블로킹(Non-blocking) 소켓 만들기 | ioctlsocket, select

by woohyeon 2020. 10. 22.
반응형

Winsock을 기준으로 설명합니다. 혹시나 틀린 부분이 있을 수 있으니 맹신은 마시고 있다면 알려주세요~ 

소켓 관련 함수에 타임 아웃을 지정하고 싶다면 비동기 소켓 + select 함수가 필요합니다. 해당 글을 전부 읽으시면 됩니다.


기본적으로 Winsock의 accept, connect, recv, send와 같은 대부분의 소켓 관련 함수들은 호출 스레드를  블로킹(blocking) 상태로 만듭니다. 블로킹 상태란 현재 스레드가 더 이상 코드를 진행하지 않고 block 상태로 멈춰 있는 상태를 말합니다. 블로킹 상태는 각 조건을 만족하거나 타임 아웃이 될 때까지 유지됩니다. 이해하기 어려우면 그냥 조건을 만족할 때까지 함수를 벗어나지 않고 기다린다고 생각하면 편합니다.

accept 함수는 클라이언트의 요청이 들어올 때까지 더 이상 코드를 진행하지 않고 블로킹됩니다.  connect는 서버에 연결될 때까지, recv는 데이터를 받을 때까지, send는 데이터 전송이 가능할 때까지 기다립니다. 그런데 이러한 제약은 종종 장애물이 될 수 있습니다.

억지스러운 예를 하나 들어보면, 서버에서 클라이언트의 패킷을 recv를 통해 받으려고 합니다. 현재 서버에 연결된 클라이언트는 5개입니다. 그리고 서버는 모든 클라이언트의 패킷을 적절하게 번갈아 가면서 받아야 합니다. 서버에서 작업 스레드가 클라이언트1의 데이터를 받기 위해 recv 함수를 호출했습니다. 하지만 클라이언트1은 잠수 중이라 1분 동안 패킷을 보내지 않습니다. 이 경우 서버는 1분 동안 다른 클라이언트들의 패킷을 받지 못하며 계속해서 블로킹 상태에서 기다리게 됩니다. 이는 분명히 문제가 됩니다.

만약 특정 시간 동안 또는 바로 데이터를 수신하지 못 할 경우 블로킹되지 않고 바로 다음 클라이언트의 데이터를 확인할 수 있다면 좋을 듯합니다. 이를 위해, 함수 호출에 필요한 소켓을 논블로킹 상태로 만들면 함수의 진행 현황이 어떻든 블로킹 상태에 빠지지 않도록 할 수 있습니다. 논블로킹 모드로 함수를 호출하게 되면 곧바로 -1을 반환합니다. 이는 위의 예시에서 클라이언트1이 패킷을 보내지 않더라도 기다리지 않고, 바로 recv 함수를 빠져나와 다른 클라이언트에 대해 recv 함수를 호출할 수 있다는 의미입니다. 물론 다른 클라이언트와 연결된 소켓도 논블로킹 모드여야 이와 같은 효과를 기대하겠지만요.

소켓을 논블로킹 상태로 만들기 위해선 ioctlsocket 이란 함수를 사용합니다. Input/Ouput Control socket 이라는 의미이며, 함수의 프로토타입은 다음과 같습니다.

int ioctlsocket(SOCKET sock, long cmd, u_long* argp)


sock
은 논블로킹 모드로 설정할 소켓을 의미합니다. cmd는 소켓에 대해 수행할 동작을 의미하는 커맨드인데, FIONBIO로 설정하면 됩니다. argp는 cmd가 수행할 동작에 대한 판별 값입니다. 이 값이 0이면 sock을 블로킹 모드로 설정하며, 0이 아닌 값이면 논블로킹 모드로 설정합니다.

다음과 같이 소켓을 논블로킹 모드로 변환 및 블로킹 모드로 복구할 수 있습니다.

{
  ...
  u_long blockingMode = 0;
  u_long nonBlockingMode = 1;

  ioctlsocket(sock, FIONBIO, &nonBlockingMode); // sock을 논블로킹 모드로 설정
  recv(sock, buf, len, 0); 			// 패킷을 수신하든 안하든 바로 리턴

  ioctlsocket(sock, FIONBIO, &blockingMode);    // sock을 블로킹 모드로 설정
  recv(sock, buf, len, 0); 		        // 패킷을 수신할 때까지 블로킹
  ...
}

 

그러나 논블로킹 모드로는 함수의 결과를 예상할 수 없습니다. 예를 들어 위 코드에서 블로킹 모드 소켓을 통해 recv를 호출한 경우, buf를 사용할 시점엔 패킷을 이미 수신했을 것이기 때문에 걱정없이 buf에 담긴 데이터를 가져다 쓸 수 있습니다. (recv 예외 처리 후) 그러나 논블로킹 모드는 패킷 수신 여부와 관계없이 바로 리턴하기 때문에 buf의 상태가 어떤지 알 수 없습니다. 이처럼 논블로킹 모드로 소켓을 사용했을 땐 이 소켓이 Read/Write 할 수 있는 상태인지 아닌지 판별해 주는 select 함수가 필요합니다.

select 함수의 프로토타입은 다음과 같습니다.

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const timeval* timeout)

: select 함수는 사용 가능 여부를 판단하고 싶은 소켓들의 집합을 인자로 받습니다. 그리고 함수가 종료되면 인자로 받은 소켓들의 집합엔 사용 가능한 소켓들만 남게 됩니다. 그리고 총 사용 가능한 소켓의 개수를 반환합니다.

nfds는 리눅스 환경에서 사용되는 변수이며, 윈도우즈 환경에선 아무 역할을 하지 않으므로 무시합니다. 0을 전달합니다.

readfds는 read가 가능한 상태인지 확인하고 싶은 소켓들의 집합입니다. 1개부터 여러개의 소켓의 집합을 전달할 수 있습니다. 함수가 종료되면 readfds엔 패킷을 정상적으로 수신한 소켓들만 남아 있습니다. 해당 집합에서 제외된 소켓은 select 함수 리턴 시까지 패킷을 아직 받지 않은 소켓들입니다. 만약 read와 관련된 소켓은 검사하고 싶지 않다면 nullptr를 전달합니다. 

writefds는 write가 가능한 상태인지 확인하고 싶은 소켓들의 집합입니다. 1개부터 여러개의 소켓의 집합을 전달할 수 있습니다. 함수가 종료되면 writefds엔 쓰기 작업이 가능한 소켓들만 남아 있습니다. 해당 집합에서 제외된 소켓은 write이 불가능한 소켓들입니다. 예를 들어 클라이언트는 connect 함수에 사용된 소켓을 통해 서버에 패킷을 전달합니다. 만약 connect 함수에 사용한 논블로킹 소켓을 select 함수에 전달했을 때 남아 있다면 서버에 정상적으로 연결된 소켓이며, 남아 있지 않다면 정상적으로 연결되지 않은 소켓입니다. readfds와 마찬가지로 검사를 원하지 않을 경우 nullptr를 전달합니다.

exceptfds는 에러 발생 여부를 확인하고 싶은 소켓들의 집합입니다. select 함수 리턴 후 남아 있는 소켓들은 에러가 발생한 소켓들입니다. nullptr를 넘기면 검사하지 않습니다. 세 개의 소켓 집합 중 적어도 하나 이상은 nullptr가 아니여야 합니다.

timeout은 최대 waiting 시간입니다. select 함수는 기본적으로 전달 받은 소켓 중 최소 1개의 사용 가능한 소켓을 찾을 때까지 리턴하지 않습니다. 만약 시간을 지정하면 사용 가능한 소켓이 없더라도 시간 초과로 0을 리턴합니다. 이 경우 사용 가능한 소켓은 없기 때문에 모든 집합은 비어 있는 상태가 됩니다. timeval 구조체 타입은 second(초)와 microsecond(10^-6초) 두 멤버를 가집니다. 즉 초 또는 마이크로초 단위로 설정 가능합니다. 이 옵션에 nullptr를 주면 최소 1개의 사용 가능한 소켓을 찾을 때까지 무한히 기다립니다. 

소켓들은 모두 fd_set* 타입으로 전달해야 합니다. 소켓을 fd_set 타입에 쉽게 저장하기 위해 매크로가 제공됩니다. 다음은 매크로를 사용하여 fe_set을 생성하고 select 함수를 사용하는 간단한 예제입니다.

// fe_set 타입 변수 선언
fd_set readSet;
fd_set writeSet;

// 초기화 
FD_ZERO(&readSet);
FD_ZERO(&writeSet);

// 집합에 소켓 추가
FD_SET(sockForRead[0], &readSet);
FD_SET(sockForRead[1], &readSet);
...

FD_SET(sockForWrite[0], &writeSet);
FD_SET(sockForWrite[1], &writeSet);
...

// 시간 제한 2초 설정
// 즉시 리턴하고 싶다면 {0, 0} 전달
timeval tval {2, 0};

// 사용 가능한 총 소켓의 수를 반환합니다.
// 즉시 사용 가능한 소켓이 없을 경우 최대 2초 동안 기다립니다.(블로킹)
// 만약 각 집합에 소켓이 2개씩 있고 모두 사용 가능하다면
// 4를 반환합니다.
int totalSocketCount = select(0, &readSet, &writeSet, nullptr, &tval);

if(totalSocketCount > 0)
{
	// 최소 1개의 사용 가능한 소켓이 존재
	// FD_ISSET(sockForRead[1], &readSet): readSet에 sockForRead[1]이 존재하는지 확인
}
else
{
	// 타임 아웃 또는 사용 가능한 소켓이 존재하지 않음
}

 

select 함수에 대한 더 자세한 내용은 다음을 참고하세요.

docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select

 




댓글