이전 포스팅에서는 블로킹과 논블로킹, 동기와 비동기형 모델의 각각 특징에 대해서 살펴보았다.
이번 포스팅은 이 개념들이 조합된 모델에 대해서 다뤄본다.

[출처] https://developer.ibm.com/articles/l-async/

위의 이미지는 ibm 문서에서 정의된 유명한 2x2매트릭스이다. 여기서 각 개념들이 조합된 I/O모델의 예시를 알아보자

1) 동기 블로킹 I/O (Sync Blocking I/O)

 동기 블로킹 방식은 I/O 호출을 하면, 응용프로그램은 멈추며, I/O를 요청한 함수가 완료될 때까지 스레드는 아무런 작업을 하지않고 기다린다. 작업이 완료되면 이후 정의된 작업을 순차적으로 진행하게 된다.
winsock에서 기본적으로 제공하는 send(), recv() 함수가 이러한 모델들이다.
아래는 동기 블로킹 소켓모델에 대한 예시이다. 이전에 구현했던 클라이언트에서 단 한번만 send()/recv()작업을 하는 코드로 변경하였다.

int main() {
	WSAData wsaData;
	SOCKET clientSocket;

    // ... 중략 ... //
    // 중략된 내용은 이전 포스팅(https://dodo000.tistory.com/29)의 클라이언트 코드와 동일합니다. 
	clientSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (clientSocket == INVALID_SOCKET) {
		printf("윈도우소켓 초기화 실패");
		return 0;
	}
	if (connect(clientSocket, (sockaddr*)&servAddr, sizeof(servAddr)) != 0) {
		printf("connect() 오류 :%d\n", WSAGetLastError());
		return 0;
	}

    // ... 중략 ... //

	if (send(clientSocket, input, sizeof(input), 0) == SOCKET_ERROR) {
		printf("send() 오류 : %d\n", WSAGetLastError());
		return 0;
	}
	printf("send 함수가 완료된 후, 다음 작업이 실행됩니다.\n");

	if (recv(clientSocket, received, BUF_SIZE, 0) == SOCKET_ERROR) {
		printf("recv() 오류 : %d\n", WSAGetLastError());
		return 0;
	}

	printf("recv 함수가 완료된 후, 다음 작업이 실행됩니다.\n");
	printf("[message from server] %s \n", received);

	return 0;
}

send()함수 이후 printf작업이 진행되며, recv()함수가 완전히 실행된 이후 다음 작업들이 실행된다.
동기-블로킹 모델은 구현에 있어서 간편하다는 장점이 있지만, 한 소켓의 send()/recv() 작업동안은 아무것도 할 수 없기에
다수의 클라이언트의 동시요청을 서비스하기 어렵다.

 

2) 동기 논블로킹I/O (Synchronous Non blocking I/O)


 동기-논블로킹 방식은 I/O 함수가 호출되자마자 전송/수신이 완료여부와 상관없이 반환한다. 이때 send나 recv 작업이 완료되지 않았다면 반환되는 응답값은 WSAEWOULDBLOCK라는 에러코드이다.
 하지만 동기방식을 택하게 되면, 응용프로그램은 입출력 작업에 뒤이어서 오는 작업을 수행하기전에 send()/recv() 작업이 온전하게 데이터를 받을때까지 체크를 해야한다.
 그래서 여러번 소켓의 함수를 호출하여 체크를 하고 완전히 입출력이 완료되었을 때, 다음 작업을 수행하는 방식이다.

int main() {
	WSAData wsaData;
	SOCKET clientSocket;
    // ... 중략 ... //
	clientSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (connect(clientSocket, (sockaddr*)&servAddr, sizeof(servAddr)) != 0) {
		printf("connect() 오류 :%d\n", WSAGetLastError());
		return 0;
	}
    // ... 중략 ... //
	u_long mode = 1;
	//소켓을 논블록모드로 지정한다.
	ioctlsocket(clientSocket, FIONBIO, &mode);

	cin.getline(input, BUF_SIZE);
	if (send(clientSocket, input, sizeof(input), 0) == SOCKET_ERROR) {
		printf("send() 오류 : %d]\n", WSAGetLastError());
		return 0;
	}
	printf("send 함수가 완료된 후, 다음 작업이 실행됩니다.\n");

	while (1) {
		// recv가 완료되지 않으면 호출 즉시 WSAEWOULDBLOCK에러를 반환한다.
		// 이후 recv를 완료하면 SOCKET_ERROR가 발생하지 않으므로, 성공할때까지 루프를 돌며 체크한다.
		if (recv(clientSocket, received, BUF_SIZE, 0) != SOCKET_ERROR) {
			break;
		}
		printf("socket error: %d\n", WSAGetLastError());
		if (WSAGetLastError() != WSAEWOULDBLOCK) {
			printf("recv() 오류 : %d]\n", WSAGetLastError());
			return 0;
		}
	}
	printf("[message from server] %s \n", received);

	return 0;
}

 여기서 ioctlsocket 함수는 소켓의 모드를 설정할 수 있는 함수이다.
FIONBIO는 소켓의 블로킹/논블로킹 여부를 지정하겠다는 옵션이며, mode 변수로 온 값이 0이면 블로킹, 0이 아니면 논블로킹 모드로 세팅된다. 

