TCP/IP 소켓 프로그래밍 - 네트워크 프로그래밍의 시작 2

mingsso·2023년 3월 29일
0

CS

목록 보기
28/30
post-thumbnail
post-custom-banner

Domain Name System

도메인 이름

인터넷에서 서비스를 제공하는 서버들 역시 IP 주소로 구분이 되지만, 기억하고 표현하기 어려움
때문에 도메인 이름이라는 것을 IP 주소에 부여해서, 이것이 IP 주소를 대신하도록 하고 있음


DNS 서버

도메인 이름은 해당 서버에 부여된 가상의 주소이지 실제 주소가 아니므로, 가상의 주소를 실제 주소로 변환하는 과정을 거쳐 사이트에 접속해야 함
→ 도메인 이름을 IP 주소로 변환하는 서버가 DNS

모든 컴퓨터에는 디폴트 DNS 서버의 주소가 등록되어 있는데, 바로 이 디폴트 DNS 서버를 통해서 도메인 이름에 대한 IP 주소 정보를 얻게 됨

물론 디폴트 DNS 서버가 모든 도메인의 IP 주소를 알고있지는 않지만, 디폴트 DNS 서버는 모르면 다른 DNS 서버에게 물어서라도 가르쳐줌

디폴트 DNS 서버는 자신이 모르는 정보에 대한 요청이 들어오면, 한 단계 상위 계층에 있는 DNS 서버에 물어봄
→ 이런 식으로 계속 올라가다 보면 최상위 DNS 서버인 루트 DNS 서버에게까지 질의가 전달되는데, 루트 DNS 서버는 해당 질문을 누구에게 전달해야할지 알고 있음
→ 그래서 루트 DNS 서버는 자신보다 하위에 있는 DNS 서버에게 다시 질의를 던져서 IP 주소를 알아냄
→ 알아낸 IP 주소는 질의가 진행된 반대 방향으로 전달되어 결국에는 질의를 시작한 호스트에게 전달될 수 있음

이렇듯 DNS는 계층적으로 관리되는 일종의 분산 데이터베이스 시스템!



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

// 문자열 형태의 도메인 이름으로부터 IP의 주소정보를 얻는 함수
struct hostent * gethostbyname(const char * hostname)

IP 주소는 hostent라는 구조체의 변수에 담겨 반환됨

struct hostent {
   char * h_name;   // 공식 도메인 이름
   char ** h_aliases;   // 공식 도메인 이름 외 해당 페이지에 접속 가능한 다른 도메인 이름
   int h_addrtype;   // 반환된 IP주소의 주소체계 정보
   int h_length;  // 반환된 IP주소의 크기
   char ** h_addr_list;  // 반환된 IP주소(문자열)
}

// IP주소를 이용해서 도메인 정보 얻어오는 함수
struct hostent * gethostbyaddr(const char * addr, sickle_t len, int family)



소켓의 다양한 옵션

소켓의 옵션의 일부는 다음과 같음 → 소켓의 옵션은 계층별로 분류됨
거의 모든 옵션은 설정상태의 참조(Get) 및 변경(Set)이 가능함 → getsocket(), setsocket() 함수 이용


Time-wait 상태


먼저 연결의 종료를 요청한 호스트는 Four-way Handshaking 이후에 소켓이 바로 소멸되지 않고 Time-wait 상태를 거침
→ 먼저 연결을 종료한 호스트는 바로 이어서 실행할 수 없음

호스트 A가 호스트 B로 전송한 마지막 ACK 메시지가 전달되지 못하고 소멸되었다면, 호스트 B는 재전송을 시도할 것임 → 하지만 호스트 A의 소켓은 완전히 종료되었기 때문에 호스트 B는 호스트 A로부터 영원히 마지막 ACK 메시지를 받지 못하게 됨 (Time-wait 상태를 거치는 이유)



주소의 재할당


시스템에 문제가 생겨서 서버가 갑작스럽게 종료된 경우, 재빨리 서버를 재가동시켜서 서비스를 이어나가야 하는데, 타임아웃 때문에 몇 분을 더 기다려야 하면 문제가 됨 → 심지어 네트워크 상황이 원활하지 않다면 Time-wait 상태가 영원히 지속될 수도 있음

소켓 옵션 SO_REUSEADDR의 상태를 변경해 Time-wait 상태에 있는 소켓의 PORT 번호를 새로 시작하는 소켓에 할당되게끔 하면 됨



Nagle 알고리즘

