[05.02/week08]YIL

CHO WanGi·2025년 5월 3일

KRAFTON JUNGLE 8th

목록 보기
44/89

어제 하루 요약

✏️ 오늘의 한 일

  • CSAPP 11 Computer Network ~ 11.4 Echo Server

🌈오늘의 성장!

CSAPP 11. Computer Network

클라이언트-서버 프로그래밍 모델 (Client-Server Model)

가장 기본적인 네트워크 애플리케이션 구조는 클라이언트-서버 모델입니다.

  • 핵심: 클라이언트 프로세스가 서버 프로세스에게 서비스를 요청하고, 서버는 그 요청을 처리하여 응답하는 방식입니다.
  • Transaction (기본 동작):
    1. 요청 (Request): 클라이언트가 서버에 요청을 보내며 트랜잭션을 시작합니다.
    2. 처리 (Processing): 서버는 요청을 받아 해석하고, 필요한 자원(예: 파일)을 조작하여 요청을 처리합니다.
    3. 응답 (Response): 서버는 처리 결과를 클라이언트에게 응답으로 보내고 다음 요청을 기다립니다.
    4. 클라이언트 처리 (Manipulation/Handling): 클라이언트는 서버로부터 받은 응답(예: HTML 파일)을 처리하여 사용자에게 보여줍니다.
  • 중요점: 클라이언트와 서버는 물리적인 컴퓨터(호스트)가 아니라, 실행 중인 프로세스를 의미합니다. 하나의 호스트에서 여러 클라이언트/서버 프로세스를 실행할 수 있으며, 트랜잭션은 같은 호스트 내에서도, 다른 호스트 사이에서도 발생할 수 있습니다.

네트워크 기초 (Networks)

클라이언트와 서버 프로세스가 데이터를 주고받는 물리적인 경로와 기본적인 네트워크 구성 요소입니다.

데이터 전송 경로

데이터는 CPU에서 처리되어 메모리에 저장된 후, 시스템 버스, I/O 브릿지, I/O 버스를 거쳐 네트워크 어댑터(랜 카드)에 도달합니다. 어댑터는 데이터를 전기 또는 광 신호로 변환하여 네트워크 매체(케이블 등)를 통해 전송합니다. 수신 시에는 역순으로 진행됩니다.

이더넷 세그먼트 (Ethernet Segment - 허브 기반)

  • 가장 기본적인 LAN 형태로, 허브(Hub)에 여러 호스트가 연결됩니다.
  • 대역폭 공유: 모든 호스트가 전체 대역폭(예: 100Mb/s)을 나눠 씁니다.
  • 충돌 도메인: 허브는 들어온 데이터를 모든 포트로 전달하므로, 동시에 여러 호스트가 전송하면 충돌이 발생합니다.

네트워크 어댑터 (Network Adapter)

  • 호스트를 네트워크에 물리적으로 연결합니다.
  • 고유한 48비트 MAC 주소를 가집니다.
  • 프레임(Frame): 데이터를 '프레임'이라는 단위로 묶어 전송합니다. 프레임은 헤더(출발지/목적지 MAC 주소 등)와 데이터(Payload)로 구성됩니다.
  • 같은 세그먼트 내 모든 어댑터는 프레임을 받지만, 헤더의 목적지 MAC 주소가 자신과 일치하는 호스트만 데이터를 읽습니다.

브릿지 이더넷 (Bridged Ethernet - 스위치 기반)

  • 여러 이더넷 세그먼트를 브릿지(Bridge) 또는 스위치(Switch)로 연결하여 더 큰 LAN을 구성합니다. (현대에는 스위치가 일반적이며, 브릿지보다 성능과 기능이 우수합니다.)
  • 장점:
    • 효율적 대역폭: 허브와 달리 포트별로 전용 대역폭을 제공할 수 있고, 불필요한 트래픽 전송을 줄입니다.
    • 학습 기능: 스위치는 각 포트에 연결된 호스트의 MAC 주소를 학습합니다.
    • 선택적 전달: 학습된 MAC 주소 테이블을 기반으로, 프레임을 목적지가 있는 포트로만 전달합니다. 같은 세그먼트 내 통신은 다른 세그먼트로 전달하지 않습니다.

