GithubHelp home page GithubHelp logo

gidskql6671 / network-progamming Goto Github PK

View Code? Open in Web Editor NEW
1.0 1.0 0.0 204 KB

2023학년도 1학기 네트워크 프로그래밍 수업을 들으며 작성한 코드들

C++ 99.75% Shell 0.05% HTML 0.19%

network-progamming's Introduction

Network Programming

네트워크 프로그래밍을 안 듣고 컴퓨터망을 듣는 호구를 위해...
열혈 TCP/IP 소켓 프로그래밍 책을 보고 정리하는 레포지토리

이론 정리

소켓의 타입

연결지향형 소켓(SOCK_STREAM)

  • 중간에 데이터가 소멸되지 않고 목적지로 전송된다.
    • 독립된 연결을 통해 데이터를 전달하기에, 연결 자체가 문제가 있는게 아닌 이상 데이터가 소멸되지 않음을 보장한다.
  • 전송 순서대로 데이터가 수신된다.
  • 전송되는 데이터의 경계(Boundary)가 존재하지 않는다.
    • 데이터를 송수신하는 소켓은 내부적으로 버퍼를 가지고 있다. 이 버퍼를 사용하여 데이터를 나눠서 받을수도 혹은 한번에 받을 수도 있다. 그렇기에 데이터의 경계가 없다고 한다.
  • 소켓 대 소켓의 연결은 반드시 1대1이어야 한다.
  • 신뢰성 있는 순차적인 바이트 기반의 연결지향 데이터 전송 방식의 소켓
  • TCP 프로토콜이 이에 해당한다.

비 연결지향형 소켓(SOCK_DGRAM)

  • 전송된 순서에 상관없이 가장 빠른 전송을 지향한다.
  • 전송된 데이터는 손실의 우려가 있고, 파손의 우려가 있다.
  • 전송되는 데이터의 경계(Boundary)가 존재한다.
  • 한번에 전송할 수 있는 데이터의 크기가 제한된다.
  • 신뢰성과 순차적 데이터 전송을 보장하지 않는, 고속의 데이터 전송을 목적으로 하는 소켓
  • UDP 프로토콜이 이에 해당한다.

인터넷 주소(Internet Address)

IP(Internet Protocol) 주소 체계는 IPv4와 IPv6 두 종류로 나뉜다. 두 체계의 차이는 IP주소 표현에 사용되는 바이트 크기이며, 자세한 설명은 생략.

IPv4 기준의 4바이트 IP 주소는 네트워크 주소와 호스트(컴퓨터를 의미) 주소로 나뉘며, 주소의 형태에 따라 A, B, C, D, E 클래스로 분류된다.

  • A 클래스 : 앞 1바이트가 네트워크 주소, 뒤 3바이트가 호스트 주소
  • B 클래스 : 앞 2바이트가 네트워크 주소, 뒤 2바이트가 호스트 주소
  • C 클래스 : 앞 3바이트가 네트워크 주소, 뒤 1바이트가 호스트 주소
  • D 클래스 : 멀티캐스트 IP 주소
  • E 클래스 : 예약된 IP 주소

네트워크 주소와 호스트 주소로 나뉘어져 있는 이유는 IP 주소로 컴퓨터를 찾을 때, 한번에 바로 접근하는 방식이 아니기 때문이다. 네트워크 주소를 사용하여 특정 네트워크에 접근한 뒤에 호스트 주소를 사용하여 특정 호스트로 접근하는 방식을 사용한다. 예를 들어, SEMI.COM라는 회사의 무대리에게 데이터를 전송한다고 가정해보자. 그러면 우선 네트워크 주소를 사용하여 SEMI.COM의 네트워크로 데이터가 전송되고, 이후 해당 네트워크를 구성하는 라우터가 호스트 주소를 사용하여 무대리에게 데이터를 전송한다.

송신자 ---(네트워크 주소)---> 네트워크(라우터, 스위치) ---(호스트 주소)---> 호스트

위 흐름에서 알 수 있듯 특정 IP 주소에서 네트워크 주소와 호스트 주소를 아는 것은 중요하다. 이것은 IP 주소의 첫 번째 바이트를 보면 알 수 있다. 왜냐하면 다음과 같이 클래스 별로 IP 주소의 경계를 나눠놓았기 때문이다.

  • 클래스 A의 첫 번째 바이트 범위 : 0이상 127이하
  • 클래스 B의 첫 번째 바이트 범위 : 128이상 191이하
  • 클래스 C의 첫 번째 바이트 범위 : 192이상 223이하

이는 다음과 같이 표현할 수도 있다.

  • 클래스 A의 첫 번째 비트는 항상 0으로 시작
  • 클래스 B의 첫 번째 비트는 항상 10으로 시작
  • 클래스 C의 첫 번째 비트는 항상 110으로 시작

PORT 번호

IP 주소가 네트워크 상의 컴퓨터를 구분하기 위한 목적으로 사용된다면, PORT 번호는 컴퓨터 내에서 프로세스를 구분하기 위한 목적으로 사용된다.

PORT 번호는 16비트로 표현되기에, 할당할 수 있는 PORT 번호의 범위는 0이상 65535이하이다. 하지만 0부터 1023까지는 '잘 알려진 PORT(Well-known PORT)'라서, 사용처가 이미 정해져있다. 추가로 PORT 번호는 기본적으로 중복이 불가능하지만, TCP 소켓과 UDP 소켓은 PORT 번호를 공유하지 않아 중복되어도 상관없다.

바이트 순서(Order)와 네트워크 바이트 순서

CPU가 데이터를 저장하는/해석하는 방식은 두 가지로 나뉜다.

  • 빅 엔디안(Big Endian) : 상위 바이트의 값을 작은 번지수에 저장하는 방식
  • 리틀 엔디안(Little Endian) : 상위 바이트의 값을 큰 번지수에 저장하는 방식

예를 들어, 0x20번지를 시작으로 4바이트 Int형 정수 0x12345678을 저장한다고 가정하면, 다음과 같이 저장된다.

0x20번지 0x21번지 0x22번지 0x23번지
빅 엔디안 0x12 0x34 0x56 0x78
리틀 엔디안 0x78 0x56 0x34 0x12

데이터의 저장 방식은 CPU에 따라 달라지며, 이러한 CPU의 데이터 저장방식을 '호스트 바이트 순서(Host Byte Order)'라고 한다. 만약 호스트 바이트 순서가 서로 다른 컴퓨터끼리 데이터를 그냥 주고받는다면, 0x1234라는 데이터를 보냈을 때 상대방은 0x3412라는 값으로 받아들일 것이다. 이런 문제를 해결하기 위해 네트워크를 통해 데이터를 전송할 때는 통일된 기준으로 데이터를 전송하기로 정하게 되었다. 이를 '네트워크 바이트 순서(Network Byte Order)'라고 하며, 빅 엔디안 방식으로 통일하기로 했다.

즉, 네트워크를 통해 수신된 데이터는 빅 엔디안 방식으로 정렬되어 있음이 보장되고, 리틀 엔디안 시스템에서 데이터를 전송할 때는 빅 엔디안 방식으로 데이터를 재정렬할 필요가 있다.

C언어의 소켓 프로그래밍에서는 네트워크 바이트 순서로 데이터를 재정렬하는 함수를 제공한다. htons, ntohs, htonl, ntohl이 그것이다. 이 함수들에서 h는 호스트 바이트 순서를 의미하고, n은 네트워크 바이트 순서를 의미한다. s는 short, l은 long(4바이트)을 의미한다. 즉, htons는 "short형 데이터를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하는 함수"이다. 일반적으로 뒤에 s가 붙으면 PORT 번호의 변환을, l이 붙으면 IP주소의 변환에 사용한다. (타입이 같으니)