네트워크 상에서 돌아다니는 패킷들의 흘러 넘침을 막기 위해 제안된 알고리즘으로, TCP 상에서 적용됨
앞서 전송한 데이터에 대한 ACK 메시지를 받아야만, 다음 데이터를 전송하는 알고리즘
기본적으로 TCP 소켓은 Nagle 알고리즘을 적용해서 데이터를 송수신하며, ACK가 수신될 때까지 최대한 버퍼링을 해서 데이터를 전송함

  • Nagle 알고리즘 적용 시 - 'N'은 수신할 ACK가 없으므로 바로 전송이 이루어지며, 'N'에 대한 ACK를 기다리는 동안 출력 버퍼에는 문자열의 나머지 'agle'이 채워짐, 이어서 'N'에 대한 ACK를 수신하고 출력버퍼에 존재하는 데이터 'agle'을 하나의 패킷으로 구성해서 전송하게 됨 (총 4개의 패킷 송수신)
  • Nagle 알고리즘 적용하지 않을 시 - 출력버퍼에 데이터가 전달되는 즉시 전송이 이루어짐 (총 10개의 패킷 송수신) → 1바이트를 전송하더라도 패킷에 포함되어야 하는 헤더정보의 크기는 수십 바이트에 이르기 때문에 네트워크 트래픽에 좋지 않은 영향


하지만 전송하는 데이터의 특성(용량이 큰 파일 전송 등)에 따라, Nagle 알고리즘의 적용 여부에 따른 트래픽 차이가 크지 않으면서도 Nagle 알고리즘을 적용하지 않을 때보다 데이터 전송이 느릴 수 있음 → 파일 데이터를 출력버퍼로 밀어넣는 작업은 시간이 많이 걸리지 않기 때문에, Nagle 알고리즘을 적용하지 않아도 출력버퍼를 거의 꽉 채운 상태에서 패킷을 전송하게 됨

Nagle 알고리즘을 적용하고 싶지 않다면, 소켓옵션 TCP_NODELAY를 1로 변경해주면 됨



프로세스의 이해와 활용

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

전체적인 서비스 제공시간은 조금 늦어지더라도, 연결요청을 해오는 모든 클라이언트에게 동시에 서비스를 제공해서 평균 만족도를 높여야 함

  • 멀티프로세스 기반 서버 = 다수의 프로세스를 생성하는 방식으로 서비스 제공(윈도우에서는 지원하지 않음)
  • 멀티플랙싱 기반 서버 = 입출력 대상을 묶어서 관리하는 방식으로 서비스 제공
  • 멀티스레딩 기반 서버 = 클라이언트의 수만큼 스레드를 생성하는 방식으로 서비스 제공



프로세스의 이해

프로세스는 메모리 공간을 차지한 상태에서 실행 중인 프로그램
모든 프로세스는 생성되는 형태에 상관없이 운영체제로부터 ID(2 이상의 정수 형태)를 부여 받음 = 프로세스 ID
숫자 1은 운영체제가 시작하자마자 실행되는(운영체제의 실행을 돕는) 프로세스에게 할당됨


멀티프로세스 기반 서버의 구현에는 fork 함수가 사용됨 → fork 함수는 호출한 프로세스의 복사본을 생성함 (새롭게 프로세스를 생성하는 것이 아님, fork 함수를 호출한 프로세스를 복사하는 것), 두 프로세스 모두 fork 함수의 반환 이후 문장을 실행하게 됨

그러나 두 프로세스는 메모리 영역까지 동일하게 복사되기 때문에, 다음 특징을 이용해서 프로세스의 흐름을 구분해야 함

  • 부모(원본) 프로세스 - fork 함수의 반환 값은 자식 프로세스의 ID
  • 자식(복사된) 프로세스 - fork 함수의 반환 값은 0



좀비 프로세스의 생성 이유

프로세스가 생성되고 나서 할 일을 다 하면(main 함수의 실행을 완료하면) 사라져야 하는데 사라지지 않고 좀비가 되어 시스템의 중요한 리소스를 차지하기도 함

fork 함수의 호출로 생성된 자식 프로세스가 종료되는 상황

  • 인자를 전달하면서 exit를 호출하는 경우
  • main 함수에서 return 문을 실행하면서 값을 반환하는 경우

운영체제는 exit 함수로 전달되는 인자 값이나 return문에 의해 반환되는 값이 자식 프로세스를 생성한 부모 프로세스에게 전달될 때까지 자식 프로세스를 소멸시키지 않는데, 바로 이 상황에 놓여있는 프로세스를 가리켜 좀비 프로세스라 함 → 부모 프로세스가 자식 프로세스의 전달 값을 요청해야 함