라우터와 internet (소문자 i)

  • internet (상호 연결된 네트워크): 서로 다른 종류의 LAN이나 WAN(Wide Area Network)을 연결한 네트워크들의 네트워크입니다.
  • 라우터(Router): 서로 다른 종류의 네트워크를 연결하는 장치입니다. 각 네트워크 인터페이스마다 별도의 포트(어댑터)를 가집니다.
  • 과제: 서로 다른 통신 방식과 주소 체계를 가진 네트워크들을 어떻게 연결하여 통신할까?
  • 해결책: 프로토콜 소프트웨어 계층. 모든 호스트와 라우터에서 실행되며, 통일된 주소 지정 방식(IP 주소)과 표준화된 데이터 전달 방식(IP 패킷)을 제공합니다.

캡슐화 (Encapsulation)

서로 다른 네트워크를 거쳐 데이터가 전달되는 과정입니다.

  1. 클라이언트 앱 → 커널 (Host A): 앱이 send() 시스템 콜을 호출하면 데이터가 커널 버퍼로 복사됩니다.
  2. 프로토콜 SW (Host A): TCP/IP 스택이 데이터를 받아 IP 헤더(PH)LAN1 프레임 헤더(FH1)를 붙여 LAN1 프레임으로 만듭니다. (캡슐화)
  3. 어댑터 → 네트워크 (Host A): LAN1 어댑터가 프레임을 네트워크 매체로 전송합니다.
  4. 네트워크 → 어댑터 → 프로토콜 SW (Router): 라우터의 LAN1 어댑터가 프레임을 수신하여 내부 프로토콜 SW로 전달합니다.
  5. 프로토콜 SW (Router): 라우터는 FH1을 제거하고 PH의 목적지 IP 주소를 확인합니다. 라우팅 테이블을 참조하여 다음 경로(LAN2)를 결정하고, 새로운 LAN2 프레임 헤더(FH2)를 붙입니다. (재캡슐화)
  6. 어댑터 → 네트워크 (Router): 라우터의 LAN2 어댑터가 새 프레임을 LAN2 네트워크로 전송합니다.
  7. 네트워크 → 어댑터 → 프로토콜 SW (Host B): Host B의 LAN2 어댑터가 프레임을 수신하여 내부 프로토콜 SW로 전달합니다.
  8. 프로토콜 SW → 서버 앱 (Host B): Host B의 프로토콜 SW는 FH2와 PH를 제거하여 원본 데이터를 얻습니다. 서버 앱이 recv() 시스템 콜을 호출하면 이 데이터가 앱의 메모리로 복사됩니다. (역캡슐화)

The Global IP Internet (대문자 I)

우리가 흔히 '인터넷'이라고 부르는 전 지구적인 네트워크입니다.

  • 핵심 구성:
    • TCP/IP 프로토콜: 인터넷 통신의 기반이 되는 프로토콜 모음 (IP, TCP, UDP 등).
    • 소켓 인터페이스: 응용 프로그램이 TCP/IP 기능을 사용할 수 있게 하는 API.

프로토콜 종류

  • IP (Internet Protocol): 기본적인 주소 지정(IP 주소)과 데이터그램(패킷) 전달 메커니즘을 제공합니다. 비신뢰성(unreliable), 최선 노력(best-effort) 방식입니다.
  • UDP (User Datagram Protocol): IP 기반 위에서 프로세스 간 비신뢰성 데이터그램 전송을 지원합니다 (포트 번호 사용).
  • TCP (Transmission Control Protocol): IP 기반 위에서 신뢰성 있는(reliable), 연결 지향형(connection-oriented), 양방향(full-duplex) 통신을 제공합니다. 패킷 손실, 순서 뒤바뀜 등을 자동으로 처리합니다.