해당 소스코드를 실행한 결과이다.

 사실 이 방식은 꽤 비효율적일 수 있다. 논블로킹으로 소켓이 바로 반환되지만 순차적으로 작업들을 처리하기 위해 계속해서 recv()함수의 결과를 확인해서 다음 작업을 진행하지 못하고 있다.
 동기-블로킹 방식의 경우 입출력 작업을 위해 한 스레드가 blocking될 경우, CPU는 다른 스레드에게 제어권을 넘기면서 병렬처리를 할 수 있다. 하지만 동기-논블로킹은 계속해서 CPU가 해당 스레드를 실행하면서 입출력 작업을 확인하므로 busy-wait라는 자원낭비가 된다.

 

3) 비동기 블로킹 I/O(Asynchronous Blocking I/O)

 비동기 블로킹 방식은 각각의 소켓들의 입출력이 완료되지 않아도 다음 작업을 실행할 수 있지만, 이후 블로킹 모드로 작동하는 select 함수를 통해 하나 이상의 소켓에서 입출력 처리가 완료되기 전까지 응용프로그램의 작동이 잠깐 멈추는 방식이다.
  ※ select 입출력 모델에 대해서는 이전포스팅에서 다루었다

 select 함수를 통해 다수의 소켓들을 묶어서 처리하기 때문에, 단일 스레드에서 다수의 클라이언트를 실시간 서비스할 수 있는 장점이 있다.
 하지만 select함수가 완료될 동안 해당 스레드는 아무런 작업을 할 수 없으며, select 함수 내부적으로 각각 소켓(리눅스에서는 파일디렉토리)들을 하나하나 순회하며 체크하기 때문에 C10K문제(10,000명의 동시접속자의 요청을 처리하는 문제)에서는 효율이 떨어지는 한계를 보인다. 

 

4) 비동기 논블로킹 I/O(Asynchronous Non blocking I/O)

 비동기 논블로킹 모델은 각 소켓들이 입출력 처리가 완료되지 않아도 바로 반환되며, 스레드는 다른 작업들을 진행한다. 이후 특정 시점에서 입출력 작업이 완료되었을 때, 이를 통지받고 이에 따른 작업을 진행한다.
 효율적으로 다른 프로세스들을 처리할 수 있고 여러 클라이언트에서 동시다발적으로 오는 요청을 처리하기에 용이하다. 하지만 구현이 보다 복잡해진다.
 이 모델의 예시로는 Overlapped I/O와 IOCP 모델이 존재한다. 이 모델을 채택한 서버에 대해서는 다음 포스팅들에서 다룰 예정이다. 

 XCode에서 Swift UI를 생성하면 다음과 같은 초기 코드가 존재한다.
App, Scene, View가 어떤 역할을 하는지 살펴보도록 한다.

@main
struct MyApp: App {
    var body: some Scene {
       ContentView()
    }
}

 

1. @main
앱을 시작하였을때 진입점을 명시해준다. 이 프로토콜은 앱을 시작하면 main()함수를 호출하게 해준다.
이 main은 모든 앱의 파일중에서 단 하나만 존재해야한다.

2. App
어플리케이션의 구조와 동작을 나타내는 프로토콜. 
App의 작동을 정의하는 body 변수를 필수로 가져야한다. body는 하나 이상의 Scene에 의해서 구성되는 인스턴스이다. app은 런타임동안 body에 가지고 있는 Scene들을 표현한다.

 - App의 life cycle 관리 : iOS 앱에 있어서 life cycle(생명주기)이란 어플리케이션의 활성/비활성 및 Foreground(화면에 띄워짐)/Background(화면에 띄워지지않은 상태에서 실행되고있음), 시스템이 발생시키는 이벤트에 의해 App의 상태가 전환되는 과정을 말한다.
 iOS14 이전의 버전에서는 App Delegate와 SceneDelegate를 통해 life cycle을 관리하였다.
 (과거 버전에서는 App의 생명주기만 존재했지만, 이후 Scene개념이 도입되면서 Scene만의 생명주기도 추가되었다)
  Delegate 클래스는 생명주기 관리외에도 코드 Clean-up, 상태 변경, 이벤트에 필수적인 작업들을 진행하였다.
 하지만 iOS 14부터의 Swift UI버전에서는 이런 요소들이 App인스턴스가 자동적으로 관리하며, 작업자는 진입점(@main)만 정의하면 된다.

※ App Delegate와 SceneDelegate 최신버전에서는 필수적이지 않기에, 프로젝트를 생성해도 기본적으로 내장되어 있지 않다. 다만 아직도 일부 SDK기능등을 이용하기 위해선 필요하여 수동적으로 도입한다.

 

3. Scene
Scene은 어플리케이션에 그려지는 화면을 표현하는 역할을 담당하는 인스턴스. 각각의 UI요소를 담당하는 View들의 계층구조에서 뿌리 역할을 담당한다.
- 계층구조



- Scene Phase: Scene을 활성·비활성·백그라운드 실행 상태를 나타내는 값이다. 이 값이 변경될 때 작업들을 정의하여 Scene의 life cycle을 관리할 수 있다.