좀비 프로세스의 소멸1: wait 함수의 사용

pid_t wait(int *statloc)
// 매개변수로 전달된 주소의 변수에 저장되는 값
// 1) 자식 프로세스가 종료되면서 전달한 값
// 2) WIFEXITED = 자식 프로세스가 정상 종료한 경우 참(True)을 반환함
// 3) WEXITSTATUS = 자식 프로세스의 전달 값을 반환함

위 함수가 호출되었을 때, 종료된 자식 프로세스가 있다면 인자값/반환값이 매개변수로 전달된 주소의 변수에 저장됨
하지만 호출된 시점에서 종료된 자식 프로세스가 없다면, 임의의 자식 프로세스가 종료될 때까지 블로킹 상태에 놓인다는 단점이 있음


좀비 프로세스의 소멸2: waitpid 함수의 사용

pid_t waitpid(pid_t pid, int *statloc, int options)
// pid - 종료를 확인하고자 하는 자식 프로세스의 id 전달

좀비 프로세스의 생성을 막는 두 번째 방법이자, 블로킹 문제의 해결책



시그널 핸들링

자식 프로세스가 종료되면, 운영체제가 자식 프로세스가 종료되었음을 부모 프로세스에게 알려주고(시그널), 이후 부모 프로세스가 미리 처리한 자식 프로세스의 종료와 관련된 일을 처리하는 것


어떤 상황이 발생하면, 특정 함수를 호출하라고 부모 프로세스가 운영체제에게 요구할 때 쓰는 함수(시그널 등록 함수)

void (*signal(int signo, void (*func)(int)))(int)

① signo - 특정한 상황에 대한 정보
→ SIGALRM (alarm 함수 호출 통해 등록된 시간이 된 상황), SIGINT (CTRL+C가 입력된 상황), SIGCHLD (자식 프로세스가 종료된 상황) 등

② (*func)(int) - 특정 상황에서 호출될 함수의 주소 값 (매개변수형이 int이고 반환형이 void인 함수)


signal 함수를 대체할 수 있으며 훨씬 안정적으로 동작하는 함수(운영체제 별로 동작방식의 차이가 없음)

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact)

① signo - 특정한 상황에 대한 정보
② act - 시그널 발생 시 호출될 함수(시그널 핸들러)의 정보 전달
③ oldact - 시그널 핸들러 함수의 포인터를 얻는데 사용



프로세스 기반의 다중접속 서버의 구현 모델

동시에 둘 이상의 클라이언트에게 서비스를 제공하는 구조로 에코 서버 확장

  • 1단계) 에코 서버(부모 프로세스)는 accept 함수호출을 통해 연결 요청을 수락함
  • 2단계) 이때 얻게 되는 소켓의 파일 디스크립터를 자식 프로세스를 생성해서 넘겨줌
  • 3단계) 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공함



TCP의 입출력 루틴 분할

지금까지는 한번 데이터를 전송하면 에코되어 돌아오는 데이터를 수신할 때까지 마냥 기다려야 했지만, 이제는 둘 이상의 프로세스를 생성할 수 있으니 데이터의 송신과 수신도 분리할 수 있음

클라이언트의 부모 프로세스는 데이터의 수신을 담당하고, 자식 프로세스는 데이터의 송신을 담당함
→ 입력과 출력을 담당하는 프로세스가 각각 다르기 때문에 서버로부터의 데이터 수신 여부에 상관없이 데이터를 전송할 수 있음



프로세스 간 통신

파이프 기반의 프로세스 간 통신

두 프로세스 간 통신을 위해서는 파이프라는 것을 생성해야 하며, 이 파이프는 소켓과 마찬가지로 운영체제에 속하는 자원임

int pipe(int filedes[2])
// filedes[0] = 파이프의 출구(데이터 수신에 사용)
// filedes[1] = 파이프의 입구(데이터 전송에 사용)

위 함수는 파이프의 생성에 사용됨
파이프에 데이터가 전달되면, 이는 임자 없는 데이터가 되고 먼저 가져가는 프로세스에게 이 데이터가 전달됨 → 양방향 통신을 위해서는 파이프를 두 개 생성



IO 멀티플렉싱 기반의 서버

멀티프로세스 서버는 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간의 데이터 교환에 복잡한 방법을 택할 수밖에 없음('IPC=프로세스 간 통신'이 복잡한 방법)
-> 멀티플렉싱이 대안이 될 수 있음

멀티플렉싱

하나의 통신채널(전송로)을 통해서 둘 이상의 데이터(시그널)을 전송하는 방식
(멀티플랙싱 기술을 사용하지 않으면 인당 2개, 총 6개의 전화기가 필요함)