IP 주소 (IP Addresses - IPv4 기준)

  • 인터넷 상의 호스트를 식별하는 32비트 고유 번호입니다.
  • struct in_addr: C에서는 보통 이 구조체에 IP 주소를 저장합니다.
    struct in_addr {
        uint32_t s_addr; /* 네트워크 바이트 순서(Big Endian)로 저장된 주소 */
    };
  • 바이트 순서 (Byte Order):
    • CPU 아키텍처마다 데이터를 메모리에 저장하는 순서가 다릅니다 (Little Endian vs. Big Endian).
    • 네트워크 바이트 순서: 인터넷 표준은 Big Endian입니다. IP 주소, 포트 번호 등은 네트워크로 전송 시 Big Endian으로 통일해야 합니다.
    • 변환 함수: 호스트 바이트 순서 ↔ 네트워크 바이트 순서 변환이 필요합니다.
      #include <arpa/inet.h>
      uint32_t htonl(uint32_t hostlong); // Host to Network Long (32비트)
      uint16_t htons(uint16_t hostshort); // Host to Network Short (16비트)
      uint32_t ntohl(uint32_t netlong);   // Network to Host Long (32비트)
      uint16_t ntohs(uint16_t netshort);  // Network to Host Short (16비트)
      • hton 계열: 데이터를 보내거나 소켓 주소 구조체에 넣기 에 사용.
      • ntoh 계열: 받은 데이터를 프로그램에서 사용하기 에 사용.
  • 표기법 변환 (Dotted-Decimal Notation ↔ Binary):
    • 사람은 128.2.194.242 같은 점으로 구분된 십진수 표기법을 사용합니다.
    • 프로그램은 바이너리(네트워크 바이트 순서) 형식을 사용합니다.
    • 변환 함수:
      #include <arpa/inet.h>
      // 문자열 -> 바이너리 (네트워크 바이트 순서)
      int inet_pton(AF_INET, const char *src, void *dst);
      // 바이너리 (네트워크 바이트 순서) -> 문자열
      const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);

인터넷 도메인 이름 (Internet Domain Names)

  • 숫자로 된 IP 주소는 기억하기 어렵기 때문에, www.google.com과 같은 사람이 읽기 쉬운 도메인 이름을 사용합니다.
  • DNS (Domain Name System): 도메인 이름과 IP 주소를 서로 변환해주는 전 세계적인 분산 데이터베이스 시스템입니다. 계층 구조를 가집니다 (예: .com (최상위), google (2차), www (서브)).
  • localhost: 항상 자신의 컴퓨터를 가리키는 특별한 이름으로, 보통 IP 주소 127.0.0.1에 매핑됩니다.

인터넷 연결 (Internet Connections - TCP 기준)

  • 특징: 신뢰성 (데이터 순서 보장), 점대점 (Point-to-Point), 전이중 (Full-Duplex).
  • 소켓 (Socket): 네트워크 연결의 종단점(Endpoint). 프로세스가 네트워크를 통해 데이터를 주고받는 창구 역할을 합니다.
  • 소켓 주소 (Socket Address): IP 주소:포트 번호 (예: 128.2.194.242:80)로 구성됩니다.
    • 포트 번호: 같은 호스트 내에서 실행 중인 여러 네트워크 통신 프로세스 중 특정 프로세스를 식별합니다.
      • 임시 포트 (Ephemeral Port): 클라이언트가 연결 요청 시 커널이 자동으로 할당하는 포트. 연결 동안만 사용됩니다.
      • 잘 알려진 포트 (Well-Known Port): 특정 서비스를 제공하는 서버가 사용하는 고정된 포트 (예: HTTP 서버 - 80번, SMTP 서버 - 25번).
  • 소켓 페어 (Socket Pair): 특정 TCP 연결을 유일하게 식별하는 정보입니다.
    • (클라이언트 IP:클라이언트 포트, 서버 IP:서버 포트) 형태의 튜플로 표현됩니다.
    • (cliaddr:cliport, servaddr:servport)

소켓 인터페이스 (The Socket Interface)

네트워크 애플리케이션을 만들기 위해 사용하는 함수들의 집합(API)입니다. UNIX I/O 함수(read, write, close 등)와 함께 사용됩니다.