추가적으로 이러한 변환은 소켓 프로그래밍에서 sockaddr_in 변수에 개발자가 직접 값을 채워줄 때만 해주면 된다. 데이터 송수신 과정에서는 함수 내부에서 알아서 데이터 재정렬을 해준다.

TCP/IP 4계층

TCP/IP 프로토콜의 과정을 4개의 계층으로 나누어 표현한 것. 이렇게 구체적으로 프로토콜을 계층화함으로써 얻게 되는 장점은 여럿 있다.

간단하게는 프로토콜 설계의 용이성을 얻는 것이고, 책에서 말하는 더 중요한 이유는 표준화 작업을 통한 '개방형 시스템(Open System)'의 설계이다. 개방형 시스템이란 공인된 표준을 사용하여 어떠한 개방형 시스템과도 상호 연동할 수 있는 시스템이다. 개방형 시스템의 장점으로는 공인된 표준에 맞추어 제작되기에 어떠한 구현체를 사용하더라도 문제가 없다는 것이다. 예를들어 IP 계층을 담당하는 라우터는 어떤 회사의 장비를 사용하더라도 어렵지 않게 교체가 가능하다. 모든 라우터 제조사들이 IP 계층의 표준에 맞추어 라우터를 제작하기 때문이다. 즉, 이렇게 표준이 있다면, 이 표준에 맞는 다양한 기술이 빠르게 발전할 수 있고 이를 사용하는데도 문제가 없어진다.

TCP/IP 4계층은 네트워크 엑세스 계층, 인터넷 계층, 전송 계층, 응용 계층으로 이루어져있다.

네트워크 엑세스 계층

**네트워크 엑세스 계층(Network Access Layer)**은 물리적인 데이터의 전송을 담당하는 계층이다. LAN, WAN, MAN과 같은 네트워크 표준과 관련된 프로토콜을 정의한다. 두 호스트가 인터넷을 통해 데이터를 주고받으려면 물리적인 연결이 존재해야 하는데, 이 부분에 대한 표준을 해당 계층이 담당하고 있다.

OSI 7계층의 물리 계층, 데이터 링크 계층을 포함한다. 책에서는 LINK 계층이라 설명한다.

인터넷 계층

**인터넷 계층(Internet Layer)**은 네트워크 상에서 데이터(패킷)의 전송을 담당하는 계층으로, 서로 다른 네트워크 간의 통신을 가능하게 하는 역할을 수행한다. 인터넷을 통해 목적지로 데이터를 전송하기 위해서 어떤 경로를 거쳐갈 것인지를 해결하는 것이 인터넷 계층이다. 이 계층에서 사용하는 프로토콜이 IP(Internet Protocol)이다.

IP 자체는 비 연결지향적이며 신뢰할 수 없는 프로토콜이다. 데이터를 전송할 때마다 거쳐야 할 경로를 선택해주지만, 그 경로는 일정치 않다. 특히 데이터 전송 도중에 경로상에 문제가 발생하면 다른 경로를 선택하는데, 이 과정에서 데이터가 손실되거나 오류가 발생할 수도 있고 이를 해결해주지 않는다. 즉, IP는 오류발생에 대한 대비가 되어있지 않은 프로토콜이다.

OSI 7계층의 네트워크 계층에 해당한다. 책에서는 IP 계층이라 설명한다.

전송 계층

**전송 계층(Transport Layer)**은 IP 계층에서 알려준 경로 정보를 바탕으로 데이터의 실제 송수신을 담당한다. 해당 계층에는 TCP, UDP와 같은 프로토콜이 있다. TCP에 대해 추가로 설명하자면, TCP는 신뢰성 있는 데이터의 전송을 담당한다.

TCP가 데이터를 보낼 때 기반이 되는 프로토콜이 IP이다. 그런데 IP는 오직 하나의 데이터 패킷이 전송되는 과정에만 중심을 두고 설계되었다. 따라서 여러 개의 데이터 패킷을 전송한다 하더라도 각각의 패킷이 전송되는 과정은 IP에 의해서 진행되므로 전송의 순서는 물론이고 전송 그 자체를 신뢰할 수 없다. 여기서 TCP가 이러한 데이터 전송 및 흐름에 있어 신뢰성 보장을 담당한다. 결론적으로 말하면 IP의 상위 계층에서 호스트 대 호스트의 데이터 송수신에 신뢰성을 부여하는 역할을 한다.

OSI 7계층의 전송 계층에 해당한다.

어플리케이션 계층

**어플리케이션 계층(Application Layer)**은 응용 계층이라고도 하며, 프로그램의 성격에 따라 클라이언트와 서버간의 데이터 송수신에 대한 프로토콜을 담당한다. 우리가 웹프로그래밍을 하면서 흔히 접하는 여러 서버나 클라이언트 관련 응용 프로그램들이 동작하는 계층이다. 대부분의 네트워크 프로그래밍은 어플리케이션 프로토콜의 설계 및 구현이 상당부분을 차지한다.

어플리케이션 계층 아래의 있는 다른 계층들은 우리가 네트워크 프로그래밍에서 소켓을 생성하면 데이터 송수신 과정에서 자동으로 처리된다. 데이터의 전송경로를 확인하는 과정이나 데이터 수신에 대한 응답의 과정 등이 소켓에 감추어져 있는 것이다. 즉, 우리는 네트워크 프로그램을 작성할 때 이런 영역에 대해 신경쓰지 않고 어플리케이션 계층의 코드만 작성함으로써 필요에 맞는 네트워크 프로그램을 작성할 수 있다.

TCP

3-Way Handshaking

TCP는 연결지향 프로토콜이고, 연결을 만들 때 정확한 전송을 보장하기 위해 상대방 컴퓨터와 사전에 세션을 수립하는 과정을 수행한다. 이것을 3-Way Handshaking이라고 한다.

[SYN] SEQ: 1000, ACK: -

호스트 A가 B에게 연결요청을 한다고 가정하자. A는 B에게 위와 같은 형태의 SYN 메시지를 보낸다. SYN은 Synchronization의 약자로, 데이터 송수신에 앞서 전송되는 '동기화 메시지'라는 의미를 담고 있다. SEQ는 Sequence Number로 메시지의 순서를 나타내는 번호이며, 호스트마다 다른 값을 가질 수 있다.

[SYN+ACK] SEQ: 2000, ACK: 1001

호스트 B는 A로부터 받은 SYN 메시지의 SEQ 값에 1을 더한 값을 ACK에 담고, 자신의 SEQ 역시 포함한 메시지를 A에게 보내준다. ACK는 Acknowledgment의 약자이며, 이러한 형태의 메시지를 SYN+ACK라고 표현한다.

[ACK] SEQ: 1001, ACK: 2001

호스트 A는 다시 호스트 B로부터 받은 메시지에서 ACK값을 확인해본다. 처음 보냈던 SYN 메시지의 SEQ 값은 1000이고, 돌려받은 메시지의 ACK 값이 1001이므로 통신이 정상적으로 오고감을 확인할 수 있다. 그러면 이번에는 SYN+ACK의 SEQ 값에 1을 더한 값을 ACK에 담아서 다시 B에게 보내준다. 또한, 이때 SEQ 값은 이전에 보낸 메시지의 SEQ에 1을 더한 값을 보내준다.

위 예시에서 알 수 있듯이, ACK로 보내는 값은 상대방이 다음번 메시지를 보낼 때의 SEQ 값이다. 이렇게 SEQ와 ACK를 주고받으며, 서로간 메시지를 정상적으로 주고받는지 검증한다.

데이터 송수신

SEQ와 ACK는 실제 데이터를 주고받을 때도 데이터가 정상적으로 송수신되었는지 확인하기 위해 사용된다.