@main
struct MyApp: App {
	// 백슬래쉬(\)는 변수가 아닌, property값을 나타낸다는 의미이다. keypath라고도 한다.
	@Environment(\.scenePhase) private var scenePhase
    var body: some Scene {
        WindowGroup {
            Text("Hello, world!")
        }
    }
	.onChange(of: scenePhase) {(newScenePhase) in
		switch newScenePhase {
        	case .active:
         		// 활성 상태가 되었을때 작동 정의
		 	case .inactive:
         		// 비활성 상태가 되었을때 작동 정의
			case .background:
            	// 백그라운드 실행 상태가 되었을때 작동 정의
        }
	}
}

 

  • active: Scene이 foreground 상태이며, 유저 이벤트를 받을 수 있는 상태
  • inactive: Scene이 foreground 상태이지만, 이벤트를 받지 못하는 비활성 상태
  • background: Scene이 현재 존재하고있지만, UI에서는 보여지지 않는 상태

여기서 Swift UI가 기본적으로 제공하는 여러타입의 Scene이 존재한다. 표현하고자 하는 Scene에 알맞은 타입을 적용하거나, 혹은 사용자가 직접 Scene을 커스텀하여 정의할 수 있다.

  • WindowGroup: 동일한 계층구조를 가지는 그룹들을 여러개의 독립된 윈도우들로 구성하여 관리하는 Scene
  • Window: 단 하나의 유일한 윈도우만 보여주는 Scene
  • DocumentGroup: 파일 등의 document들을 Read/Write할 수 있도록 지원하는 Scene
  • Settings: App의 설정값들을 보여주고 수정할 수 있게하는 Scene
  • WKNotificationScene: 원격 혹은 로컬에서 알림을 받았을 때 나타나는 Scene

 

4. View
 view는 앱의 UI를 표현하는 프로토콜이다. view 프로토콜은 커스텀한 view의 내용물을 담는 body를 정의해야한다.
body는 swift ui에서 제공하는 하나 이상의 built-in view를 조합해서 작성할 수 있다. 이렇게 view 안에 하위 view들이 구성되는 형태, 즉 view끼리의 계층구조가 구성된다.
  - modifier: view는 modifier을 정의하여 크기, 색상, 폰트 등 여러가지 요소들을 수정할 수 있다.

import SwiftUI

struct ContentView: View {
    var body: some View {
		Vstack{
            Text("Hello SwiftUI!")
                .font(.title)
            Spacer()
            Button(action: {
            	 print("Clicked")
        	}) {
            HStack {
                Image(systemName: "heart")
                Text("Start")
                .font(.title)
            }
            .foregroundColor(.blue)
        }
    }
}

// 정의한 View를 UI에 그려진 화면으로 미리볼 수 있도록 제공한다
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

위의 예시 코드에서 .font, .foreground 등의 설정값이 각 view들의 modifier가 된다.

UI를 구성하는 
built-in view는고정 텍스트(Text), 수정가능한 텍스트 영역(TextEditor), 버튼(Button), 이미지(Image) 등 다양한 종류를 지원하고 있고, 이들의 스타일을 편집하는 modifier도 굉장히 다양한 옵션들이 존재한다.

지난 포스팅에서 select 모델의 특징을 알아보며 블로킹(Blocking)과 비동기(Asynchronous) 입출력 모델이라 설명을 하였다. 컴퓨터공학 분야에서는 기술적 구현에 따라 블로킹과 논블로킹, 그리고 호출된 함수와 호출된 함수의 순차를 신경쓰는가 아닌가에 따라 동기와 비동기로 나뉘어진다.
 이번 포스팅에서는 이 개념에 대해서 상세히 다뤄보겠다.


1. 블로킹(Blocking) vs 논블로킹(Non-Blocking)

 블로킹과 논블로킹은 기술적인 구현을 통해 나뉘는 개념이다. 코드를 통해 블로킹으로 구현하였는가, 논블로킹을 구현하였는가에 따라 프로세스의 CPU 유휴상태가 달라진다.

1) 블로킹(Blocking)

Blocking방식

- 블로킹은 함수A가 함수B를 호출하였을때, 제어권을 함수 B에게 넘겨준다. A는 B의 결과가 반환되기 전까지 아무런 작업도 하지않고 대기한다. 
- 마찬가지로 입출력 요청이 발생했을때, 응용프로그램(Application)은 커널(Kernel)에게 시스템 요청(System call)을 하며 커널의 입출력 처리가 마칠때까지 입출력을 요청한 응용프로그램의 스레드는 아무런 작업을 진행하지 않고 기다린다.

※ 시스템 호출(system call): 응용프로그램에서는 수행할 수 있는 기능이 제약적이다. 따라서 특정 기능의 수행이 필요할 때 커널의 도움이 필요하다. 이때 커널의 기능을 사용하기 위해 요청하는것이 시스템 호출이다. 

-  단일 스레드로 블로킹 방식의 입출력을 한다면, 입출력 작업이 완료될 때 까지 응용프로그램이 멈춰있는다. (이전에 구현했던 send()/recv()함수를 통한 서버와 클라이언트가 그 예시이다.)
- 멀티 스레드로 다수의 클라이언트를 서비스하는 환경에서 문맥 교환(Context Switching)이 잦다. CPU는 실행중인 스레드가 입출력 작업으로 인해 일시적으로 중지되면 다른 스레드에게 CPU자원을 할당하여 실행시킨다. 이때 CPU의 자원을 할당받아 실행시키는 작업이 문맥 교환이라고 한다. 문맥 교환 작업이 일어나는 동안은 프로세스 처리하는 작업을 못하게 되므로 오버헤드가 발생하여 효율이 떨어진다.

