네트워크 프로그래밍의 기초

Soyun Park·2023년 11월 22일
0
post-thumbnail

1. 인터넷의 구조

1-1. IP 주소

  • 인터넷에 연결된 호스트는 반드시 하나 이상의 IP 주소를 가지고 있어 이것으로 해당 호스트를 특정 지을 수 있다.
  • IP 주소는 부호 없는 32비트 수치인데 사람이 보기 쉽도록 8비트 숫자 네개로 분할하여 192.168.1.3과 같이 표시한다.
  • 포트 번호는 16비트 부호 없는 숫자다.
  • 하나의 IP 주소에도 여러 회선이 연결될 수 있다.
  • 포트에서 접속을 기다리면서 어떤 작업을 해주는 프로세스를 서버 프로세스라고 한다.
  • 서버에 접속해서 어떤 작업을 수행하는 프로세스를 클라이언트 프로세스라고 한다.

  • 유명한 서비스들은 사전에 공유된 정해진 포트를 사용한다. 이를 알려진 포트라고 한다.
  • 예를 들어 메일을 송신할 때 사용하는 SMTP는 25번, 웹 브라우저가 사용하는 HTTP는 80번 포트를 사용한다.

1-2. IP

  • 인터넷에서 사용하는 통신 규약을 IP라고 한다.
  • IP 레벨에서는 패킷이란 개념만 존재한다. 패킷이란 데이터의 뭉치, 즉 바이트 열을 말한다.
  • 인터넷에서는 호스트 간에 패킷을 주고받음과 동시에 통신도 주고받는다.

  • 패킷을 받은 호스트는 자신에게 온 패킷이라면 받고, 아니라면 다른 곳으로 보낸다. 따라서 순서와 수신 여부를 보장하지 않는다.

1-3. TCP와 UDP

  • TCP 프로토콜에서 패킷이 스트림으로 추상화되는 과정은 다음과 같다. 스트림은 바이트의 열이다.
    1. 이 바이트의 열을 앞에서부터 일정 크기로 자른다.
    2. 각 부분에 번호를 붙여서 패킷으로써 전송한다.
    3. 받는 쪽에서는 패킷 번호를 보고 데이터를 재형성한다.
    4. 빠진 부분 없이 바이트 열이 만들어지면 스트림으로 다룰 수 있게 된다.
  • 다음과 같이 3번 패킷이 길을 잃었다. 일정 시간이 지나도 오지 않는다면 전송 측에서는 다시 한번 패킷을 보낸다.

  • UDP 프로토콜은 패킷이 도착하는 순서와 수신 여부를 보장하지 않는다. 대신 TCP에 비해 속도가 빠르고 처리가 간단하다.

1-4. IPv6

  • IPv4와 IPv6의 차이점은 주소의 크기이다. IPv4는 32비트이고 IPv6는 128비트이다.
  • IPv6의 주소 표시는 16비트 묶음 8개를 16진수로 표기하고 구분자로 : 를 사용한다.
  • 이때 0000 이 반복해서 나타난다면 한 곳에만 :: 이라고 표기할 수 있다.



2. 호스트 이름과 리졸버

2-1. 호스트명

  • 네트워크 상의 호스트는 IP 주소로 식별되지만 사람이 다루기 어렵다. 그래서 IP 주소 대신에 호스트명을 사용한다.
  • 호스트명과 IP 주소를 대응시켜 놓으면 사람은 호스트명을, 컴퓨터는 IP 주소를 다룰 수 있게 된다.
  • 이러한 호스트명과 IP 주소의 매핑은 /etc/hosts 에 기록하여 관리할 수 있다.

  • 하지만 호스트가 늘어날 때마다 기록해야하므로 현실적이지 않다.

2-2. DNS와 도메인

  • DNS는 호스트명을 도메인이라고 하는 영역에 나눠서 관리함으로써 호스트명의 관리를 전 세계에 분산시켰다.
  • 도메인은 트리 구조로 루트 디렉터리에 해당하는 루트 도메인, 그 밑에 com, org, kr과 같은 최상위 도메인, 그 밑에 계속 배치되는 구조이다. 각각의 도메인을 도메인 이름이라고 한다.
  • 도메인 이름은 오른쪽이 루트에 해당한다. 예를 들어 www.example.com/은 / 밑에, com 밑에, example.com 밑에, www.example.com 도메인이 있다.
  • 인터넷 상의 호스트를 루트 도메인에서부터 전부 기술한 것을 FQDN이라고 한다.