예시를 통해 설명해보자. 호스트 A의 현재 SEQ가 1200이고 보내야 할 데이터가 200바이트일때, 데이터를 100바이트씩 나누어 보내는 상황이라고 가정하자.
우선 호스트 A는 상대방에게 SEQ 1200, 100바이트의 데이터가 담긴 메시지를 보낸다. 상대방은 이 메시지를 정상적으로 받았다면, ACK 1301이 담긴 메시지를 보낸다. 이 값이 나오게 된 이유는 ACK = SEQ + 전송된 바이트 크기 + 1이라는 공식에 의해서이다. 메시지를 온전하게 전부 받았는지 확인하기 위해서 전송된 바이트 크기를 더해주는 것이고, 1을 더해주는 이유는 Handshaking의 경우처럼 다음 번에 전달된 SEQ 번호를 알리기 위함이다.
다음으로 넘어가 상대방이 보낸 ACK 1301 메시지를 받은 호스트 A는 SEQ 1301, 100바이트의 데이터가 담긴 메시지를 다시 상대방에게 보낸다. 이후, 상대방은 ACK 1402 메시지를 보낼 것이고, 호스트 A가 이를 받으면 정상적인 데이터 송수신이 끝이 난다.

그렇다면 데이터가 손실된 경우 어떻게 동작할까? 우선 호스트 A가 메시지를 보냈는데 중간에 문제가 발생하여 상대방이 메시지를 받지 못했다고 가정하자. 그러면 호스트 A는 일정시간이 지나도 보낸 메시지에 대한 ACK 메시지를 받지 못할 것이고, 호스트 A는 재전송을 진행한다. 이렇듯 데이터의 손실에 대한 재전송을 위해, TCP 소켓은 ACK 응답을 요구하는 패킷 전송 시에 타이머를 동작시킨다. 그리고 이 타이머가 Timeout되었을 때 패킷을 재전송한다.

4-Way Handshaking

TCP 소켓은 연결지향 프로토콜이고, 또한 데이터가 모두 전송되어야하는 신뢰성이 중요하다. 그렇기에 소켓을 끊을 때도 별도의 과정이 필요하다. 그냥 연결을 바로 끊어버릴 경우, 상대방이 전송할 데이터가 남아있을 때 문제가 되기 때문이다. 이 연결을 끊는 과정에는 4번의 통신이 필요하기에 4-Way Handshaking이라고 한다.

4-Way Handshaking의 진행과정은 우선 연결 종료를 하고 싶은 호스트 A가 먼저 FIN이 포함된 메시지를 호스트 B에게 보낸다. 이 메시지를 받은 호스트 B는 다시 ACK 메시지를 보내준다. 이후 호스트 B가 전송해야할 메시지를 모두 전송한 경우, 호스트 A에게 FIN 메시지를 보내준다. 이 메시지를 받은 호스트 A는 호스트 B에게 ACK 메시지를 보내주고 연결이 종료된다.

호스트 B가 ACK 메시지를 보내고 FIN 메시지를 다시 보내는 이유는 위 설명에서 유추가능하다. 먼저 보낸 ACK 메시지는 호스트 A에게 FIN 메시지를 잘 받았다는 의미로 보내주는 것이다. 그리고 만약 보내야할 메시지가 아직 남아있는 경우, 이 메시지들을 먼저 전송하기 위해 FIN 메시지를 추후에 보내는 것이다.

마지막으로 호스트 B가 FIN 메시지를 보내기 전에 전송한 패킷이 어떠한 이유로 인해 FIN 메시지보다 늦게 도착하는 상황이 발생한다면 어떻게 될까? 호스트 A가 FIN을 받자마자 세션을 종료시킨다면, 뒤늦게 도착하는 메시지가 소멸되고 데이터는 유실된다. 다른 예시도 있다. 만약 호스트 A가 호스트 B로부터 FIN 메시지를 받고, ACK 메시지를 다시 보내주었다. 그런데 ACK가 중간에 소실되었다면 호스트 B는 자신이 보낸 FIN 데이터가 소실되었다고 생각하고 재전송을 할 것이다. 하지만 호스트 A의 세션은 이미 완전히 종료된 상태이기에, 호스트 B는 영원히 ACK 메시지를 받을 수 없게 된다.
TCP는 이러한 현상을 막기 위해 호스트 A는 FIN을 수신하더라도 일정시간(Default 3분)동안 세션을 남겨놓고 잉여 메시지를 기다리는 과정을 거치게 되는데 이 과정을 "TIME_WAIT"라고 한다. 일정시간이 지나면, 세션을 만료하고 연결을 종료시키며, "CLOSE" 상태로 바뀌며 완전히 연결이 끝이 난다.

Nagle 알고리즘

Nagle 알고리즘은 앞서 전송한 데이터에 대한 ACK 메시지를 받아야만, 다음 데이터를 전송하는 알고리즘이다. 기본적으로 TCP 소켓은 Nagle 알고리즘을 적용해서 데이터를 송수신한다. 때문에 ACK가 수신될 때까지 최대한 버퍼링을 해서 데이터를 전송한다.

Nagle 알고리즘을 적용하지 않은 상태에서 문자열 "nagle"을 전송하는 예를 들어보자. 극단적인 예시로 한 문자씩 출력 버퍼로 전달된다고 가정하면, 문자 'n'부터 'e'까지 5개의 패킷이 전송될 것이다. 그렇다면 ACK 메시지까지 포함하여 총 10개의 메시지가 오고간다. 이는 네트워크 트래픽에 좋지 않은 영향을 준다. 여기에 Nagle 알고리즘을 적용한다면, 첫번째 문자 'n'을 담은 메시지를 보내고 이에 대한 ACK 메시지를 받을 때까지 나머지 4개의 문자는 출력 버퍼에서 쌓인다. 이후 첫번째 ACK 메시지를 받으면 출력 버퍼에 쌓여있던 4개의 문자가 하나의 메시지에 포함되어 전송된다. 그렇다면 주고받은 메시지는 ACK 메시지를 포함하여 4개 밖에 되지 않는다. 예시에서 보이듯 네트워크의 효율적인 사용을 위해서는 Nagle 알고리즘을 반드시 적용해야 한다.

하지만 Nagle 알고리즘이 항상 좋은 것은 아니다. 전송하는 데이터의 특성에 따라 Nagle 알고리즘을 적용하지 않아도 트래픽의 차이가 크지 않으면서 전송 속도는 더 빠를 수 있다. 용량이 큰 파일 데이터의 전송이 대표적인 예시이다. 파일 데이터를 출력 버퍼로 밀어 넣는 작업은 시간이 걸리지 않고, 때문에 Nagle 알고리즘을 적용하지 않아도 출력 버퍼를 거의 꽉 채운 상태에서 패킷을 전송하게 된다. 따라서 패킷의 수가 크게 증가하지 않으면서도 ACK를 기다리지 않고 연속해서 데이터를 전송하니 전송속도도 놀랍게 향상된다.

즉 일반적인 경우 Nagle 알고리즘을 사용하는 것이 좋지만, 데이터의 특성에 따라 적절히 판단하여 Nagle 알고리즘을 사용하지 않을 때도 있어야 한다.

UDP

UDP는 전송 계층의 프로토콜 중 하나이다. 신뢰성 있는 데이터의 송수신을 위해 '흐름 제어(Flow Control)'을 하는 TCP와 달리 UDP는 흐름 제어를 하지 않는다. 그렇기 때문에 일반적으로 UDP가 TCP보다 빠르다. 그 이유는 TCP가 데이터 송수신 이전, 이후에 거치는 핸드쉐이킹 과정과 데이터 송수신 과정에서 거치는 흐름제어 때문이다. 이 말은 다르게 말하면 이 과정을 최대한 줄일 수 있으면, TCP도 UDP 못지않은 속도를 낼 수 있다는 뜻이다. 한번에 보내는 데이터의 크기를 크게해서 보낸다거나 하는 식으로 말이다.