※ 오버헤드(overhead): 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간·메모리 

이렇게 블로킹 I/O는 비효율성이 있기때문에 이를 개선하기 위한 논블로킹 모델이 고안되었다.


2) 논블로킹(Non-Blocking)

Non-Blocking 방식

- 논블로킹은 함수A가 함수B를 호출하였을때, B는 데이터가 준비되지 않아도 곧바로 반환을 한다. 이때 함수A가 가지고 있는 제어권도 B에게 넘겨준 즉시 A가 돌려받는 것이다.
- 함수 B가 반환한 데이터는 신뢰성이 없기때문에, A의 작업을 수행하면서 주기적으로 B가 원하는 결과값이 구해졌는지 체크가 필요하다. 이 작업을 폴링(polling)이라고 한다

- 논블로킹 입출력 모델은 응용 프로그램이 커널에게 시스템 요청을 했을때, 곧바로 응답을 반환한다. (예를들어 windows에서는 WSAEWOULDBLOCK라는 응답을 바로 반환한다)
- 멀티 스레드 환경에서 입출력 요청이 발생했을때 다른 스레드를 실행시키기 위한 문맥교환이 필요없다. 입출력을 요청한 스레드는 중단없이 실행되기 때문이다.
- 블로킹 형식보다 개선된 효율성을 보이지만 논블로킹이 만능은 아니다. 다수의 클라이언트로부터 동시다발적인 요청이 온다면, 커널에서 원하는 응답값이 준비되었는지 확인하는 과정에서 많은 비용이 들기 때문이다.
- 운영 체제들은 polling작업을 커널에서 지원하여 효율을 높인 I/O 모델들을 지원한다. 리눅스의 epoll이 그 예이다.


2. 동기(Synchronous) vs 비동기(Asynchronous)

 동기와 비동기는 기술로 구분되는 것이 추상적인 개념이다. 블로킹 논블로킹은 제어권이 누구에게 있느냐가 관심이였다면, 동기와 비동기는 호출한 함수가  작업들의 순서가 보장되느냐 아니냐가 관심사이다

1) 동기(Synchronous)

Synchronous 방식

-  동기방식은 함수A가 호출되었을때, 함수B는 먼저 호출된 함수A의 종료까지 기다린다. 함수A가 종료된 후에서야 함수B가 실행된다.
- 함수 작업A가 완료되는동안 함수B는 기다리고 있기때문에 얼핏 블로킹과 유사해보이지만, 동기모델의 관심사는 순차적인 작업의 순서를 보장한다는 것이다. 

- 동기 입출력 모델에서는 입출력이 발생하였을 때, 응용프로그램의 스레드는  커널에게 입출력의 입출력 작업이 진행되는 동안 아무런 작업을 하지않고 기다린다. 이후 커널의 작업이 끝나면 응용프로그램의 스레드는 다음 작업을 진행한다.
- 동기 입출력 모델에서 응용프로그램이 입출력을 마칠때까지 기다린다는 점에서 마치 블로킹과 유사하지만, 블로킹 형태의 입출력은 CPU자체의 처리를 입출력 작업이 끝나기 전까지 막는것을 뜻하며, 동기-논블로킹 형태로 구현도 가능하다. 입출력함수를 논블로킹으로 설정하면 CPU자체는 곧바로 반환하지만 동기형식이기 때문에 입출력 결과를 계속해서 확인하는 polling작업을 하면서 CPU는 busy-wait(CPU가 계속 프로세스를 작업하며 대기하는 상태)가 될 수 있다.

- 동기방식은 순서를 기다리기 때문에 효율성이 떨어질때도 있지만, 간단한 작업들이 순차적으로 필요할때는 효율적일 수 있다.


2) 비동기(Asynchoronous)

Asynchronous 방식

- 비동기방식은 함수A가 먼저 호출되었다고 해도, 뒤이어 호출되는 함수B와 함수C는 앞서 호출된 함수의 완료여부에 상관없이 실행된다.
- 각 함수들의 호출 순서와 함수들의 완료가 같은 순서가 된다고 보장할 수 없다.
- 함수B와 함수C의 시점에서는 선행된 작업을 기다리지 않아서 논블로킹과 유사해보이지만, 비동기는 순서를 지키지않고 작업을 진행한다는 것이 관심사이다.

- 비동기 입출력 모델에서는 응용프로그램이 입출력 요청이 발생했을때, 응용프로그램 스레드는 커널에게 작업을 맡긴 후 완료를 기다리지 않고 다음 작업들을 진행한다.
- 비동기와 논블로킹이 유사해보이지만, 입출력 요청이 비동기형태로 구현되어도 블로킹 형태로 처리될 수 있다.

- 여러 작업들을 병행해서 가능하므로 효율을 높일 수 있다. 하지만 구현에 있어서 보다 더 복잡한 문제를 고민해야할 가능성이 동기방식보다 높다. 


 이번 포스팅을 통해 블로킹(Blocking)과 논블로킹(Non Blocking) 그리고 동기(Synchronous)와 비동기(Asynchronous)에 대해 상세히 알아봤다.
 사실 동기방식은 블로킹, 비동기방식이 논블로킹이 구현에 있어서 편리함이 있기에 두 개념이 묶여질 때가 많다. 하지만 이들은 별개의 개념이다. 다음 포스팅에서는 이 개념들이 조합된 형태에 대해 다뤄볼 예정이다.

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 모델들이 존재하는데 이후 포스팅에서 다뤄볼 예정이다. 

 