소켓 주소 구조체

  • 소켓 함수들은 주소 정보를 담기 위해 특정 구조체를 사용합니다.
  • IPv4 소켓 주소 구조체 (sockaddr_in):
    struct sockaddr_in {
        uint16_t sin_family;  /* 주소 체계 (항상 AF_INET) */
        uint16_t sin_port;    /* 포트 번호 (네트워크 바이트 순서) */
        struct in_addr sin_addr; /* IP 주소 (네트워크 바이트 순서) */
        unsigned char sin_zero[8]; /* 사용되지 않음 (패딩) */
    };
  • 일반 소켓 주소 구조체 (sockaddr):
    struct sockaddr {
        uint16_t sa_family; /* 주소 체계 */
        char sa_data[14];   /* 주소 데이터 (IP 주소 및 포트 번호 포함) */
    };
    • sockaddr_in 구조체를 bind, connect, accept 같은 함수에 전달할 때는 (struct sockaddr *)로 타입 캐스팅합니다. 커널은 sa_family 필드를 보고 실제 구조체 타입을 파악합니다.

소켓의 두 가지 관점

  • 커널 관점: 소켓은 통신 종단점이며, 커널이 관리하는 내부 객체입니다.
  • 애플리케이션 관점: 소켓은 파일 디스크립터(File Descriptor)입니다. 따라서 일반 파일처럼 read(), write(), close() 같은 표준 I/O 함수를 사용하여 데이터를 주고받을 수 있습니다.

socket() 함수: 소켓 생성

클라이언트와 서버 모두 통신을 시작하기 전에 소켓 디스크립터를 생성해야 합니다.

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

int socket(int domain, int type, int protocol);
  • domain: 프로토콜 체계 (e.g., AF_INET for IPv4).
  • type: 소켓 타입 (e.g., SOCK_STREAM for TCP, SOCK_DGRAM for UDP).
  • protocol: 특정 프로토콜 지정 (보통 0을 사용하여 domaintype에 맞는 기본 프로토콜 자동 선택).
  • 반환값: 성공 시 소켓 디스크립터 (음이 아닌 정수), 실패 시 -1.
  • 예시 (TCP/IPv4): int sockfd = socket(AF_INET, SOCK_STREAM, 0);

connect() 함수: 클라이언트가 서버에 연결 요청

클라이언트가 생성된 소켓을 사용하여 서버와의 연결을 시도합니다.

#include <sys/socket.h>

int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
  • clientfd: socket()으로 생성한 클라이언트의 소켓 디스크립터.
  • addr: 연결할 서버의 소켓 주소 정보를 담고 있는 구조체 포인터 (struct sockaddr_instruct sockaddr *로 캐스팅).
  • addrlen: 서버 주소 구조체의 크기.
  • 동작: 지정된 서버 주소로 TCP 연결(3-way handshake)을 시도합니다. 연결이 성공하거나 오류가 발생할 때까지 함수는 보통 블록(block)됩니다.
  • 성공 시: clientfd는 데이터를 읽고 쓸 준비가 되며, 연결은 소켓 페어 (클라이언트 IP:자동 할당된 포트, 서버 IP:서버 포트)로 식별됩니다.

서버측 함수: bind(), listen(), accept()

bind() 함수: 소켓에 주소 할당

서버가 사용할 소켓 디스크립터에 자신의 IP 주소와 잘 알려진 포트 번호를 "묶는" 역할을 합니다.

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: socket()으로 생성한 서버의 소켓 디스크립터.
  • addr: 서버 자신의 IP 주소와 포트 번호를 담고 있는 소켓 주소 구조체 포인터 (struct sockaddr_instruct sockaddr *로 캐스팅). IP 주소 부분에 INADDR_ANY를 사용하면 커널이 알아서 적절한 호스트 IP를 사용합니다.
  • addrlen: 서버 주소 구조체의 크기.
  • 목적: 클라이언트가 특정 주소와 포트로 보낸 요청이 이 sockfd로 전달되도록 합니다.

listen() 함수: 소켓을 리스닝 상태로 전환

서버가 클라이언트의 연결 요청을 받아들일 준비가 되었음을 커널에 알립니다. socket()으로 생성된 소켓은 기본적으로 클라이언트용(능동 소켓)으로 간주되므로, 서버는 이 함수를 호출하여 소켓을 서버용(수동 리스닝 소켓)으로 만들어야 합니다.