UDP는 흐름 제어를 하지 않는데, 그렇다면 UDP의 역할은 어디까지일까? 우선 호스트 A에서 전송된 UDP 패킷이 호스트 B에게 전달되도록 하는 것은 IP(인터넷 계층)의 역할이다. 이렇게 전달된 UDP 패킷을 호스트 B에 존재하는 UDP 소켓 중 하나에게 최종 전달하는 것이 UDP의 역할이다. 즉, UDP의 역할 중 가장 중요한 것은 호스트로 전달된 패킷을 PORT 정보를 참조하여 최종 목적지인 UDP 소켓에 전달하는 것이다.

UDP는 흐름 제어를 하지 않기에 데이터의 신뢰성은 보장할 수 없지만, 그 성능은 빠르다. (물론 최근 네트워크 기술들의 발전으로 UDP도 데이터가 손실되는 경우가 거의 없다.) 즉, 압축파일을 전송하는 것과 같이 데이터의 일부만 손상되도 문제가 발생하는 경우, 반드시 TCP를 기반으로 송수신해야한다. 그러나 인터넷으로 실시간 영상 및 음성을 스트리밍하는 경우 데이터의 일부가 손상되더라도 큰 문제가 되지 않는다. 하지만 속도는 중요한 요소인데, 실시간으로 멀티미디어 데이터를 받아야하기에 속도가 빨라야한다. 즉, 이러한 경우가 UDP를 기반으로 통신하기에 좋은 상황일 수 있다. 일반적으로 스트리밍 데이터는 조금씩 나누어 작게 자주 보내기 때문에, TCP로 이를 구현할 경우 UDP에 비해 느릴 가능성이 크다.

DNS

IP 주소와 도메인 이름 사이에서의 변환을 수행하는 시스템을 **DNS(Domain Name System)**이라 한다.

DNS 서버

브라우저 주소창에 도메인을 입력하면, DNS 서버에 먼저 도메인 이름을 IP 주소로 변환해달라는 요청을 보낸다. 모든 컴퓨터에는 디폴트 DNS 서버의 주소가 등록되어 있기에, 해당 서버를 통해 도메인 이름에 대한 IP 주소를 얻게 된다.

디폴트 DNS 서버가 모든 도메인의 IP 주소를 알고 있지는 않다. 모르는 도메인에 대한 요청이 들어올 경우, 한 단계 상위 계층에 있는 DNS 서버에 물어본다. 이런 식으로 계속 올라가다 보면 최상위 DNS 서버인 Root DNS 서버에 도달하게 되고, Root DNS 서버는 해당 질문을 누구에게 전달해야할지 알고 있다. 그래서 자기보다 하위에 있는 DNS 서버에 물어보게 되고 결국은 IP 주소를 알아낸다. 이렇듯 DNS는 계층적으로 관리되는 일종의 분산 데이터베이스 시스템이다.

IP 주소와 도메인 이름의 변환

사용자가 도메인 이름을 쓰는 이유는 이해가 쉽지만, 프로그램에서 요청을 보낼 때도 도메인 이름을 써야할 이유가 있을까? 이에 대한 해답은 IP 주소와 도메인 이름 중 무엇이 더 변경되기 쉬울까를 생각해보면 간단히 답이 나온다. 어떠한 서버의 IP 주소가 변경되는 일은 드문 일이 아니다. 특히나 ISP 서비스, 클라우드 환경 등을 사용하는 경우 IP 주소의 변경은 언제든 발생할 수 있다. 만약 프로그램에서 IP 주소를 사용하는 경우, 이렇게 IP 주소가 변경된다면 바로 문제가 생길 것이다. IP 주소와 달리 도메인 이름의 경우 한번 등록해두면 평생 유지가 가능하다. IP 주소가 변경되더라도 DNS를 업데이트하면 되기에 도메인 주소는 변경되지 않는다. 즉, 프로그램에서 도메인 이름을 사용하는 경우, IP 주소가 변경에서 자유로워진다.

다중접속 서버의 구현방법들

멀티 프로세스

다수의 프로세스를 생성하는 방식으로 다중접속이 가능하도록 한다.

프로세스들은 기본적으로 메모리를 공유하지 않기 때문에, 프로세스간 통신(IPC, Inter Process Communication)을 하려면 특정한 방식들을 사용해야 한다. 그 예시로는 Pipe, Named Pipe, Message Queue 등이 있다.

멀티 프로세스 기반의 구현은 단점이 몇가지 있다.

  • 프로세스 생성이라는 부담스러운 작업과정을 거친다.
  • 두 프로세스 사이에서의 데이터 교환을 위해 별도의 IPC 기법을 적용해야 한다.
  • 컨텍스트 스위칭(Context Switching) 비용이 크다.
    • 컨텍스트 스위칭이란 여러 개의 프로세스가 실행되고 있을 때, 기존에 실행하던 프로세스를 중단하고 다른 프로세스를 실행하는 것
    • 이때, 현재 실행 중인 프로세스의 정보를 내리고, 실행될 프로세스의 정보를 메모리에 올리는 작업의 비용이 상당함

멀티 플렉싱

입출력 대상을 묶어서 관리하는 방식으로 다중접속이 가능하도록 한다. 멀티 플렉싱은 하나의 통신채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술이다.

멀티 쓰레드

클라이언트의 수만큼 쓰레드를 생성하는 방식으로 다중접속이 가능하도록 한다. 쓰레드는 멀티 프로세스의 특성은 유지하면서 단점을 어느정도 극복하기 위해 나온 기술이다. 이는 멀티 프로세스의 여러 단점을 최소화하기 위해서, 설계된 일종의 '경량화 된 프로세스'이다. 그렇기에 쓰레드는 프로세스와 비교해서 다음의 장점을 지닌다.

  • 쓰레드의 생성 및 컨텍스트 스위칭은 프로세스의 생성 및 컨텍스트 스위칭보다 빠르다.
  • 쓰레드 사이에서의 데이터 교환에는 특별한 기법이 필요치 않다.

프로세스의 메모리 구조는 전역변수가 할당되는 '데이터 영역', 동적 할당이 이루어지는 'Heap', 함수의 실행에 사용되는 'Stack'으로 이루어진다. 프로세스의 경우 이를 완전히 별도로 유지하기에, 프로세스의 복제에는 이 메모리들이 모두 복제된다. 그러나 둘 이상의 실행흐름을 갖는 것이 목적이라면, Stack 영역만을 분리함으로써 그 목적을 달성할 수 있다. 이 경우 얻게되는 장점을 다음과 같다.

  • 컨텍스트 스위칭 시 데이터 영역과 Heap은 올리고 내릴 필요가 없다.
  • 데이터 영역과 Heap을 이용해서 데이터를 교환할 수 있다.

이러한 장점을 얻기위해 쓰레드가 등장했다. 쓰레드는 별도의 실행흐름을 유지하기 위해서 Stack 영역만 독립적으로 유지하며, 나머지 영역은 공유한다.

프로세스는 운영체제 관점에서 별도의 실행흐름을 구성하는 단위이며, 쓰레드는 프로세스 관점에서 별도의 실행흐름을 구성하는 단위이다.

쓰레드는 데이터, Heap 영역의 메모리를 공유한다. 즉, 멀티 쓰레드 환경에서 한 자원에 대해 여러 쓰레드가 동시에 접근하게 될 수도 있는데, 이는 경우에 따라 문제가 될 수 있다. 이러한 개념에서, 임계 영역(Critical Section)이란 둘 이상의 쓰레드가 동시에 접근해서는 안되는 공유 자원을 접근하는 코드가 포함된 영역을 말한다. 멀티 쓰레드 환경에서는 이 임계 영역을 잘 다루는 것이 중요하다.