이전 포스팅에서는 echoServer를 통해 상호간의 통신을 하는 서버와 클라이언트를 구현해보았다.
다만 이전의 서버는 하나의 서버프로세스는 하나의 클라이언트의 처리만 진행하였다.
하지만 실제 서버는 여러개의 클라이언트를 동시다발적으로 처리해야한다.
때문에 이번 포스팅에서는 다중 접속 서버의 모델들의 종류에 대해 설명을 한다.

1) 멀티 프로세스 서버
부모 프로세스가 클라이언트의 접속이 오면, 해당 클라이언트와 데이터를 송수신할 담당할 자식 프로세스를 생성한다.
자식 프로세스는 클라이언트와의 연결이 종료되면 종료된다.
하나의 프로세스가 클라이언트에 대해 통신을 전담하기에 대용량의 데이터를 전송하기에 유리하지만, 프로세스의 생성은 큰 비용이 들기때문에 시스템 자원을 크게 사용한다는 단점이 있다.

 

2) 멀티 플렉싱 서버
여러개의 클라이언트로 부터 오는 입출력 정보를 하나의 전송로에 묶어서 한꺼번에 데이터를 송수신한다.
송수신하는 데이터의 크기가 작은 경우에 용이하며, 멀티 프로세스 서버보다 다수의 클라이언트를 처리하는데 유리하다.
하지만 멀티스레드 방식에 비해 많은 접속을 받지 못하며 접속자가 많을수록 클라이언트의 데이터를 체크하기위한 작업에 소요되는 시간이 길어져 부하가 생기는 단점이 있다.
 멀티 플렉싱 서버는 다수의 클라이언트 정보를 배열로 묶어 관리하는 select함수를 이용하여 구현할 수 있다. 이 부분은 다음 포스팅에서 다뤄보겠다.

 

 

3) 멀티 스레드 서버

하나의 프로세스에서 각 클라이언트의 송수신을 담당하는 스레드(thread)를 생성한다. 스레드는 프로세스에 비해 생성비용이 적어 멀티 프로세스 방식의 서버보다 더 많은 클라이언트의 접속을 처리하는데 용이하다.
하지만 스레드간의 교착상태나 race컨디션 등의 위험이 있다.

 

이번 포스팅에서는 서로 통신을 하는 서버와 클라이언트를 다루어볼 것이다.
여기서 가장 간단한 형태인 iterative 방식의 echoServer를 구현하면서, 서버와 클라이언트 소켓생성과 통신까지 전체적인 과정을 살펴본다.

1) echoServer.cpp

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

int main()
{
	printf("Server Start!\n");
    
 // 1) WinSock 라이브러리를 초기화한다   
	WSAData wsaData; //WinSock 초기화 구조체
	int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);

	if (initWinSockRes != 0) {
		printf("윈도우소켓 초기화 실패");
		return 0;
	}

 // 2) 서버와 클라이언트 소켓을 생성하고 서버소켓을 bind, listen한다.
	SOCKET servSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	SOCKET clntSocket;
	sockaddr_in servAddr, clntAddr;

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

	if (bind(servSocket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0) {
		printf("bind() 오류 [오류코드: %d]\n", WSAGetLastError());
		return 0;
	}
	
    // 크기 3의 대기열을 만든다.
    // 클라이언트의 연결요청이 왔을때 서버에서 준비된 소켓이 없다면 대기열에서 연결을 기다리게된다. 
	if (listen(servSocket, 3) != 0) {
		printf("listen() 오류 [오류코드: %d]\n", WSAGetLastError());
		return 0;
	}
	
	char buf[BUF_SIZE] = {};
	int  clntAddrLen = sizeof(clntAddr);


	// 순차적으로 3개까지의 클라이언트를 연결해서 송수신작업을 한다.
	for (int i = 0; i < 3; i++) {
    
 // 3) 클라이언트가 연결을 요청하면 수락한다.
		clntSocket = accept(servSocket, (sockaddr*)& clntAddr, &clntAddrLen);
		if (clntSocket == -1) {
			printf("accept() 오류 [오류 코드: %d]\n", WSAGetLastError());
			return 0;
		}
		printf("accept client\n");

		while (1) {
			memset(buf, 0, BUF_SIZE);
            
		//4) 클라이언트로부터 온 메세지를 받는다.
			int recvBytes = recv(clntSocket, buf, BUF_SIZE, 0);
			if (recvBytes == SOCKET_ERROR) {
				printf("recv() 오류 [오류 코드: %d]\n", WSAGetLastError());
				break;
			}

			printf("[message from client]: %s (%d bytes)\n", buf, recvBytes);

			if (recvBytes == 0) {
				printf("클라이언트 연결 종료됨.");
				break;
			}
            
        //5) 클라이언트로부터 온 메세지를 그대로 클라이언트에게 전송한다.
			send(clntSocket, buf, recvBytes - 1, 0);
		}
		
	}

	WSACleanup();

	return 0;
}

 먼저 서버의 코드이다.

 한꺼번에 1명의 클라이언트를 받을 수 있으며, 한 개의 클라이언트와 통신중에 다른 클라이언트의 요청이 온다면 해당 클라이언트는 이전에 먼저 연결된 클라이언트가 종료될때까지 연결을 기다려야 한다.
 서버는 연결된 클라이언트가 종료될때까지 클라이언트로부터 오는 메세지를 받고, 그대로 전송하는 작업을 한다.
 3번까지 클라이언트에게 echoServer로써 기능하는 역할을 마무리하면 서버는 종료된다.