서버에도 멀티플렉싱 기술을 도입하여, 하나의 프로세스를 통해 여러 개의 클라이언트와 소통할 수 있음
-> 하나의 스레드에서 다수의 클라이언트에 연결된 소켓(파일 디스크립터)을 관리하고 소켓들을 감시하다가 어떤 클라이언트에 데이터가 수신되면 서버와 통신을 하게 됨



select 함수의 기능과 호출순서

select 함수를 사용하면 한곳에 여러 개의 파일 디스크립터를 모아놓고 동시에 이들을 관찰할 수 있음

  • 데이터를 수신한 소켓은 어떤 소켓인지
  • 데이터 전송 시 블로킹되지 않고 바로 전송할 수 있는 소켓은 어떤 소켓인지 (출력 스트림이 꽉 차지 않아서 바로 전송 가능한 경우)
  • 예외상황이 발생한 소켓은 무엇인지


select 함수의 호출과정은,
① 파일 디스크립터의 설정 + 검사의 범위 지정 + 타임아웃의 설정
② select 함수의 호출
③ 호출결과 확인


파일 디스크립터의 설정
먼저 관찰하고자 하는 파일 디스크립터를 관찰 항목(수신, 전송, 예외)에 따라 구분해서 모아야 함 → fd_set형 변수(비트단위로 이루어진 배열) 이용

fd_set 배열의 비트가 1로 설정되면 해당 파일 디스크립터가 관찰의 대상임을 의미함
(파일 디스크립터 1과 3이 관찰대상)

fd_set형 변수에 값을 등록하거나 변경하는 등의 작업은 다음 매크로 함수들의 도움을 통해서 이루어짐

  • FD_ZERO(fd_set *fdset) = 인자로 전달된 주소의 fd_set형 변수의 모든 비트를 0으로 초기화
  • FD_SET(int fd, fd_set *fdset) = 매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보를 등록함
  • FD_CLR(int fd, fd_set *fdset) = 매개변수 fdset으로 전달된 주소의 변수에서 매개변수 fd로 전달된 파일 디스크립터 정보를 삭제함
  • FD_ISSET(int fd, fd_set *fdset) = 매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보가 있으면 양수를 반환함

검사(관찰)의 범위지정과 타임아웃의 설정

int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)
// maxfd - 검사 대상이 되는 파일 디스크립터의 수
// readset - '수신된 데이터의 존재여부'에 관심있는 파일 디스크립터 정보를 등록해서 그 변수의 주소값을 전달
// writeset - '블로킹 없는 데이터 전송의 가능여부'에 관심있는 파일 디스크립터 정보를 등록해서 그 변수의 주소값을 전달
// exceptset - '예외상황의 발생여부'에 관심있는 파일 디스크립터 정보를 등록해서 그 변수의 주소값을 전달
// timeout - 무한 블로킹 상태에 빠지지 않도록 타임아웃을 설정

select 함수에 세가지 관찰항목별로 fd_set형 변수를 선언해서 파일 디스크립터 정보를 등록하고, 이 변수의 주소 값을 위 함수의 2,3,4번째 인자로 전달함

오류발생시에는 -1이 반환되고, 타임아웃에 의한 반환 시에는 0이 반환되고, 관심대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 생기면 0보다 큰 값이 반환됨(변화가 발생한 파일 디스크립터의 수)

struct timeval {
   long tv_sec;   // seconds
   long tv_usec;   // microseconds
}

파일 디스크립터에 변화가 발생하지 않아도, timeval 자료형에서 지정한 시간이 지나면 함수가 반환됨(0을 반환)



다양한 입출력 함수들

send & recv 입출력 함수

ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)
ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags)
// sockfd - 데이터 전송/수신 대상과의 연결을 의미하는 소켓의 파일 디스크립터 전달
// buf - 전송할/수신된 데이터를 저장하고 있는 버퍼의 주소 값 전달
// nbytes - 전송할 바이트 수 전달
// flags - 데이터 전송 시 적용할 다양한 옵션 정보 전달 



readv & writev 입출력 함수

여러 버퍼에 나뉘어 저장되어 있는 데이터를 모아서 전송하고, 모아서 수신하는 기능의 함수

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt)
// filedes - 데이터 전송의 목적지를 나타내는 소켓(파일)의 파일 디스크립터 전달
// iov - 전송할 데이터의 위차 및 크기 정보가 담긴 구조체 iovec 배열의 주소 값 전달
// iovcnt - 두 번째 인자로 전달된 주소 값이 가리키는 배열의 길이 정보 전달