이런 임계 영역 문제와 관련해서 함수를 '쓰레드에 안전한 함수(Thread-safe Function)'와 '쓰레드에 불안전한 함수(Thread-unsafe Function)'으로 나눌 수 있다. Thread-safe 함수는 둘 이상의 쓰레드에 의해서 동시에 호출되어도 문제를 일으키지 않는 함수를 뜻하며, Thread-unsafe 함수는 문제가 발생할 수 있는 함수를 뜻한다. 하지만 이것은 임계 영역의 유무를 뜻하는 것은 아닌데, 임계 영역이 포함되더라도 이에 적절한 조치가 되어있다면 Thread-safe 함수가 될 수 있다.

여러 쓰레드가 동시에 특정 자원에 접근하지 못하도록 하는 것을 동기화(Synchronization)이라고 한다. 동기화를 하는 방식에는 Mutex, Semaphore, Monitor 등의 방식이 존재한다.

동기화가 필요한 상황은 다음 두 가지 측면에서 생각해볼 수 있다.

  • 동일한 메모리 영역으로의 동시접근이 발생하는 상황
  • 동일한 메모리 영역에 접근하는 쓰레드의 실행순서를 지정해야하는 상황

동기화를 잘 하는 것은 굉장히 중요한데, 그렇다고 아무 생각없이 동기화 코드를 넣을 경우 Dead-lock 문제에 빠질 수 있다. 데드락은 쓰레드가 원하는 자원(Lock도 포함)을 얻지 못해 무한정 자원을 기다리는 교착 상태를 의미한다. 어느 한 쓰레드가 영원히 lock을 반환하지 않거나 혹은 서로가 서로가 가지고 있는 Lock이 필요한 고착 상태의 빠지는 경우 등 다양한 상황에서 데드락 문제가 발생할 수 있다.

Mutex

뮤텍스(Mutex)란 Mutual Exclusion의 약자로 상호배제라는 뜻이며, 이는 쓰레드의 동시접근을 허용하지 않는다는 의미가 있다. Mutex 혹은 Lock이라고 부르는 자물쇠를 임계영역에 들어갈 때 잠그고 들어간 뒤, 임계영역에서 나올 때 풀고 들어가는 것으로 비유를 할 수 있다. Mutex에서는 자물쇠와 열쇠는 반드시 하나씩만 존재하기에, 어느 한 쓰레드가 임계영역으로 들어가며 자물쇠를 잠그면, 다른 쓰레드는 임계영역으로 들어올 수 없어진다. 이후 해당 쓰레드가 임계영역을 나가며 자물쇠를 풀면, 다른 쓰레드가 접근 순서에 맞게 차례대로 들어올 수 있다.

Semaphore

세마포어(Semaphore)는 뮤텍스와 매우 유사하다. 큰 차이점은 세마포어는 뮤텍스와 다르게 지정한 개수 만큼의 쓰레드가 동시에 접근할 수 있다. 만약 지정한 개수가 1개라면, 뮤텍스와 동일하게 한 번에 한 개의 쓰레드만이 접근가능하며, 이 때의 세마포어를 바이너리 세마포어(Binary Semaphore)라고 한다.

세마포어는 세마포어 값(Semaphore Value)를 통해서 동기화를 관리한다. 임계 영역에 들어가는 쓰레드가 세마포어 값을 1 낮추고 들어가고, 1 올리면서 나온다. 그런데, 이때 세마포어 값은 0 미만으로 감소할 수 없기에, 세마포어 값이 0이라면 대기한다. 이러한 방식으로 동기화를 하기에 초기 세마포어 값이 1이라면 한 번에 한 쓰레드만 들어갈 수 있는 바이너리 세마포어가 되고, 2라면 한 번에 2개의 쓰레드가 들어갈 수 있는 세마포어가 된다.

그런데 세마포어의 경우 동시접근 동기화보다는 실행 순서를 동기화하는 성향으로 쓰는 경향이 강하다. 이에 대한 예시는 세마포어 예시 코드를 확인해보자.

멀티캐스트

멀티캐스트(Multicast)란 한 번의 송신으로 특정 그룹에 가입되어 있는 모든 컴퓨터들에게 메시지를 전송하는 것을 말한다. 멀티캐스트의 데이터 전송 특성은 다음과 같다.

  • 멀티캐스트 서버는 특정 멀티캐스트 그룹을 대상으로 데이터를 딱 한번 전송한다.
  • 해당 멀티캐스트 그룹에 속하는 클라이언트는 모두 데이터를 수신한다.
  • 멀티캐스트 그룹의 수는 IP주소 범위 내에서 얼마든지 추가가 가능하다.

여기서 말하는 멀티캐스트 그룹이란 클래스 D에 속하는 IP주소(224.0.0.0 ~ 239.255.255.255)를 의미한다.

멀티캐스트는 UDP를 기반으로 한다. 그렇기에 멀티캐스트 패킷 역시 그 형태가 UDP 패킷과 동일하며, 단지 라우터들이 이 패킷을 복사해서 다수의 호스트에게 전달하는 것이다. 위에서 말한 것처럼 서버는 단 한번의 메시지만 보내고 이를 라우터가 적절히 복사하여 클라이언트에게 전달해주기 때문에, 트래픽 측면에서 이점을 가져올 수 있다. 이런 트래픽의 이점 때문에 멀티캐스트 방식의 데이터 전송은 "멀티미디어 데이터의 실시간 전송"에 주로 사용된다.

멀티캐스트에서 TTL(Time to Live)은 중요한 요소 중 하나이다. TTL은 정수로 표현되며, 멀티캐스트 패킷이 라우터를 하나 거칠 때마다 1씩 감소된다. 그리고 이 값이 0이 되면 패킷은 더 이상 전달되지 못하고 소멸된다. 따라서 TTL을 너무 적게 설정하면 목적지에 도달하지 못하는 문제가 발생할 수 있고, 너무 크게 설정하면 네트워크 트래픽에 좋지 못한 영향을 줄 수 있다.

브로드캐스트

브로드캐스트(Broadcast)는 한번에 여러 호스트에게 데이터를 전송한다는 점에서 멀티캐스트와 유사하다. 그러나 전송이 이루어지는 범위에서 차이가 난다. 멀티캐스트는 서로 다른 네트워크상에 존재하는 호스트라도 멀티캐스트 그룹에 가입만 되어있으면 데이터의 수신이 가능하다. 하지만 브로드캐스트는 동일한 네트워크로 연결되어 있는 호스트로만 데이터의 전송 대상이 제한된다.

브로드캐스트는 UDP를 기반으로 데이터를 송수신하며, 데이터 전송 시 사용되는 IP주소의 형태에 따라서 Directed 브로드캐스트, Local 브로드캐스트로 구분된다.

Directed 브로드캐스트의 IP주소는 네트워크 주소를 제외한 나머지 호스트 주소의 비트를 전부 1로 설정해서 얻을 수 있다. 예를 들어 네트워크 주소가 192.12.34인 네트워크에 연결된 모든 호스트에게 데이터를 전송하려면 129.12.34.255로 데이터를 전송하면 된다. 이렇듯 특정 지역의 네트워크에 연결된 모든 호스트에게 데이터를 전송하려면 Directed 브로드캐스트 방식으로 데이터를 전송하면 된다.

Local 브로드캐스트를 위해서는 255.255.255.255라는 IP주소가 특별히 예약되어 있다. 예를 들어 네트워크 주소가 192.32.24인 네트워크에 연결되어 있는 호스트가 IP주소 255.255.255.255를 대상으로 데이터를 전송하면, 192.32.24로 시작하는 IP주소의 모든 호스트에게 데이터가 전달된다.