2-3. DNS에 의한 도메인 이름 관리

  • example.com 처럼 호스트명에 대응되지 않는 도메인명도 있다. 즉, example.com 도메인은 com 도메인과 다른 관리자가 있어서, 그 밑의 도메인을 독자적으로 관리한다.
  • example.com 도메인 밑의 도메인은 example.com 도메인 관리자에게 물으면 알 수 있다. 이러한 서버를 DNS 서버라고 한다.
  • 도메인은 트리 구조로 되어 있어서 example.com 의 DNS 서버는 com 도메인의 DNS 관리자에게 물으면 된다. com 의 DNS 서버는 루트 도메인의 DNS 서버에게 물으면 된다.
  • / 도메인의 DNS 서버의 IP 주소는 모든 DNS 서버에 직접 등록이 되어 있다. 이러한 방식으로 DNS가 호스트명을 IP 주소로 바꿔준다.

2-4. 리졸버

  • 호스트명과 IP 주소를 교환해주는 존재를 리졸버라고 한다.
  • 리눅스에서는 IP 주소의 리졸버로 libc가 있고 해당 설정은 /etc/nsswitch.conf 에 기술된다.



3. 소켓 API

3-1. 소켓

  • 리눅스에서는 네트워크 통신을 위해 소켓을 사용한다. 스트림을 연결하는 역할을 한다.
  • 소켓은 넓은 범위에서 응용할 수 있는데, 예를 들면 서버와 클라이언트, TCP와 UDP나 IP, 인터넷 이외의 프로토콜에서 사용할 수 있다.


3-2. 클라이언트 측 소켓 API

  • 클라이언트 측에서 서버에 스트림을 연결시키는 시스템 콜은 다음과 같다.
    1. socket(2)
    2. connect(2)

3-3. socket(2)

#include <sys/socket.h>
#include <sys/types.h>
int socket(int domain, int type, int protocol);
  • socket()은 소켓을 만들고 이에 대응하는 파일 디스크립터를 반환한다.
  • 인자 domain, type, protocol은 전부 합쳐서 만들 소켓에 무엇을 연결할지 지정한다. 예를 들어 IPv4 상의 TCP라면, socket(PF_INET, SOCK_STREAM, 0)을 호출한다.

3-4. connect(2)

#include <sys/socket.h>
#include <sys/types.h>
int. connect(int sock, const struct sockaddr *addr, socklen_t addrlen);
  • connect()는 소켓 sock에서 스트림을 꺼내서 addr로 지정한 주소의 서버에 스트림을 연결한다.
  • addr은 open()에서의 경로와 유사하다. 인터넷이라면 호스트 이름이 아닌 IP 주소와 포트 번호를 지정하면 된다.
  • addrlen은 *addr의 크기를 지정한다.

3-5. 서버 측 소켓 API

  • 스트림의 연결을 기다리고 있는 서버 측 시스템 콜은 다음과 같다.
    1. socket(2)
    2. bind(2)
    3. listen(2)
    4. accept(2)

3-6. bind(2)

#include <sys/socket.h>
#include <sys/types.h>
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
  • bind()는 접속을 기다리는 주소 addr을 소켓 sock에 할당한다.
  • addrlen은 *addr의 크기이다.

3-7. listen(2)

#include <sys/socket.h>
int listen(int sock, int backlog);
  • listen()은 소켓 sock이 서버용 소켓, 즉 접속을 기다리는 소켓임을 커널에 알린다.
  • backlog는 동시에 받아들일 수 있는 커넥션의 최대 수이다.

3-8. accept(2)

#include <sys/socket.h>
#include <sys/types.h>
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
  • accept()는 sock에 클라이언트가 접속하는 것을 기다리다 접속이 완료되면 연결된 스트림의 파일 디스트립터를 반환한다.
  • addr에는 클라이언트의 주소가 기재되며, addrlen에는 *addr의 크기가 적힌다.



