1) Select I/O 모델
먼저 앞선 포스팅에서 멀티플렉싱 I/O모델을 알아보았다.
멀티플렉싱 구조의 서버는 다수의 클라이언트의 입출력 정보를 한꺼번에 묶어서 검사하고 이에 따른 처리를 진행하는 모델인데, 이 역할을 select() 함수가 지원해준다.
select함수는 다수의 소켓으로부터 읽기/쓰기/예외사항 별 이벤트를 감지하는 역할을 수행한다.

 


Windows에서 select()함수의 구조를 먼저 알아보자

fd_set read_fds, write_fds, except_fds;
timeval timeout;

select(0, &read_fds, &write_fds, &except_fds, &timeout);

※ read_fds / write_fds / except_fds의 인자가 오는 자리에 대신 NULL을 기입할 수 있으며, NULL을 기입할 경우 해당 이벤트는 감시하지 않는다.

- 1번째 인자:  BSD socket과의 호환을 위해 형식적으로 남겨둔 인자다. Winsock에서는 사용하지는 않는 값이다.
  (Linux에서는 해당 값을 입력하면, fd배열에서 해당 인자의 값까지의 갯수만 체크한다)
- 2번째 인자: 읽기 작업이 필요한 소켓이 있는지 검사한다. 해당 소켓으로부터 클라이언트가 연결되어 accept가 필요하거나, 클라이언트로부터 데이터가 도착하여 읽기 작업이 필요한 경우 읽기 관련 작업이 필요하다는 이벤트가 발생한다.
- 3번째 인자: 쓰기가 가능한 소켓이 있는지 검사한다. 클라이언트 소켓이 connect() 함수호출을 완료하고 데이터를 보낼 수 있을때나, send()함수의 작업을 완료하고 다시 데이터를 송신할 수 있는 상태가 되면 이벤트가 발생된다.
- 4번째 인자: 예외사항이 발생한 소켓이 있는지 검사한다.
- 5번째 인자: select 함수가 커널에 머물며 체크를 하는 최대시간을 지정한다. 만일 NULL로 설정하면 결과값을 반환하기 전까지 서버는 blocking 상태가 된다.
- 반환값: select 함수에서 이벤트가 발생하여 작업을 대기중인 소켓들의 갯수를 반환한다. 만일 timeout이 되었는데 감지된 이벤트가 없다면 0을 반환한다. select함수 처리도중 오류가 발생하였다면 SOCKET_ERROR를 반환한다.

[출처] https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select

 


2) 파일 디스크립터(FD, File Descriptor) 란? 
 select함수에 fd_set이라는 구조체가 객체의 포인터가 인자로 온다. 이것은 파일 디스크립터(fd)들을 저장한 객체인데, select 서버 전체의 코드를 살펴보기 전에 파일 디스크립터가 무엇인지 알아보도록 하자.
 Windows Socket은 Berkeley 소프트웨어 배포 (BSD, 릴리스 4.3)의 UNIX 소켓 구현을 기반하여 구현되었으며, BSD Unix와의 호환을 위해 Unix/Linux의 소켓함수와 유사한 구조를 가지는 것이 많다.
 Unix/Linux는 소켓을 file단위로 취급한다(Unix/Linux 하드웨어를 포함한 시스템의 모든것을 파일단위로 관리한다). 여기서 프로세스가 작업을 수행하기 위해 파일을 참조해야하는데, 이때 각 파일을 구분하게 해주는 값이 파일 디스크립터이다. 파일을 open()/read()/write() 등의 작업을 할때 인자로 이 파일 디스크립터 정보를 넘겨주며 작업을 진행한다.
 Unix/Linux에서는 각 파일의 디스크립터를 음이 아닌 정수값으로 구성되어있다. (음수는 NULL혹은 오류코드를 기술하는데 사용된다). 여기서 0,1,2은 표준 입출력을 위해 예약된 정수여서 파일이 생성되었을때 파일 디스크립터는 3부터 가장 작은 숫자에서 다른 파일이 사용하지 않은 숫자를 할당한다.

※ 표준 입출력을 나타내는 파일 디스크립터

  • 표준 입력(Standard Input): 0
  • 표준 출력(Standard Output): 1
  • 표준 오류(Standard Error): 2

 

3) Unix/Linux의 fd_set 구조체
 여러개의 파일 디스크립터의 입출력 정보를 묶은 구조가 fd_set이다. 
 Unix/Linux에서 fd_set은 비트값을 통하여 각 파일의 상태를 나타낸다. 그 이유는 만일 fd가 가지는 고유한 정수값을 그대로 저장하게 된다면 한 파일의 정보당 4byte의 크기를 저장하게 된다. 하지만 bit값으로 구성된 배열에 각 파일의 fd값을 인덱스로 정보를 나타낸다면, 한 파일당 1bit의 크기만으로 표현할 수 있어서 저장공간이 절약된다.

초기의 fd_set
fd=3인 파일의 상태변화가 있을때의 fd_set

 

4) Windows의 fd_set 구조체 
 Unix/Linux의 구조와 호환을위해 fd_set 구조체를 select 함수의 인자로 넘겨주고 있지만, 실제로 Windows는 Unix와는 다른 형식으로 소켓을 취급한다. Windows는 소켓을 파일이 아닌 커널 객체로 취급하며 handle을 통해 다룬다. 실제 Windows의 fd_set 구조체는 이 소켓 핸들을 가르키는 포인터 배열과 배열의 크기정보를 가지고있다. 이 정보들은 BSD Unix와의 호환을 위해 unsigned int형태로 재관리된다. 