HTTP 개요

웹 서버란 HTTP 프로토콜을 기반으로 웹 페이지에 해당하는 파일을 클라이언트에게 전송하는 역할을 서버를 의미한다. 여기서 HTTP란 Hypertext Transfer Protocol의 약자이다. 그리고 Hypertext란 클라이언트의 선택에 따라서 이동이 가능한 조직화된 정보를 의미한다. 마지막으로 HTTP 프로토콜은 Hypertext의 전송을 목적으로 설계된 어플리케이션 레벨의 프로토콜이다. 정리하자면 웹 서버는 HTTP 프로토콜을 기반으로 Hypertext를 전송하는 서버이다.

HTTP

HTTP의 프로토콜의 요청 및 응답방식은 인터넷이라는 환경하에서 많은 클라이언트에게 서비스할 수 있도록 간단하게 설계되어 있다. 그 설계는 클라이언트의 요청에 응답을 하고 나서, 연결을 바로 끊는 방식이다. 즉, 서버는 클라이언트의 상태정보를 유지하지 않는다. 앞서 요청했던 클라이언트가 다시 요청을 해와도, 앞서 요청했던 클라이언트라는 것을 인식하지 못한다. 이러한 특징 때문에, HTTP 프로토콜은 상태가 존재하지 않는 Stateless 프로토콜이라고 한다. 단, 이러한 Stateless한 HTTP의 특징을 보완하고자, 쿠키와 세션이라는 상태정보의 유지를 가능하도록 하는 기술이 개발되어 있다.

HTTP Request Message의 구성은 Start Line, HTTP Header, Body로 구성되어 있다. 자세한 설명은 생략
HTTP Response Message의 구성은 Start Line, HTTP Header, Body로 구성되어 있다. 자세한 설명은 생략
HTTP Header와 Body 사이에는 공백 라인이 있어, Header와 Body가 혼동될 여지를 없앤다.

C 네트워크 프로그래밍

TCP 소켓

TCP 소켓을 통해 write 함수를 호출할 경우 데이터를 출력버퍼로 넣고, read 함수를 호출할 경우 입력버퍼로부터 저장된 데이터를 읽어들인다. 즉, 실제 데이터가 전송되거나 수신되는 시점은 write, read 함수를 호출할 때가 아니다. write 함수를 호출하면 출력버퍼에 데이터가 전달되고 상황에 맞게 적절히 데이터를 상대방의 입력버퍼로 전송한다. 그러면 상대방은 read 함수를 호출해서 입력버퍼에 저장된 데이터를 읽게되는 것이다.

입출력 버퍼의 특성은 다음과 같다.

  • 입출력 버퍼는 TCP 소켓 각각에 대해 별도로 존재한다.
  • 입출력 버퍼는 소켓 생성시 자동으로 생성된다.
  • 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속해서 전송이 이루어진다.
  • 소켓을 닫으면 입력버퍼에 남아있는 데이터는 소멸된다.

데이터가 입출력 버퍼를 통해 전달된다는 사실을 알았다. 그렇다면 클라이언트의 입력버퍼 크기보다 큰 데이터를 전송할 경우 어떻게 될까? TCP는 데이터의 흐름을 컨트롤하고, 입력버퍼의 크기를 초과하는 분량의 데이터 전송은 하지 않는다. TCP는 '슬라이딩 윈도우(Sliding Window)'라는 프로토콜을 사용하여 클라이언트의 입력 버퍼에 수용할 수 있을 정도로만 데이터를 전송해간다. 예를들어 클라이언트의 입력 버퍼가 50바이트이고, 보내야할 데이터가 100바이트라고 가정하자. 그렇다면 가장 처음 50바이트의 데이터가 전송되고, 이후 입력 버퍼가 20바이트만큼 비었다면 그 다음 20바이트를 보내주는 방식으로 동작한다.

Half-close

close 함수의 호출을 완전 종료를 의미한다. 완전 종료라는 것은 데이터를 전송하는 것 뿐만 아니라 수신하는 것 조차 더 이상 불가능한 상황을 의미한다. 때문에 한쪽에서의 일방적인 close 호출은 경우에 따라 위험할 수 있다. 상대방이 보낸 데이터가 전송되고 있을 때 close 함수가 호출되면, 해당 호스트는 이 데이터를 받을 수 없다. 이런 문제를 해결하기 위해 데이터의 송수신에 사용되는 스트림의 일부만 종료(Half-close)하는 방법이 제공된다.

소켓을 통해 두 호스트가 연결되면, 상호간 데이터의 송수신이 가능한 '스트림이 형성된 상태'가 된다. 이 스트림은 한 방향으로만 데이터를 전송할 수 있기에, 양방향 통신을 위해서는 두 개의 스트림이 필요하다. 즉, 각 호스트 별로 입력 스트림과 출력 스트림이 형성되는 것이다. 기존 close 함수 호출은 한번에 이 두 스트림을 모두 끊어버리지만, Half-close 방식은 이 중 하나의 스트림만 끊게 된다.

C 네트워크 프로그래밍에서 Half-close는 shutdown 함수로 수행할 수 있다.

UDP 소켓

UDP 서버, 클라이언트는 비 연결지향형이기에 TCP와 달리 연결된 상태로 데이터를 송수신하지 않는다. 때문에 TCP와 달리 연결 설정 과정이 필요없고, UDP 소켓의 생성과 데이터 송수신 과정만 존재한다.

또한 TCP에서는 소켓과 소켓의 관계가 1대1이었던 것과 달리 UDP에서는 서버건 클라이언트건 하나의 소켓만 있으면 된다. 또한 UDP 소켓 하나만 있다면, 여러 호스트를 대상으로 데이터의 송수신이 가능하다.

TCP 소켓은 목적지에 해당하는 소켓과 연결된 상태이기 때문에, 주소 정보를 따로 추가할 필요가 없었다. 그러나 UDP는 연결상태를 유지하지 않으므로, 데이터를 전송할 때마다 반드시 목적지의 주소정보를 별도로 추가해야 한다.

또한, UDP는 데이터의 경계가 있다. 이 말은 상대방이 데이터를 3번 보내면, 나도 3번의 거쳐 받아야하는 것이다. TCP의 경우 입력 버퍼에 쌓아놨다가 한번에 받을 수 있는 것과는 차이가 있다. 비슷한 예시로 상대방이 데이터를 한번에 보내면, 이를 한번에 받아야하지 여러 번 나누어 받을 수 없다.

Connected UDP 소켓

기본적으로 UDP 소켓은 목적지 정보가 등록되어 있지 않은 unconnected 소켓이다. 매번 메시지 전송을 할 때마다 UDP 소켓에 목적지의 IP, PORT를 등록하고, 데이터를 전송한 다음, 소켓에 등록된 목적지 정보를 삭제하는 과정을 거친다. 그러나 같은 호스트와 반복해서 메시지를 주고받아야 하는 경우, 매번 목적지 정보를 등록하고 삭제하는 과정이 불필요하다. 이럴 때 UDP 소켓을 목적지 정보가 등록된 connected 소켓으로 만드는 것이 효율적일 것이다.

C 시스템 프로그래밍에서 connect 함수를 호출해서 Connected UDP 소켓을 만들 수 있다. TCP 소켓에서는 해당 함수가 연결을 만들었지만, UDP 소켓에서는 단순히 목적지를 등록하는 것에서 끝난다.

소켓의 옵션

getsockoptsetsockopt로 소켓의 속성을 조회하고 이를 변경할 수 있다.

SO_REUSEADDR 옵션에 1(True) 값을 주면, Time-wait 상태에 있는 소켓에 할당된 PORT 번호를 새로 시작하는 소켓에 할당되게끔 변경할 수 있다. 기본값은 0(False)이다.
TCP_NODELAY 옵션에 1(True) 값을 주면, Nagle 알고리즘을 사용하지 않는다. 기본값은 0(False)이다.