#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • sockfd: bind()까지 완료된 서버의 소켓 디스크립터.
  • backlog: 연결 요청 대기 큐(Queue)의 최대 크기. 아직 accept()되지 않은 연결 요청을 임시로 저장하는 공간입니다.
  • 결과: sockfd는 이제 리스닝 소켓(Listening Socket)이 됩니다.

accept() 함수: 클라이언트 연결 수락

리스닝 소켓으로 들어오는 클라이언트의 연결 요청을 수락하고, 실제 통신에 사용할 새로운 소켓을 생성합니다.

#include <sys/socket.h>

int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen);
  • listenfd: listen() 함수 호출로 만들어진 리스닝 소켓 디스크립터.
  • addr: 연결 요청을 한 클라이언트의 주소 정보를 저장할 구조체 포인터.
  • addrlen: addr 구조체의 크기를 저장할 변수의 포인터. 함수 호출 전에는 버퍼 크기를, 호출 후에는 실제 클라이언트 주소 구조체의 크기를 담습니다.
  • 동작: 클라이언트의 연결 요청이 들어올 때까지 블록(block)됩니다. 요청이 오면, 대기 큐에서 첫 번째 요청을 꺼내 새로운 소켓 디스크립터(연결 소켓)를 생성하고, 클라이언트의 주소 정보를 addr에 채웁니다.
  • 반환값: 성공 시 연결 소켓 디스크립터(Connected Socket Descriptor), 실패 시 -1.
  • 중요 구분:
    • 리스닝 디스크립터 (listenfd): 클라이언트 연결 요청을 받는 용도. 서버가 실행되는 동안 계속 존재합니다. 이 디스크립터로 직접 데이터 통신을 하지는 않습니다.
    • 연결 디스크립터 (connfd, accept의 반환값): 특정 클라이언트와의 실제 데이터 통신에 사용되는 소켓. 각 클라이언트 연결마다 새로 생성되며, 해당 클라이언트와의 통신이 끝나면 close()됩니다.

호스트 및 서비스 변환 (Host and Service Conversion)

프로그램에서는 도메인 이름(www.example.com)이나 서비스 이름(http)을 사용하지만, 소켓 함수는 바이너리 IP 주소와 포트 번호가 필요합니다. 이 변환을 도와주는 함수들이 있습니다.

  • getaddrinfo(): 호스트 이름/주소 문자열, 서비스 이름/포트 문자열을 입력받아, 소켓 주소 구조체(struct sockaddr) 정보가 담긴 연결 리스트(linked list)를 반환합니다. 이 함수는 프로토콜(IPv4/IPv6) 독립적인 코드를 작성하는 데 매우 유용합니다.

    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netdb.h>
    
    int getaddrinfo(const char *host, const char *service,
                    const struct addrinfo *hints,
                    struct addrinfo **result);
    
    void freeaddrinfo(struct addrinfo *res); // 결과 리스트 메모리 해제
    const char *gai_strerror(int errcode);  // 에러 코드 -> 에러 메시지 문자열
    • host: 호스트 이름 (e.g., "www.google.com") 또는 IP 주소 문자열 (e.g., "142.250.196.196").
    • service: 서비스 이름 (e.g., "http") 또는 포트 번호 문자열 (e.g., "80").
    • hints: 검색 조건을 지정하는 addrinfo 구조체 포인터 (e.g., 주소 체계, 소켓 타입 지정). NULL일 경우 기본값 사용.
    • result: 결과를 저장할 addrinfo 구조체 포인터의 주소. 성공 시 이 포인터는 결과 연결 리스트의 시작을 가리킵니다.
    • 사용 패턴: getaddrinfo 호출 -> 반환된 리스트 순회 -> 각 노드 정보로 socket() 시도 -> 성공하면 connect() (클라이언트) 또는 bind() (서버) 시도 -> 성공하면 루프 탈출, 실패하면 소켓 닫고 다음 노드 시도 -> 사용 후 freeaddrinfo로 메모리 해제.
  • getnameinfo(): 소켓 주소 구조체(struct sockaddr)를 입력받아, 호스트 이름과 서비스 이름 문자열로 변환합니다. (역방향 변환)

    #include <sys/socket.h>
    #include <netdb.h>
    
    int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                    char *host, size_t hostlen,
                    char *service, size_t servlen, int flags);
    • sa: 변환할 소켓 주소 구조체 포인터.
    • salen: sa 구조체의 크기.
    • host, hostlen: 호스트 이름을 저장할 버퍼와 그 크기.
    • service, servlen: 서비스 이름을 저장할 버퍼와 그 크기.
    • flags: 동작을 제어하는 플래그 (e.g., NI_NUMERICHOST - DNS 조회 없이 IP 주소 문자열 반환, NI_NUMERICSERV - 서비스 이름 조회 없이 포트 번호 문자열 반환).