2) echoClient.cpp

#pragma comment(lib, "ws2_32")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <string>
#include <iostream>
#include <winsock2.h>
#include <memory>
#define BUF_SIZE 1024

using namespace std;

int main() {
	WSAData wsaData;
	SOCKET clnt_socket;
	
	printf("Client Start!\n");

	// WINSOCK 초기화
	int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);

	if (initWinSockRes == SOCKET_ERROR) {
		return 0;
	}

	clnt_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (clnt_socket == INVALID_SOCKET) {
		printf("윈도우소켓 초기화 실패");
		return 0;
	}

	sockaddr_in servAddr;

	// 로컬 주소로 연결한다. 포트는 서버에서 bind한 포트와 동일해야, 서버로 연결이 가능하다
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	servAddr.sin_port = htons(11000);


	if (connect(clnt_socket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0) {
		printf("connect() 오류 [오류 코드: %d\]n", WSAGetLastError());
		return 0;
	}

	char input[BUF_SIZE];
	char received[BUF_SIZE];

	while (1) {
		memset(input, 0, BUF_SIZE);
		memset(received, 0, BUF_SIZE);

		cin.getline(input, BUF_SIZE);
		
		if (string{ input } == "quit") {
			closesocket(clnt_socket);
			break;
		}

		if ( send(clnt_socket, input, sizeof(input), 0) == SOCKET_ERROR ) {
			printf("send() 오류 [오류 코드: %d]\n", WSAGetLastError());
			break;
		}
		
		if (recv(clnt_socket, received, BUF_SIZE, 0) == SOCKET_ERROR ) {
			printf("recv() 오류 [오류 코드: %d]\n", WSAGetLastError());
			break;
		}

		printf("[message from server] %s \n", received);
	}

	return 0;
}

 다음은 클라이언트의 코드이다. 서버와 마찬가지로 WinSock라이브러리를 초기화한 뒤 서버에 연결한다.
서버와 연결되면 클라이언트에 보내고싶은 문자열을 입력해 전송이 가능하다. 전송된 문자열은 곧 서버로부터 똑같이 되돌려받는다.
 만일 quit라고 입력한다면, 클라이언트는 종료되며 서버는 다음 클라이언트를 받을 준비를 한다(3번의 서비스를 했다면 서버도 종료된다)

 


3) 실행 결과

먼저 서버에 첫번째 클라이언트를 연결한 뒤 메시지를 보낸 결과다

서버 1번 클라이언트

 1번 클라이언트에게 메세지를 보낸대로 서버는 대답을 보내준다.
여기에서 1번 클라이언트가 종료되지 않은 시점에서 2번과 3번 클라이언트를 연결시키고, 메세지를 보낸다.

2번 클라이언트

3번 클라이언트 

 2번과 3번 클라이언트는 메세지를 송신해도 이미 1번 클라이언트가 선점되어 처리되고 있으므로,
메세지에 대한 답변을 받지못한채 블록킹(다음 처리를 하지못하고, 현재 작업이 끝날때까지 기다림) 되고있다.

여기에서 1번클라이언트를 종료시킨다.

서버

 

1번 클라이언트를 종료시키면 대기하고 있던 2번 클라이언트의 메세지가 보내진다

2번 클라이언트
3번 클라이언트

2번 클라이언트는 메세지를 받고 다음 메세지를 받을 수 있지만, 3번 클라이언트는 여전히 recv함수에서 블록킹 된 상태다.

서버 3번 클라이언트

2번 클라이언트가 종료되고 나서야 마지막으로 3번 클라이언트가 메세지를 보내고 받을 수 있다.

※ 클라이언트의 코드에서 send함수 뒤에 send함수가 완료되었다는 로그를 남기면, 2번 3번 클라이언트가 블록킹 되는 시점은 send가 아닌 recv라는 것을 알 수 있다.


윈도우 소켓으로 서버와 클라이언트의 통신을 구현해보고 알아보았다.
하지만 지금 간단하게 구현된 서버는 서비스에서 거의 사용하지 못한다. 클라이언트를 한명씩 순차적으로만 처리할 수 있다면 기다리는 다른 클라이언트들은 긴 대기시간을 가지게 된다.

서버는 빠른 응답도 중요하기 때문에 이후 포스팅에서는 지금의 구조를 개선해서, 다수의 클라이언트를 동시에 처리할 수 있는 서버(병렬 처리서버)를 다뤄볼 것이다.

지난 포스팅까지 TCP프로토콜을 사용하는 서버와 클라이언트를 구현해보았다.
이번 포스팅에서는 서로 연결된 네트워크끼리 데이터를 통신하는 send함수와 recv함수에 대해서 알아본다.

1) send 함수