멀티 프로세스

fork 함수를 사용해서 자식 프로세스를 생성한다.
wait, waitpid 함수를 사용해서 자식 프로세스의 종료를 기다린다.
signal, sigaction 함수를 사용해서 이벤트 자식의 종료 이벤트가 발생했을 때, 실행할 콜백 함수를 설정할 수 있다.
pipe 함수를 사용해서 다른 프로세스간 데이터를 주고받을 수 있다.

멀티 플렉싱

select 함수를 사용하여 한 곳에서 여러 개의 파일 디스크립터를 모아놓고, 이들의 이벤트를 동시에 받을 수 있다. 받을 수 있는 이벤트는 다음과 같다.

  • 수신한 데이터를 지니고 있는 소켓이 있는가?
  • 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?
  • 예외상황이 발생한 소켓은 무엇인가?

사실 select는 오래 전에 개발된 멀티플렉싱 기법이다. 때문에 허용할 수 있는 동시접속자의 수의 한계가 명확하다. 그렇기에 웹 기반의 서버개발이 주를 이루는 오늘날의 개발환경에서는 적절치 않다.

select 기반의 IO 멀티 플렉싱에는 불편한 점이 여럿 있다. 그 중 가장 큰 두가지는 다음 두가지이다.

  • select 함수호출 이후에 모든 파일 디스크립터를 대상으로 하는 반복문이 필요하다.
  • select 함수를 호출할 때마다 이벤트에 대한 정보들을 인자로 매번 전달해야 한다.

select 함수는 호출되고 나면, 이벤트가 발생한 파일 디스크립터만 따로 묶어 알려주지 않는다. 인자로 전달한 fd_set형 변수의 변화를 통해 이벤트가 발생한 파일 디스크립터를 알려준다. 그렇기에 매번 모든 파일 디스크립터를 대상으로 하는 반복문이 필요하다. 또한 이 과정에서 관찰대상을 묶어놓은 fd_set형 변수에 변화가 생기기 때문에, select 함수를 호출할 때마다 새롭게 관찰대상의 정보를 전달해야 한다.

이 중, 성능의 더 큰 영향을 미치는 것이 매번 새롭게 관찰대상의 정보를 전달하는 것이다. 이는 select 함수를 호출할 때마다 관찰대상에 대한 정보를 매번 운영체제에게 전달하는 것을 의미한다. 응용 프로그램에서 운영체제에게 데이터를 전달하는 것은 프로그램에 많은 부담이 따르고, 이는 코드의 개선으로 덜 수 있는 유형의 부담이 아니기에 성능에 치명적인 약점이 된다.

이 문제를 해결하려면, 운영체제에게 관찰대상의 정보를 딱 한번만 알려주고, 관찰대상의 범위 또는 내용에 변경이 있을때 변경 사항만 알려주도록 바꿀 필요가 있다. 단, 이는 운영체제가 이러한 방식을 지원할 때 가능한 방식이고, 때문에 운영체제 별로 지원여부도 다르고 지원방식에도 차이가 있다. 리눅스에서 지원하는 방식을 가리켜 epoll이라 하고, 윈도우에서는 IOCP라고 한다.

select 함수의 단점을 극복한 epoll에는 다음의 장점이 있다. 이는 앞서 말한 select 함수의 단점에 상반된 특징이기도 하다.

  • 이벤트의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
  • select 함수에 대응하는 epoll_wait 함수호출 시, 관찰대상의 정보를 매번 전달할 필요가 없다.

select 함수의 단점을 커버하는 epoll이라는 방식이 있지만, select 함수가 전혀 필요 없는 것은 아니다. 우선 epoll 방식은 리눅스에서만 지원된다. 이렇듯 개선된 IO 멀티플렉싱 모델은 운영체제 별로 호환되지 않는다. 반면 select 함수는 대부분의 운영체제에서 지원한다. 따라서 다양한 운영체제에서 운영이 가능해야한 서버이면서, 서버의 접속자 수가 많지 않은 경우 select 함수를 사용하여도 좋은 선택일 수 있다.

레벨 트리거(Level Trigger)와 엣지 트리거(Edge Trigger)

레벨 트리거, 엣지 트리거는 전기전자에서 사용하는 개념이지만 시스템 프로그래밍, 네트워크 프로그래밍 등 다양한 곳에서 적용할 수 있다.

레벨 트리거는 어떠한 상태가 조건에 부합할 때, 계속해서 이벤트를 트리거한다. 예를 들어, 1과 0의 값을 가질 수 있는 변수가 1인 상태를 체크하기 위해 레벨 트리거를 사용한다고 가정해보자. 그렇다면, 해당 변수의 값이 0이라면 이벤트가 트리거되지 않고, 1이라면 지속적으로 이벤트가 트리거된다.
엣지 트리거는 상태가 변하는 순간에 이벤트를 트리거한다. 위의 예시를 다시 가져와보자. 해당 변수의 상태는 0에서 1로 변하거나 1에서 0으로 변할 수 있다. 엣지 트리거는 이 변하는 순간에 이벤트를 트리거하는 것이다.

이것을 네트워크 프로그래밍, 그 중에서도 입력 버퍼에 적용해보자. 그렇다면 레벨 트리거는 입력 버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 트리거된다. 이와 달리 엣지 트리거는 비어있는 입력 버퍼에 데이터가 추가되는 순간과 데이터가 있던 입력 버퍼에서 데이터가 모두 없어지는 순간에만 이벤트를 트리거한다.

엣지 트리거는 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있다는 큰 장점이 있다. 예를 들어, 서버가 클라이언트 A, B, C에게 데이터를 수신받고, 수신한 데이터를 A, B, C 순으로 조합한 뒤, 조합한 데이터를 임의의 호스트에게 준다고 가정하자. 클라이언트 A, B, C가 항상 차례로 접속해서 차례대로 데이터를 주고, 데이터를 수신할 클라이언트 역시 이 데이터를 즉시 받을 수 있도록 대기하고 있다면 서버의 구현이 간단해진다. 하지만 그런 일은 현실적으로 자주 발생하지 않는다. 클라이언트들이 데이터의 순서에 상관없이 데이터를 서버에 전송하는 상황이 발생하거나, 데이터를 수신할 클라이언트가 접속 조차 하지 않은 상황이 있을 수도 있다. 이러한 상황에서 입력버퍼에 데이터가 수신된 상황일 때, 이를 읽어 들이고 처리하는 시점을 서버가 결정할 수 있도록 하는 것은 서버 구현에 엄청난 유연성을 제공한다.

멀티 쓰레드

멀티 쓰레드 환경으로 프로그램을 작성할 때, 다행히 기본적으로 제공되는 대부분의 표준함수들은 쓰레드에 안전하다. 또한, 쓰레드에 불안전한 함수가 정의되어 있어도, 같은 기능을 갖는 쓰레드에 안전한 함수가 정의되어 있다. 예를들어, gethostbyname이라는 함수는 쓰레드에 불안전하지만, 쓰레드에 안전한 함수인 gethostbyname_r이 추가로 정의되어 있다. 일반적으로 쓰레드에 안전한 형태로 재구현된 함수의 이름에는 _r이 붙는다. (리눅스 기준)

그렇다면 두 이상의 쓰레드가 동시에 접근가능한 코드블록에서는 기존 함수들 대신 _r이 붙는 함수를 호출해야 한다. 하지만 이를 개발자가 직접 바꾸어주기에는 많은 수고가 들어간다. 그렇기에 헤더파일 선언 이전에 매크로 _REENTRANT를 정의하면, 컴파일러가 자동으로 변경해준다. 혹은 이것 말고도 컴파일을 할 때, -D_REENTRANT 옵션을 넣어주는 방식으로도 가능하다.