ssive_t readv(int filedes, const struct iovec *iov, int iovcnt)



멀티캐스트

멀티캐스트 방식의 데이터 전송은 UDP를 기반으로 하므로 UDP 서버/클라이언트의 구현방식과 매우 유사함
하지만 UDP에서의 데이터 전송은 하나의 목적지를 두고 이루어지지만, 멀티캐스트에서의 데이터 전송은 특정 그룹에 가입(등록)되어 있는 다수의 호스트가 목적지가 됨

멀티캐스트의 데이터 전송방식과 멀티캐스트 트래픽 이점

  • 멀티캐스트 서버는 특정 멀티캐스트 그룹을 대상으로 데이터를 딱 한 번 전송함
  • 딱 한 번 전송하더라도 그룹에 속하는 클라이언트는 모두 데이터를 수신함
  • 멀티캐스트 그룹의 수는 IP주소 범위 내에서 얼마든지 추가가 가능함
  • 특정 멀티캐스트 그룹으로 전송되는 데이터를 수신하려면 해당 그룹에 가입하면 됨
    *멀티캐스트 그룹 = 클래스 D에 속하는 IP주소(224.0.0.0~239.255.255.255)

하나의 영역에 동일한 패킷이 둘 이상 전송되지 않기 때문에 트래픽에 부정적인 영향을 끼치지 않음 → '멀티미디어 데이터의 실시간 전송'에 주로 사용됨



라우팅과 TTL, 그리고 그룹으로의 가입방법

멀티캐스트 패킷의 전송을 위해서는 TTL의 설정과정이 반드시 필요함

*TTL = '패킷을 얼마나 멀리 전달할 것인가'를 결정하는 주 요소로 정수로 표현됨, 라우터를 하나 거칠 때마다 1씩 감소하며 0이 되면 패킷이 소멸됨 → TTL을 너무 크게 설정하면 네트워크 트래픽에 좋지 못한 영향을 줄 수 있고, 너무 적게 설정해도 목적지에 도달하지 않는 문제가 발생할 수 있음

  • 프로그램 상에서의 TTL 설정은 소켓의 옵션 설정을 통해 이루어짐 → 프로토콜 레벨 IPPROTO_IP / 옵션 이름 IP_MULTICAST_TTL
  • 멀티캐스트 그룹으로의 가입 역시 소켓의 옵션 설정을 통해 이루어짐 → 프로토콜 레벨 IPPROTO_IP / 옵션 이름 IP_ADD_MEMBERSHIP



멀티캐스트 Sender와 Receiver의 구현

멀티캐스트 기반에서는 서버, 클라이언트라는 표현을 대신해서 전송자, 수신자라는 표현을 사용함

  • Sender - 멀티캐스트 데이터의 전송 주체
  • Receiver - 멀티캐스트 그룹의 가입과정이 필요한 데이터의 수신주체



브로드캐스트

브로드캐스트는 한 번에 여러 호스트에게 데이터를 전송한다는 점에서 멀티캐스트와 유사함

하지만 멀티캐스트는 서로 다른 네트워크 상에 존재하는 호스트라 할지라도, 멀티캐스트 그룹에 가입만 되어 있으면 데이터의 수신이 가능한 반면, 브로드캐스트는 동일한 네트워크로 연결되어 있는 호스트로, 데이터의 전송 대상이 제한됨

브로드캐스트의 이해와 구현방법

멀티캐스트와 마찬가지로 UDP를 기반으로 데이터를 송수신하며, 데이터 전송 시 사용되는 IP주소의 형태에 따라 두 가지 형태로 구분됨
→ Directed 브로드캐스트 / Local 브로드캐스트

Directed 브로드캐스트

  • 특정 지역의 네트워크에 연결된 모든 호스트에게 데이터를 전송할 때 사용
  • IP주소는 네트워크 주소를 제외한 나머지 호스트 주소를 전부 1로 설정해서 얻을 수 있음 → 네트워크 주소가 192.12.34인 네트워크에 연결되어 있는 모든 호스트에게 데이터를 전송하려면 192.12.34.255로 데이터를 전송하면 됨

Local 브로드캐스트

  • 255.255.255.255라는 IP주소가 특별히 예약되어 있음 → 네트워크 주소가 192.32.24인 네트워크에 연결되어 있는 호스트가 IP주소 255.255.255.255를 대상으로 데이터를 전송하면, 192.32.24로 시작하는 IP주소의 모든 호스트에게 데이터가 전달됨
profile
🐥👩‍💻💰
post-custom-banner

0개의 댓글