send(socket, buffer, sizeof(buffer), 0);

send함수를 통해 소켓으로 연결되어있는 다른 프로세스에게 데이터를 보낼 수 있다.

- 1번째 인자: 데이터를 보낼 연결상태의 소켓
- 2번째 인자: 전송할 데이터를 담고있는 데이터버퍼의 포인터. 데이터버퍼는 char배열 형식이다
- 3번째 인자: 데이터 버퍼의 byte크기 
- 4번째 인자: 특별한 옵션을 주기 위한 플래그이다. 옵션은 '|| (OR)'로 조합할 수 있다. 따로 옵션을 지정하지 않는다면 0을 주어 생략가능하다.

옵션 값 옵션 값 의미
MSG_DONTROUTE 데이터를 전송할때 라우팅 테이블을 참조하지 않는다
MSG_OOB 데이터를 긴급 전송모드(Out-of-band data 모드)로 전송한다

send함수는 오류가 없다면 보냈던 데이터의 byte크기를 반환한다. 만일 오류가 발생한다면 SOCKET_ERROR라는 오류코드를 반환한다. WSAGetLastError() 함수를 호출하면 어떤 종류의 오류가 발생하였는지 확인할 수 있다.


2) recv 함수

recv(clientSocket, buffer, BUF_SIZE, 0);

recv함수를 통해 다른 프로세스로부터 온 데이터를 수신할 수 있다.

- 1번째 인자: 데이터를 수신할 연결상태의 소켓
- 2번째 인자: 수신된 데이터를 담을 데이터버퍼의 포인터. 데이터버퍼는 char배열 형식이다
- 3번째 인자: 데이터 버퍼의 byte크기
- 4번째 인자: 특별한 옵션을 주기 위한 플래그. send함수와 마찬가지로 별 옵션을 주지않는다면 0을 기입한다.

recv함수도 정상적으로 수신되었을 때에는 받은 데이터의 byte크기를 반환한다. 오류가 발생하였을때는 마찬가지로 SOCKET_ERROR를 반환한다.


이번 포스팅으로 데이터를 전송하는 send()와 recv()함수를 알아보았다.
이후 포스팅에서는 echoServer(보냈던 메세지를 그대로 답해주는 서버)를 우선 구현하여 서버와 클라이언트 통신을 다뤄볼 예정이다.

지난번 포스팅에서는 서버의 소켓을 만들었다
이번에는 클라이언트의 소켓을 정의해보고 서버에 연결한다.

1. 클라이언트의 소켓

//클라이언트 소켓 생성
SOCKET clntSocket;
clntSocket = socket(AF_INET, SOCK_STREAM, 0);

if (clntSocket == INVALID_SOCKET) {
	printf("invalid socker error");
	return 0;
}

//서버 소켓 주소정보 입력
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(11000);


//서버로 연결
if (connect(clntSocket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0) {
	printf("connect() error! [error code: %d\]n", WSAGetLastError());
	return 0;
}

클라이언트의 소켓 생성과정은 서버에 비해 훨씬 간단하다.


1) 클라이언트 소켓 생성

clntSocket = socket(AF_INET, SOCK_STREAM, 0);

socket함수를 통해 소켓을 생성한다. 소켓 생성함수의 인자들에 관한 설명은 이전 포스팅들에서 다뤘으므로 생략한다.
여기서 3번째 인자는 0을 기입해서 생략할 수 있는데, 이 경우에는 자동적으로 알맞은 프로토콜이 배정된다. 



2) 소켓 연결

connect( clientSocket, (sockaddr*)&servAddr, sizeof(servAddr))

 connect 함수를 사용하여 클라이언트에서 서버로 연결할 수 있다.

1번째 인자: 현재 다른것과 연결이 되지않은 소켓을 기입한다
2번째 인자: 연결할 정보를 담은 sockaddr 구조체의 포인터
3번째 인자: 2번째로 넘겨준 sockaddr 구조체의 byte 크기

클라이언트 소켓을 생성하고, 이것을 서버 소켓의 주소를 대상으로 연결하면 서버-클라이언트 통신이 완성된다.


서버와 클라이언트의 통신이 수립되는 과정을 요약한 그림이다.


다음 포스팅에서는 이렇게 서로 연결된 서버-클라이언트가 데이터를 송수신하는 Winsock함수를 살펴보고,
보냈던 말을 그대로 답변하는 echo서버를 구현할 것이다.

이전의 글에서 소켓을 만드는 법을 배웠다. 이제는 만든 소켓을 이용해서 두 프로세스 간의 통신을 하는 작업을 할 것이다.
여기서 TCP환경과 UDP환경에서 필요한 작업이 다르다. 그래서 이번 포스팅에서는 TCP에서의 통신을 먼저 알아보려 한다. 만들어진 소켓을 이용해서 서버가 TCP통신을 시작하는 일련의 과정들을 포스팅한다.

SOCKET servSocket, clntSocket;

//1. 소켓 생성하기
servSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//2. 소켓 주소 구조체 만들기
sockaddr_in servAddr;
ZeroMemory( &servAddr, sizeof(servAddr) );

//3. 소켓 주소 구조체에 정보 작성하기
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(11021);