4. 이름 해결

4-1. 호스트명과 IP 주소를 변환하기

  • 호스트명, 서비스명으로부터 IP 주소를 변환해주는 API는 다음과 같다.
    • getaddrinfo()
    • getnameinfo()
    • freeaddrinfo()
    • gai_strerror()
  • getnameinfo()는 IP 주소나 포트 번호로부터 도메인명이나 서비스명을 얻기 위해 사용한다.

4-2. getaddrinfo(3)

#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service,
				const struct addrinfo *hints, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int err);

struct addrinfo{
	int ai_flags;
    int ai_family;
    int ai_socketype;
    int ai_protocol;
    socklen_t ai_addrlen;
    struct sockaddr *ai_addr;
    char *ai_canonname;
    struct addrinfo *ai_next;
};
  • getaddrinfo()는 접속 대상인 node의 주소 후보를 res에 기재한다.
  • service와 hint로 범위를 좁힐 수 있다.
  • res는 struct addrinfo의 링크드 리스트 형태를 가진다.

  • 이 struct addrinfo의 메모리 영역은 malloc()으로 할당되므로 명시적으로 해제해야 한다. 이를 freeaddrinfo()가 수행한다.



5. daytime 클라이언트 작성

5-1. daytime 명령어 만들기

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

static int open_connection(char *host, char *service);

int main(int argc, char *argv[]){
    int sock;
    FILE *f;
    char buf[1024];
	
  	// daytime 서비스에 연결
    sock = open_connection((argc>1 ? argv[1] : "localhost"), "daytime");
    f = fdopen(sock, "r");
    if (!f) {
        perror("fdopen(3)");
        exit(1);
    }
  
    // 연결된 소켓으로부터 현재 시간 정보를 출력
    fgets(buf, sizeof buf, f);
    fclose(f);
    fputs(buf, stdout);
    exit(0);
}

static int open_connection(char *host, char *service){
    int sock;
    struct addrinfo hints, *res, *ai;
    int err;

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC; // ipv4, ipv6 둘 다 사용
    hints.ai_socktype = SOCK_STREAM; // TCP 소켓 사용
  
  	// 호스트와 서비스에 대한 주소 정보를 가져옴
    if ((err = getaddrinfo(host, service, &hints, &res)) != 0){
        fprintf(stderr, "getaddrinfo(3): %s\n", gai_strerror(err));
        exit(1);
    }
  
    for (ai = res; ai; ai = ai->ai_next) {
      	// 소켓 생성
        sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
        if (sock < 0) {
            continue;
        }
      
      	// 서버에 연결
      	// 성공 시 소켓 디스크립터 반환
        if (connect(sock, ai->ai_addr, ai->ai_addrlen) < 0) {
            close(sock);
            continue;
        }
        /* success */
        freeaddrinfo(res);
        return sock;
    }
    fprintf(stderr, "socket(2)/connect(2) failed");
    freeaddrinfo(res); // 할당한 addrinfo 해제
    exit(1);
}

5-2. 인터넷 슈퍼 서버

  • 작성한 코드를 테스트하려면 daytime 서버를 먼저 구동해야 한다.
  • daytime은 inetd와 xinetd 내부에 포함된 프로그램이다.
  • inetd는 지정된 포트에 클라이언트가 접속하는 것을 기다린다. 인터넷 슈퍼 서버라고도 한다.
  • 접속이 완료되면 셸과 마찬가지로 dup()를 사용하여 소켓을 표준 입력과 표준 출력에 옮겨서 서브 프로그램을 exec한다.
  • 그러면 프로그램은 표준 입출력에 입출력함으로써 네트워크 통신이 가능해진다.
  • xinetd는 inetd의 보안과 관련된 부분이 개선된 개량판이다.

5-3. 우분투에서의 xintd 설정

  • apt-get을 사용하여 xinetd를 설치한다.
    $ sudo apt-get install xinetd
  • daytime 프로토콜 설정 파일 /etc/xinetd.d/daytime 을 수정한다.

  • 변경한 설정을 반영한다.
    $ sudo systemctl reload xinetd

5-4. daytime 커맨드 실행 예


0개의 댓글