리눅스의 쓰레드는 처음 호출하는, 쓰레드의 main 함수를 반환했다고 해서 자동으로 소멸되지 않는다. 때문에 pthread_join 혹은 pthread_detach 함수의 호출로 쓰레드의 소멸을 직접 명시해야하고, 그렇지 않으면 쓰레드에 의해 할당된 메모리 공간이 계속 남아있게 된다. pthread_join 함수는 호출되면 쓰레드의 종료를 대기하고, 쓰레드의 소멸을 유도한다. 그러나 이 함수의 문제점은 쓰레드가 종료될 때까지 블로킹 상태에 놓이는 것이다. 이와 달리 pthread_detach 함수는 인자로 넣은 쓰레드가 종료됨과 동시에 소멸되도록 유도한다. 함수명에서 유추할 수 있듯이 이 함수로 detach시킨 쓰레드를 대상으로는 pthread_join 함수의 호출이 불가능해진다.

입출력 함수들

write, read : 가장 기본적인 입출력 함수

send, recv : 조금 더 소켓에 특화된 입출력 함수

  • 마지막 인자로 데이터 송수신시 적용할 수 있는 옵션 정보를 넣을 수 있다.
    • MSG_OOB : 긴급 데이터(Out-of-band data)의 전송을 위한 옵션
    • MSG_PEEK : 입력 버퍼에 수신된 데이터의 존재 유무 확인을 위한 옵션
    • MSG_DONTROUTE : 데이터 전송과정에서 라우팅 테이블을 참조하지 않을 것을 요구하는 옵션. 따라서 로컬 네트워크 상에서 목적지를 찾을 때 사용함
    • MSG_DONTWAIT : 입출력 함수의 호출과정에서 블로킹 되지 않을 것을 요구함. 즉 Non-Blocking IO를 위한 옵션
    • MSG_WAITALL : 요청한 바이트 수에 해당하는 데이터가 전부 수신될 때까지, 호출된 함수가 반환되는 것을 막기 위한 옵션

readv, writev : 데이터 송수신의 효율성을 향상시키는데 도움을 주는 입출력 함수 두 함수의 기능을 한마디로 정리하면, 데이터를 모아서 전송하고 모아서 수신하는 기능의 함수이다. 즉, writev 함수를 사용하면 여러 버퍼에 나뉘어 저장되어 있는 데이터를 한번에 전송할 수 있고, 반대로 readv 함수를 사용하면 데이터를 여러 버퍼에 나눠서 수신할 수 있다.

MSG_OOB

MSG_OOB의 OOB는 Out Of Band를 의미한다. 그리고 Out-pf-band data는 전혀 다른 통신 경로로 전송되는 데이터를 의미한다. 이 뜻에서 알 수 있듯이 Out-of-band 형태로 데이터가 전송되려면, 별도의 통신 경로가 확보되어서 고속으로 데이터가 전달되어야 한다. 그러나 TCP에서는 별도의 통신 경로를 제공하지 않고, 단순히 Urgent mode라는 것을 이용해서 데이터를 전달해준다.

이 Urgent mode의 동작은 패킷의 TCP 헤더에 URGURG Pointer라는 헤더 필드의 값에 의해 좌우된다. URG의 값이 1이면 긴급 메시지가 존재하는 패킷이라는 뜻이고, URG Pointer 헤더의 값은 Urgent Pointer의 위치(오프셋)을 나타낸다. 그리고 이 Urgent Pointer의 바로 앞에 있는 값이 긴급 메시지의 값이 된다. 예를 들어 패킷의 데이터가 369이고 URG가 1, URG Pointer가 3이라면, 9라는 값이 긴급 메시지의 값이 된다.

MSG_PEEK & MSG_DONTWAIT

MSG_PEEK 옵션은 MSG_DONTWAIT 옵션과 함께 설정되어 입력버퍼에 수신된 데이터가 존재하는지 확인하는 용도로 주로 사용된다. MSG_PEEK 옵션을 주고 recv 함수를 호출하면 입력버퍼에 존재하는 데이터가 읽혀지더라도 입력버퍼에서 데이터가 지워지지 않는다.

표준 입출력 함수

fgets, fputs와 같은 표준 입출력 함수를 데이터 송수신에 사용할 수 있다. 이러한 표준 입출력 함수를 사용하면 얻는 장점으로는 아래 두 가지가 있다.

  • 표준 입출력 함수는 이식성(Portability)가 좋다.
  • 표준 입출력 함수는 버퍼링을 통한 성능의 향상에 도움이 된다.

첫번째 장점은 비단 입출력 함수뿐만 아니라, 모든 표준 함수들이 가지는 장점이다. 모든 운영체제(컴파일러)가 지원하도록 ANSI C에서 표준으로 정의했기 때문이다.

두번째 장점에 대해 이야기해보면, 표준 입출력 함수를 사용할 경우 추가적인 입출력 버퍼를 제공받는다. 이는 소켓을 생성할 때 운영체제가 만들어주는 버퍼와는 별개의 버퍼이다. 버퍼는 기본적으로 성능의 향상을 목적으로 하지만, 소켓 버퍼의 경우 TCP의 구현을 위한 목적이 더 강하다.(흐름 제어, 신뢰성 등) 반면 표준 입출력 함수 사용시 제공되는 버퍼는 오로지 성능 향상만을 목적으로 제공된다. 이 표준 입출력 버퍼가 소켓 버퍼 앞단에서 성능 향상에 큰 영향을 줄 수 있다.

구체적으로 어떻게 성능에 이점이 되는 지는 두가지 측면에서 알아볼 수 있다.
첫번째는 패킷을 보내는 횟수를 줄임으로써 얻는 이점이다. 패킷에는 기본적으로 구성되는 헤더정보들이 있다. 그러니 10 바이트의 데이터를 10번의 패킷으로 나누어 보내는 것은 1번의 패킷으로 보내는 것에 비해 패킷의 크기가 커진다. 이는 네트워크 트래픽의 면에서 좋지 않다. 그러니 표준 출력 버퍼를 통해 데이터를 한번에 모아 보낼 경우, 성능상 이점을 가져올 수 있다.
두번째는 소켓의 출력버퍼로 데이터를 이동시키는 데도 시간이 꽤나 소요된다는 점이다. 위 예와 마찬가지로 1바이트씩 10번 보내는 것이 10바이트를 한번에 보내는 것보다 시간이 훨씬 많이 소요된다.

하지만 표준 입출력 함수를 사용했을 때 장점만 있는 것은 아니다. 표준 입출력 함수를 사용했을 때 불편한 점들은 다음과 같다.

  • 양방향 통신이 쉽지 않다.
  • 상황에 따라서 fflush 함수의 호출이 빈번히 등장할 수 있다.
  • 파일 디스크립터를 FILE 구조체의 포인터로 변환해야 한다.

소켓의 경우, 소켓 하나로 읽고 쓰기를 동시에 하기 때문에, 한 소켓으로 읽기 모드, 쓰기 모드의 전환이 필요하다. 그런데 버퍼링 문제로 인해 읽기, 쓰기 모드로 작업의 형태가 바뀔 때마다 fflush 함수를 호출해야하고, 이렇게 되면 표준 입출력 함수의 장점인 버퍼링 기반의 성능향상에도 영향을 미친다.

표준 입출력 함수는 적용에 따른 부가적인 코드의 발생 때문에 생각만큼 즐겨 사용되지는 않는다. 하지만 경우에 따라서는 유용하게 사용될 수 있으니, 적절한 상황에 사용하면 좋다.

network-progamming's People

Contributors

gidskql6671 avatar

Stargazers

Wooseok Jang avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.