소켓 인터페이스 헬퍼 함수 (Helper Functions)

CSAPP 라이브러리 등에서는 일반적인 클라이언트/서버 설정을 단순화하는 래퍼(wrapper) 함수를 제공합니다.

  • open_clientfd(): 클라이언트 측에서 getaddrinfo, socket, connect 과정을 캡슐화하여 지정된 호스트/포트로의 연결을 설정하고 연결된 소켓 디스크립터를 반환합니다.
    int open_clientfd(char *hostname, char *port);
  • open_listenfd(): 서버 측에서 getaddrinfo, socket, bind, listen 과정을 캡슐화하여 리스닝 소켓 디스크립터를 생성하고 반환합니다.
    int open_listenfd(char *port);

에코 클라이언트 및 서버 예제 (Echo Client and Server)

소켓 인터페이스 사용법을 보여주는 간단한 예제입니다. 클라이언트는 표준 입력에서 텍스트를 읽어 서버로 보내고, 서버는 받은 텍스트를 그대로 클라이언트에게 돌려보냅니다. 클라이언트는 서버로부터 받은 텍스트를 표준 출력에 표시합니다.

  • 클라이언트 (주요 로직):

    1. open_clientfd(host, port) 호출하여 서버에 연결하고 clientfd 얻기.
    2. 루프 시작:
      • 표준 입력에서 한 줄 읽기 (Fgets).
      • 읽은 내용을 clientfd를 통해 서버로 전송 (Rio_writen).
      • clientfd로부터 서버의 응답(에코) 읽기 (Rio_readlineb).
      • 받은 내용을 표준 출력에 쓰기 (Fputs).
    3. 루프 종료 (EOF 또는 서버 연결 종료 시).
    4. Close(clientfd) 호출하여 연결 닫기.
  • 서버 (주요 로직):

    1. open_listenfd(port) 호출하여 리스닝 소켓 listenfd 얻기.
    2. 무한 루프 시작:
      • Accept(listenfd, ...) 호출하여 클라이언트 연결 요청 대기 및 수락, 새로운 연결 소켓 connfd 얻기.
      • (선택) Getnameinfo로 연결된 클라이언트 정보 얻기.
      • echo(connfd) 함수 호출하여 클라이언트와 통신 처리.
      • Close(connfd) 호출하여 해당 클라이언트와의 연결 닫기. (다음 클라이언트 요청 처리를 위해 listenfd는 계속 열려 있음).
  • echo() 함수 (서버측):

    1. connfd를 인자로 받음.
    2. 루프 시작:
      • connfd로부터 데이터 읽기 (Rio_readlineb). 읽은 바이트 수가 0이면 클라이언트가 연결을 닫은 것이므로 루프 종료.
      • (선택) 서버 로그 출력.
      • 읽은 데이터를 그대로 connfd로 다시 쓰기 (Rio_writen).
    3. 루프 종료.
  • 커널의 역할: 클라이언트와 서버 프로세스는 소켓 API(시스템 콜)를 통해 커널에 요청합니다. 실제 TCP/IP 통신 처리(연결 설정, 데이터 패킷화, 전송, 수신, 오류 제어, 버퍼링 등)는 운영체제 커널이 담당합니다.

⛏ 오늘의 삽질

그림으로도 어렵다

profile
제 Velog에 오신 모든 분들이 작더라도 인사이트를 얻어가셨으면 좋겠습니다 :)

0개의 댓글