※ 핸들(handle) : 스레드,파일,그래픽과 같은 시스템 자원을 응용 프로그램이 직접 접근할 수 없다. 따라서 이러한 시스템 자원들을 접근할 수 있도록 돕는것이 핸들이다.

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;


4) fd_set 매크로
fd_set의 데이터를 변경하거나 체크하기 위한 매크로 함수가 존재한다. 

  • FD_ZERO(*set) : fd_set을 초기화한다. fd_set사용하기전에 반드시 해야하는 작업이다.
  • FD_CLR(s, *set) : fd_set에서 첫번째 인자에 기입된 소켓s를 뺀다.
  • FD_ISSET(s, *set) :  소켓 s가 fd_set의 멤버인지 확인한다. 맞다면 true를 반환한다.
  • FD_SET(s, *set) : 소켓 s를 fd_set에 추가한다.

해당 매크로를 통하여 fd_set에 있는 상태를 조회/수정하거나 초기화할 수 있다.

 

5) Select Echo Server 코드

#pragma comment(lib, "ws2_32")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <stdio.h>
#include <winsock2.h>
#include <memory>
#define MAX_LEN 1024 
#define BACKLOG 5

int main()
{
	WSADATA wsaData;

	// 서버 
	SOCKET serverSocket; 
	struct sockaddr_in listen_addr, accept_addr;
	char buf[MAX_LEN];
	fd_set temp_fds, read_fds;

	int readn, addr_len;
	unsigned int i, fd_num = 0;

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 1;

	serverSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (serverSocket == INVALID_SOCKET)
		return 1;

	memset((void*)&listen_addr, 0x00, sizeof(listen_addr));

	listen_addr.sin_family = AF_INET;
	listen_addr.sin_port = htons(11000);
	listen_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	if (bind(serverSocket, (struct sockaddr*)&listen_addr, sizeof(listen_addr)) == SOCKET_ERROR)
		return 1;

	if (listen(serverSocket, BACKLOG) == SOCKET_ERROR)
		return 1;

	FD_ZERO(&read_fds);
	FD_SET(serverSocket, &read_fds);
	printf("클라이언트로부터의 접속을 대기중입니다\n");

	while (1)
	{
		//select 함수가 호출완료되면, 변화한 fd정보 이외의 것은 초기화된다.
		//따라서 기존의 fd_set 데이터를 temp_fds에 저장해야한다. (최초에는 빈 fd_set값이 저장된다)
		 temp_fds = read_fds;

		// 별다른 타임아웃이 없으므로, select 함수로 입력이 오기전까지 Blocking되어 대기한다.
		fd_num = select(0, &temp_fds, NULL, NULL, NULL);

		for (i = 0; i <= temp_fds.fd_count; i++)
		{
			SOCKET currentSocket = temp_fds.fd_array[i];

			if (FD_ISSET(currentSocket, &temp_fds))
			{
				// 새로 연결이 들어온 경우
				if (currentSocket == serverSocket )
				{
					addr_len = sizeof(struct sockaddr_in);
					printf("새로운 소켓의 연결이 왔습니다\n");
	
					SOCKET clientSocket = accept(serverSocket, (struct sockaddr*)&accept_addr, &addr_len);
					if (clientSocket == INVALID_SOCKET)
					{
						printf("잘못된 소켓입니다\n");
						continue;
					}
					// 연결온 클라이언트 소켓정보를 read_fds에 추가
					FD_SET(clientSocket, &read_fds);

				} else { //기존에 이미 연결된 소켓으로부터 메시지를 받을때
					memset(buf, 0x00, MAX_LEN);
					readn = recv(currentSocket, buf, MAX_LEN, 0);
					if (readn <= 0)
					{
						printf("클라이언트와의 연결이 종료되었습니다\n");
						closesocket(currentSocket);

						// 연결이 끊긴 소켓을 read_fds에서 제외
						FD_CLR(currentSocket, &read_fds);
					}
					else
					{
						printf("%s 메시지 수신함\n", buf);
						send(currentSocket, buf, readn, 0);
					}
				}

				if (--fd_num <= 0) break;
			}
		}
	}
	closesocket(serverSocket);
	WSACleanup();
	return 0;
}

 

 

6) Select Server 모델의 특징과 장단점
 Select Server는 블록킹(Blocking)방식의 비동기(Asynchronous)식 통신을 가진다는 특징이 있다.

  • 블록킹(Blocking) 방식: 요청한 작업이 완료되어 응답을 받을 때 까지 대기하는 방식
  • 비동기적(Asynchronous) 방식: 요청된 순서대로 작업의 처리순서를 보장하지 않는 방식

 따라서 SelectServer는 Select요청을 통해 응답을 받을때까진 블록킹이 된 채로 기다린다. 이후 Select서버에서 응답을 기다리는동안 변화한 파일들의 정보가 있다면, 한꺼번에 받아들여 처리한다. 때문에 클라이언트에서는 요청한 순서대로 응답을 보낸다는 보장이 없다는 특징을 가진다.

 Select Server는 select()함수를 통해 하나의 프로세스로 여러개의 파일(네트워크에서는 클라이언트 소켓)의 상태를 조회하며 처리할 수 있다. 따라서 싱글 스레드로 동시성을 보장한다.
 하지만 fd_set구조체에 저장된 파일디스크립터들의 상태를 하나하나 체크해야하므로, 수많은 파일(소켓)들을 대상으로 진행할때에는 성능이 저하된다는 단점이 있다.

 이번 포스팅을 통해 하나의 I/O Model에 대해 알아보았다.
 입출력모델은 Select Server외에도 Overlapped I/O 모델, IOCP 모델들이 존재하는데 이후 포스팅에서 다뤄볼 예정이다. 

 

+ Recent posts