//4. 소켓에 주소 bind하기
if (  bind(servSocket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0 ) {
	printf("socket bind error!");
	return;
}

//5. 소켓을 listen상태로 만들기
if ( listen(servSocket, 0) != 0 ) {
	printf("socket listen error!\n");
	return;
}

//6. 클라이언트의 연결이 오면 accept하기
int clntAddrLen = sizeof(clntAddr);
clntSocket = accept( servSocket, (sockaddr*)&clntAddr, &clntAddrLen);
if (clntSocket == -1) {
	printf("socket accept error!\n");
	return;
}

 

1) 소켓주소 구조체 정의하기

먼저 소켓을 통신하기 전에, 소켓의 주소를 할당하는 작업이 필요하다.
소켓이 연결할 주소를 저장하는 구조체를 윈도우즈 소켓 라이브러리가 제공해준다.
소켓 구조체중에 대표적인 2가지인 sockaddr구조체sockaddr_in구조체를 알아보자.

struct sockaddr {
        ushort  sa_family;
        char    sa_data[14];
};

 sockaddr은 2byte의 주소체계를 지정하는 부분과, 14byte의 주소정보를 지정하는 부분이 있다.
sockaddr 구조체의 주소 데이터를 저장하는 sa_data배열에는 ip주소(ex 127.0.0.1)과 port번호(8080), 공백문자(sockaddr구조체의 총 크기를 16byte로 맞추기위함)로 구성되어 있다. 공백문자들로 구성되어있다.

struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};

struct in_addr {
    unsigned long s_addr; 
};

 반면 sockaddr_in은 2byte의 주소체계를 지정하는 부분과, 2byte의 포트를 지정하는 부분, 4byte 크기의 주소를 저장하는 in_addr구조체로 구성된 sin_addr, 그 외에 8byte의 0으로만 구성된 sin_zero필드가 구성되어있다.
sin_zero는 sockaddr_in구조체가 sockaddr와 크기를 맞추기위해 존재하지만, 아무런 의미를 가지지 않고있다. 

[출처] https://topic.alibabacloud.com/a/the-difference-between-sockaddr-and-sockaddr_in-reproduced_8_8_31409815.html

 

The difference between sockaddr and sockaddr_in (reproduced)

Original link: http://kenby.iteye.com/blog/1149001 struct sockaddr and struct sockaddr_in are two structures used to handle the addresses of network traffic. In various system calls or functions, these two structures are used whenever dealing with network

topic.alibabacloud.com

사실 이후 다루게 될 소켓에 주소를 할당하는 bind함수는 sockaddr 구조체를 사용한다. 따라서 연결할 주소정보와 포트정보가 분리된 sockaddr_in 구조체는 sockaddr정보로 변환되어 할당된다.

 

 


2) bind(): 소켓의 주소 할당하기

소켓 주소 구조체를 정의하였으면, bind함수를 통해 정의된 주소를 소켓에 할당할 수 있다.
bind 함수는 소켓에 ip주소를 할당하는 역할을 한다.

int bind(
  SOCKET          s,
  const sockaddr* addr,
  int             addrlen
);

1번째 인자: 주소를 할당할 소켓
2번째 인자: 소켓의 주소
3번째 인자: 소켓의 주소정보의 길이

이 bind 함수는 정상적으로 할당된다면 0을 반환한다. 만일 오류가 발생한다면 SOCKET_ERROR(-1 값)을 반환한다.


3) listen():  소켓을 클라이언트를 받을 수 있는 상태로 만들기

bind까지 마친 소켓은 주소를 가진 소켓이 된다. 여기에서 다른 곳에서 접속을 시도하는 클라이언트를 받을 수 있게 하려면, 해당 소켓을 접속 대기상태로 만들어줘야한다. 이는 listen함수를 이용하여 진행한다.

int WSAAPI listen(
  SOCKET s,
  int    backlog
);

1번째 인자: listen상태로 만들 소켓
2번째 인자: 대기 가능한 최대 연결 갯수. 0을 지정하면 default값으로 지정된다.

이 작업을 진행한 소켓은 이제 수동적으로 연결이 오기를 기다리게 된다. listen함수를 성공하면 0, 실패하면 -1을 반환한다. 


4) accept():  클라이언트로부터 오는 연결을 수락한다

listen상태가 된 소켓은 클라이언트로부터 연결이 오면, accept함수를 이용해 그 정보를 저장한다. 
이 함수는 연결된 클라이언트를 정보로 담은 소켓을 반환값으로 return한다. 만일 accept에 실패하면 -1을 반환한다.
accept함수는 블로킹 함수이다. 따라서 이 함수가 실행될 때 까지 해당 스레드는 대기하게 된다.

SOCKET WSAAPI accept(
  SOCKET   s,
  sockaddr* addr,
  int* addrlen
);

1번째 인자: listen 상태가 된 서버소켓
2번째 인자: 연결된 클라이언트의 주소정보를 담을 sockaddr 구조체. 연결이 되면 해당 구조체에 클라이언트 주소정보가 저장된다.
3번째 인자: 2번째 인자로 넘겨준 구조체의 크기

여기서 2번째와 3번째 인자는 옵션이므로 NULL로 생략이 가능하다.


위의 과정을 거쳐서 서버의 소켓을 생성하였다. 다음 번에는 클라이언트 프로그램에서 소켓을 생성하는 부분과, 서버와의 통신에 대해 포스팅해보겠다.

+ Recent posts