Krafton Jungle/4. CSAPP

[Computer System] ⑪ 네트워크 프로그래밍 (1)

munsik22 2025. 5. 1. 17:51

11.1 클라이언트-서버 프로그래밍 모델

🔹 모든 네트워크 애플리케이션은 클라이언트-서버 모델에 기반함
🔹 클라이언트는 서비스를 요청하고, 서버는 자원을 관리하며 그 요청을 처리함
🔹 트랜잭션 순서
    1. 클라이언트가 요청 전송
    2. 서버가 요청 처리
    3. 서버가 응답 전송
    4. 클라이언트가 응답 수신 및 처리
🔹 클라이언트와 서버는 모두 프로세스이며, 같은 호스트에서도 실행될 수 있음

[출처] 위키피디아

 

클라이언트 서버 모델client–server model은 서비스 요청자인 클라이언트와 서비스 자원의 제공자인 서버 간에 작업을 분리해주는 분산 애플리케이션 구조이자 네트워크 아키텍처를 나타낸다. 웹 시스템도 확장된 클라이언트 서버 시스템으로 분류되나, 일반적으로는 클라이언트 서버 시스템이라고 하면 웹 시스템이 나오기 이전의, 사용자 PC에는 클라이언트가 설치되어 화면을 처리하고 서버에서는 자료를 처리하는 시스템을 의미한다.


모든 네트워크 응용 프로그램은 클라이언트-서버 모델을 기반으로 한다. 이 모델에서 응용 프로그램은 서버 프로세스와 하나 이상의 클라이언트 프로세스로 구성된다. 서버는 어떤 자원을 관리하며, 그 자원을 조작함으로써 클라이언트에게 서비스를 제공한다.

  • 클라이언트-서버 모델의 기본 구조:
    • 서버: 어떤 자원을 관리하고, 그 자원을 조작하여 클라이언트에게 서비스를 제공한다.
      (예: 웹 서버(디스크 파일 관리), FTP 서버(디스크 파일 저장 및 검색), 이메일 서버(스풀 파일 읽기 및 업데이트))
    • 클라이언트: 서버가 제공하는 서비스를 요청한다.

클라이언트-서버 트랜잭션

  • 클라이언트-서버 트랜잭션 (4단계)
    1. 클라이언트 요청: 클라이언트가 서비스가 필요할 때, 서버에 요청을 보내 트랜잭션을 시작한다.
      (예: 웹 브라우저가 파일이 필요할 때 웹 서버에 요청을 보낸다.)
    2. 서버 처리: 서버는 요청을 받고, 해석하고, 적절한 방식으로 자원을 조작한다.
      (예: 웹 서버가 브라우저로부터 요청을 받으면 디스크 파일을 읽는다.)
    3. 서버 응답: 서버는 클라이언트에게 응답을 보내고 다음 요청을 기다린다.
      (예: 웹 서버가 파일을 클라이언트에게 보낸다.)
    4. 클라이언트 처리: 클라이언트는 응답을 받고 처리한다.
      (예: 웹 브라우저가 서버로부터 페이지를 받으면 화면에 표시한다.)
  • 프로세스와 호스트의 구분
    • 클라이언트와 서버는 기계나 호스트가 아닌 프로세스이다.
    • 단일 호스트에서 여러 다른 클라이언트와 서버를 동시에 실행할 수 있다.
    • 클라이언트와 서버 간의 트랜잭션은 동일하거나 다른 호스트에서 이루어질 수 있다.
    • 클라이언트-서버 모델은 클라이언트와 서버가 호스트에 어떻게 매핑되는지에 관계없이 동일하다.
더보기

[출처] Difference Between Two-Tier And Three-Tier Database Architecture | GeeksforGeeks

[출처] 3-Tier Architecture | IBM

🔸 2-Tier Architecture

2-Tier 아키텍처에서는 응용프로그램 로직이 클라이언트의 사용자 인터페이스 또는 서버의 DB (또는 둘 다)에 내장된다. 2-Tier 클라이언트/서버 아키텍처에서는 사용자 시스템 인터페이스가 일반적으로 사용자의 데스크톱 환경에 위치하며, DB 관리 서비스는 일반적으로 여러 클라이언트에 서비스를 제공하는 서버 시스템에 위치한다.

🔸 3-Tier Architecture

3-Tier 아키텍처는 응용프로그램을 프레젠테이션 계층(또는 클라이언트 계층), 데이터가 처리되는 애플리케이션 계층(또는 비즈니스 계층), 그리고 응용프로그램과 관련된 데이터가 저장 및 관리되는 데이터 계층이라는 3개의 논리적이고 물리적인 컴퓨팅 계층으로 구성하는 확립된 소프트웨어 애플리케이션 아키텍처다.

  • Presentation Tier - 프론트엔드 (HTML, CSS, Javascript 등)
    일반 사용자가 응용프로그램과 상호작용하는 응용프로그램의 사용자 인터페이스 및 커뮤니케이션 계층이다. 주요 목적은 정보를 표시하고 사용자로부터 정보를 수집하는 것이다. 이 최상위 레벨 계층은 예를 들어 웹 브라우저, 데스크탑 애플리케이션 또는 GUI에서 실행될 수 있다.
  • Application Tier - 미들웨어/백엔드 (Python, PHP, Java 등)
    특정 비즈니스 규칙 세트인 비즈니스 논리를 사용하여 프레젠테이션 계층에서 수집된 정보가 처리된다(경우에 따라 데이터 계층의 다른 정보와 관련하여 처리됨). 또한 데이터 계층의 데이터를 추가, 삭제 또는 수정할 수도 있다.
  • Data Tier - 백엔드 (MySQL, pymongo 등)
    응용프로그램이 처리하는 정보가 저장 및 관리되는 곳이다.

2-Tier 아키텍처는 클라이언트가 데이터베이스에 직접 연결되므로 규모가 작고 간단한 설정에 적합하다. 반면 3-Tier 아키텍처는 애플리케이션 서버라는 중간 계층을 추가하여 더 복잡한 작업을 관리하고 시스템 확장성을 높이고 업데이트 및 보안을 용이하게 한다. 2-Tier 아키텍처와 3-Tier 아키텍처 중 어떤 것을 선택할지는 프로젝트의 규모와 필요에 따라 달라질 수 있다. 소규모 프로젝트에서는 2-Tier 아키텍처가 적합하지만, 규모가 크고 복잡한 시스템에는 일반적으로 3-Tier이 더 좋은 선택이다.

항목 2-Tier 3-Tier
아키텍처 기본 클라이언트-서버 모델 웹 기반 애플리케이션
애플리케이션 로직 클라이언트의 UI나 서버의 DB에 내장됨 데이터와 사용자 인터페이스와 분리됨
구성 클라이언트 + 데이터 프레젠테이션 + 애플리케이션 + 데이터
빌드 및 유지보수 난이도 쉬움 어려움
실행 속도 느림 빠름
보안성 낮음 높음
성능 저하 속도 빠름 비교적 느림

11.2 네트워크

🔹 네트워크는 I/O 장치처럼 데이터 송수신 역할을 함
🔹 LAN(Local Area Network): 이더넷 기반, 하나의 허브에 여러 호스트가 연결됨
🔹 브리지(Bridge)를 통해 여러 LAN을 연결 → 브리지드 이더넷 형성
🔹 네트워크 인터페이스는 DMA로 메모리와 데이터를 교환함
🔹 프레임: 헤더 + 페이로드 구조로 전송됨

 

클라이언트와 서버는 종종 별도의 호스트에서 실행되며 컴퓨터 네트워크의 하드웨어 및 소프트웨어 자원을 사용하여 통신한다.

네트워크 호스트의 HW 구성

  • 호스트에게 네트워크의 역할
    • 호스트에게 네트워크는 위 그림에 나타난 것처럼 데이터를 주고받는 또 다른 I/O 장치일 뿐이다.
  • 물리적 인터페이스 및 데이터 전송
    • I/O 버스의 확장 슬롯에 꽂힌 어댑터가 네트워크에 대한 물리적 인터페이스를 제공한다.
    • 네트워크로부터 수신된 데이터는 일반적으로 DMA 전송을 통해 어댑터에서 I/O 버스와 메모리 버스를 거쳐 메모리로 복사된다.
    • 마찬가지로 메모리에서 네트워크로도 데이터를 복사할 수 있다.
  • 네트워크의 물리적 구조
    • 물리적으로 네트워크는 지리적 근접성에 따라 구성된 계층적 시스템이다.
    • 가장 낮은 수준은 건물이나 캠퍼스에 걸쳐 있는 LAN(Local Area Network, 근거리 통신망)이다.
    • 가장 대중적인 LAN 기술은 이더넷Ethernet이다. 이더넷은 3Mb/s에서 10Gb/s까지 진화하며 놀라울 정도로 탄력적임이 입증되었다.

이더넷 세그먼트

  • 이더넷 세그먼트
    • 구성 요소: 와이어(꼬인 쌍선)와 허브
    • 연결 범위: 작은 영역 (방, 건물 층)
    • 와이어 대역폭: 일반적으로 100Mb/s 또는 1Gb/s
    • 허브 기능: 각 포트에서 수신된 모든 비트를 다른 모든 포트로 복사
  • 이더넷 어댑터 및 프레임
    • 각 이더넷 어댑터는 전역적으로 고유한 48비트 주소를 가진다.
    • 호스트는 프레임이라는 비트 덩어리를 세그먼트의 다른 호스트로 보낼 수 있다.
    • 프레임에는 소스, 대상, 프레임 길이를 식별하는 헤더 비트와 데이터 페이로드가 포함된다.
    • 모든 호스트 어댑터가 프레임을 보지만, 대상 호스트만 실제로 읽는다.

브릿지로 연결된 이더넷 세그먼트

  • 브릿지형 이더넷
    • 여러 이더넷 세그먼트는 전선(와이어)와 브릿지라는 작은 상자를 사용하여 더 큰 LAN인 브리짓형 이더넷으로 연결될 수 있다.
    • 브릿지형 이더넷은 건물 전체나 캠퍼스까지 연결할 수 있다.
    • 브릿지형 이더넷에서는 브릿지 간 연결 와이어와 브릿지와 허브 간 연결 와이어가 있다.
    • 와이어 대역폭은 다를 수 있다 (예: 브릿지 간 1Gb/s, 허브-브릿지 100Mb/s).
  • 브릿지의 기능
    • 브릿지는 허브보다 더 높은 와이어 대역폭을 가진다.
    • 브릿지는 분산 알고리즘을 사용하여 어떤 호스트가 어떤 포트에서 도달 가능한지 자동으로 학습한다.
    • 필요한 경우에만 프레임을 한 포트에서 다른 포트로 선택적으로 복사한다.
    • 예: 호스트 A가 같은 세그먼트의 호스트 B에게 프레임을 보내면 브릿지 X는 프레임을 버려서 다른 세그먼트의 대역폭을 절약한다.
    • 예: 호스트 A가 다른 세그먼트의 호스트 C에게 프레임을 보내면 브릿지 X는 프레임을 브릿지 Y에 연결된 포트로만 복사하고, 브릿지 Y는 호스트 C의 세그먼트에 연결된 포트로만 복사한다.

LAN의 개념도

💡 라우팅(Routing) 흐름:
내 컴퓨터 → 집 라우터 → 엣지 라우터(동네 단위) → ISP 라우터 (지역 단위) → 백본 라우터 (국가 단위) → 백본 라우터 → ISP 라우터 → 엣지 라우터 → 집 라우터 → 목적지 컴퓨터
- 백본 라우터 : 국가 단위 WAN들을 모으는 중앙 노드
  • 라우터Router의 역할
    • 호환되지 않는 여러 LAN을 연결하여 인터넷을 형성하는 특수한 컴퓨터
    • 각 라우터는 연결된 네트워크마다 어댑터(포트)를 가짐
  • WAN과의 연결
    • 라우터는 WAN(Wide-Area Network, 광역 통신망)으로 알려진 고속 점대점 전화망도 연결할 수 있다.
  • 인터넷 구성
    • 일반적으로 라우터는 임의의 LAN 및 WAN의 쌍을 사용하여 인터넷을 구축하는 데 사용될 수 있다.

3개의 라우터로 연결된 2개의 LAN과 WAN을 가진 인터넷 예시

 

인터넷의 중요한 특징은 근본적으로 다르고 호환되지 않는 기술을 가진 다양한 LAN과 WAN으로 구성될 수 있다는 것이다. 각 호스트는 물리적으로 서로 연결되어 있지만, 소스 호스트가 이러한 모든 호환되지 않는 네트워크를 통해 다른 대상 호스트에게 데이터 비트를 보내는 것이 어떻게 가능할까? 그것은 바로 문식이 덕분이다.

해답은 여러 가지 네트워크 간의 차이를 줄여주는 각 호스트와 라우터에서 돌고 있는 프로토콜 소프트웨어의 계층이다. 이 소프트웨어는 호스트와 라우터가 데이터를 전송하기 위해 협력하는 방법을 규정하는 프로토콜을 구현한다. 이 프로토콜은 두 가지 기본 기능을 제공해야 한다.

  • 명명 체계 Naming scheme
    • 서로 다른 LAN 기술은 호스트에 주소를 할당하는 방식이 다르고 호환되지 않는다.
    • 인터넷 프로토콜은 호스트 주소에 대한 통일된 형식을 정의함으로써 이러한 차이를 해소한다.
    • 각 호스트에는 고유하게 식별되는 이러한 인터넷 주소 중 하나 이상이 할당된다.
  • 전달 메커니즘 Delivery mechanism
    • 서로 다른 네트워킹 기술은 와이어에 비트를 인코딩하고 이 비트를 프레임으로 패키징하는 방식이 다르고 호환되지 않는다.
    • 인터넷 프로토콜은 데이터 비트를 패킷packet이라는 별도의 청크로 묶는 통일된 방식을 정의함으로써 이러한 차이를 해소한다.
    • 패킷은 패킷 크기와 소스 및 대상 호스트 주소를 포함하는 헤더와 소스 호스트에서 보낸 데이터 비트를 포함하는 페이로드로 구성된다.

인터넷에서 데이터가 하나의 호스트에서 다른 호스트로 이동하는 방법

 

위 그림은 호스트와 라우터가 호환되지 않는 LAN 간에 데이터를 전송하기 위해 인터넷 프로토콜을 사용하는 방법을 보여주는 예시이다. 예시 인터넷은 라우터에 의해 연결된 두 개의 LAN으로 구성된다. LAN1에 연결된 호스트 A에서 실행되는 클라이언트는 LAN2에 연결된 호스트 B에서 실행되는 서버로 데이터 바이트 시퀀스를 전송한다. 기본적인 8단계는 다음과 같다.

  1. 클라이언트 데이터 복사: 호스트 A의 클라이언트는 시스템 호출을 통해 데이터를 클라이언트의 가상 주소 공간에서 커널 버퍼로 복사한다.
  2. 프레임 생성 (호스트 A): 호스트 A의 프로토콜 소프트웨어는 인터넷 헤더와 LAN1 프레임 헤더를 데이터에 추가하여 LAN1 프레임을 생성한다. 인터넷 헤더는 인터넷 호스트 B의 주소를 포함하며, LAN1 프레임 헤더는 라우터의 주소를 포함한다. 그런 다음 프레임을 어댑터로 전달한다. LAN1 프레임의 페이로드가 인터넷 패킷이며, 이 패킷의 페이로드가 실제 사용자 데이터라는 점에 주목해야 한다. 이러한 종류의 캡슐화Encapsulation는 인터넷 네트워킹의 근본적인 통찰 중 하나이다.
  3. 프레임 네트워크 전송 (LAN1): LAN1 어댑터는 프레임을 네트워크로 복사한다.
  4. 프레임 수신 및 전달 (라우터): 프레임이 라우터에 도착하면 라우터의 LAN1 어댑터가 와이어에서 이를 읽고 프로토콜 소프트웨어로 전달한다.
  5. 패킷 포워딩 (라우터): 라우터는 인터넷 패킷 헤더에서 대상 인터넷 주소를 가져와 이를 라우팅 테이블의 인덱스로 사용하여 패킷을 어디로 포워딩할지 결정한다. 이 경우 LAN2이다. 그런 다음 라우터는 기존 LAN1 프레임 헤더를 제거하고 호스트 B의 주소를 포함하는 새로운 LAN2 프레임 헤더를 앞에 추가한 후 결과 프레임을 어댑터로 전달한다.
  6. 프레임 네트워크 전송 (LAN2): 라우터의 LAN2 어댑터는 프레임을 네트워크로 복사한다.
  7. 프레임 수신 (호스트 B): 프레임이 호스트 B에 도착하면 어댑터가 와이어에서 프레임을 읽고 프로토콜 소프트웨어로 전달한다.
  8. 데이터 추출 및 복사 (호스트 B): 마지막으로 호스트 B의 프로토콜 소프트웨어는 패킷 헤더와 프레임 헤더를 제거한다. 서버가 데이터를 읽는 시스템 호출을 하면 프로토콜 소프트웨어는 결과 데이터를 서버의 가상 주소 공간으로 복사할 것이다.

이 설명은 TCP/IP 계층 모델의 관점에서 인터넷을 통해 데이터가 전송되는 과정을 설명한 것이라고도 볼 수 있다.

단계 설명 TCP/IP 계층 대응되는 OSI 계층
1. 클라이언트 시스템 콜 호출 → 커널 복사 사용자 프로세스 → 커널 공간 응용 계층 응용/표현/세션
2. 인터넷 헤더 + LAN 프레임 헤더 생성 인터넷 + 링크 계층 전송 / 인터넷 / 링크 계층 전송/네트워크/데이터링크
3. LAN1 어댑터가 프레임 전송 물리 네트워크 전송 물리 계층 물리 계층
4. 라우터가 프레임 수신 링크 계층 처리 링크 계층 데이터링크 계층
5. 라우터가 IP 주소로 라우팅 → 새 LAN2 헤더 붙임 인터넷 계층에서 포워딩 인터넷 계층 네트워크 계층
6. LAN2 어댑터 전송 물리 매체 물리 계층 물리 계층
7. 호스트 B가 프레임 수신 → 소프트웨어 전달 링크 → 인터넷 계층 링크 + 인터넷 데이터링크 + 네트워크
8. 서버가 데이터 수신 시스템 콜로 가상 주소에 복사 응용 계층에서 처리 완료 응용 계층 응용 계층
  • 다른 이슈들
    • 서로 다른 네트워크드링 서로 다른 최대 프레임 크기(세그멘테이션)를 가진다면?
    • 라우터들은 어디로 프레임을 전달해야 하는지 어떻게 아는가?
    • 라우터들은 네트워트 구조가 변경되는지 어떻게 아는가?
    • 패킷이 손실된다면?
  • 이러한 질문들은 컴퓨터 네트워킹이라고 알려진 시스템 영역에서 다뤄진다.

11.3 글로벌 IP 인터넷

🔹 인터넷은 전 세계 호스트들의 집합이며 TCP/IP 프로토콜을 사용함
🔹 IP (Internet Protocol): 각 호스트에 32비트 주소(IP)를 부여 비신뢰성 전송 제공 (패킷 손실 가능)
🔹 TCP (Transmission Control Protocol): 연결 지향, 신뢰성 보장 양방향, 스트림 기반 통신
🔹 클라이언트/서버는 소켓을 통해 통신하며, TCP/IP 커널 코드와 연동함.

인터넷 클라이언트-서버 애플리케이션의 HW/SW 구조

 

각 인터넷 호스트는 TCP/IP 프로토콜Transmission Control Protocol/Internet Protocol을 구현하는 SW를 실행하며, 이는 거의 모든 현대 컴퓨터 시스템에서 지원된다. 인터넷 클라이언트와 서버는 함수와 유닉스 I/O 함수를 혼합하여 통신한다. 소켓 함수는 일반적으로 커널로 트랩하여 TCP/IP의 다양한 커널 모드 함수를 호출하는 시스템 호출로 구현된다.

  • TCP/IP 프로토콜
    • 거의 모든 현대 컴퓨터 시스템에서 지원되는 인터넷 프로토콜
    • 각 인터넷 호스트에서 실행되는 소프트웨어로 구현됨
    • 클라이언트와 서버는 소켓 인터페이스sockets interface 함수와 UNIX I/O 함수를 사용하여 통신
  • 소켓 함수
    • 일반적으로 시스템 호출로 구현되어 커널로 트랩하고 TCP/IP의 다양한 커널 모드 함수를 호출
  • TCP/IP 패밀리
    • 다양한 기능을 제공하는 프로토콜의 집합
  • IPInternet Protocol
    • 기본적인 명명 체계와 전달 메커니즘 제공
    • 하나의 인터넷 호스트에서 다른 호스트로 데이터그램이라는 패킷을 보낼 수 있음
    • 데이터그램이 네트워크에서 손실되거나 중복되어도 복구 노력을 하지 않는다는 점에서 신뢰할 수 없다.
  • UDPUser Datagram Protocol
    • IP를 약간 확장하여 호스트 간이 아닌 프로세스 간 데이터그램 전송 가능
  • TCPTransmission Control Protocol
    • IP 위에 구축된 복잡한 프로토콜
    • 프로세스 간의 신뢰성 있는 전이중(양방향) 연결 제공

프로그래머의 관점에서 인터넷을 다음과 같은 속성을 가진 전 세계적인 호스트들의 집합으로 생각할 수 있다.

  • 호스트 집합은 32비트 IP 주소 집합에 매핑된다. (예: 121.53.105.193)
  • IP 주소 집합은 인터넷 도메인 이름이라는 식별자 집합에 매핑된다. (예: 121.53.105.193는 daum.net에 매핑되었다.)
  • 한 인터넷 호스트의 프로세스는 연결을 통해 다른 인터넷 호스트의 프로세스와 통신할 수 있다.
구분 TCP UDP
연결 방식 연결 지향 (Connection-oriented) - 통신 전에 연결 설정 필요 비연결 지향 (Connectionless) - 연결 설정 없이 바로 통신
신뢰성 높음 - 데이터 전송 보장, 순서 보장, 오류 제어, 흐름 제어 낮음 - 데이터 전송 보장 안 함, 순서 보장 안 함
데이터 순서 순서 보장 순서 보장 안 함
속도/오버헤드 느림 - 연결 설정 및 신뢰성 보장을 위한 추가 오버헤드 발생 빠름 - 오버헤드가 적음
사용 예시 웹 브라우징 (HTTP), 파일 전송 (FTP), 이메일 (SMTP) 등 온라인 게임, 스트리밍 (영상, 음성), DNS 등

11.3.1  IP 주소

🔹 IP 주소는 인터넷에서 각 호스트를 식별하는 32비트 부호 없는 정수
 └ 예: 0x8002c2f2 = 128.2.194.242 (dotted-decimal 표기)
🔹 IPv4에서는 항상 big-endian 방식 사용됨
🔹 바이트 순서 변환 함수
 ├ htonl(), htons() – host → network byte order
 └ ntohl(), ntohs() – network → host byte order
🔹 문자열 ↔ 바이너리 변환 함수
 ├ inet_pton(AF_INET, src, dst): dotted-decimal → 네트워크 바이트
 └ inet_ntop(AF_INET, src, dst, size): 네트워크 바이트 → dotted-decimal

 

IP 주소는 부호 없는 32비트 정수(unsigned 32-bit int)이다. 네트워크 프로그램은 아래 코드에 나타난 IP 주소 구조체에 IP 주소를 저장한다.

/* IP address structure */
struct in_addr {
	uint32_t s_addr; /* Address in network byte order (big-endian) */
};
  • IPv4 주소 공간은 여러 클래스로 나뉜다.

  • w.x.y.z/n 형식의 네트워크 ID
    • n = 호스트 주소의 비트 수
    • 예: 128.2.0.0/16 → Class B address (참고)
  • 네트워크 바이트 순서
    • 인터넷 호스트는 다른 호스트 바이트 순서를 가질 수 있으므로, TCP/IP는 패킷 헤더에 네트워크를 통해 전달되는 정수 데이터 항목(IP 주소 등)에 대해 통일된 네트워크 바이트 순서(빅 엔디안)를 정의한다.
    • IP 주소 구조체의 주소는 호스트 바이트 순서가 리틀 엔디안이라도 항상 (빅 엔디안) 네트워크 바이트 순서로 저장된다.
더보기
[출처] Big Endian and Little Endian in Memory ❘ Yogin Savani

 

저장할 때 상위 바이트를 먼저 저장하는 것을 빅 엔디안Big Endian, 저장할 때 하위 바이트를 먼저 저장하는 것을 리틀 엔디안Little Endian이라고 한다.

 

동일한 데이터 0x12345678에 대해 Little Endian의 경우에는 0x78 | 0x56 | 0x34 | 0x12로 저장하게 되는데, 이를 Big Endian을 사용하는 시스템에 그대로 전송하면 0x78563412로 읽어서 문제가 발생하게 된다. 이러한 이유 때문에 네트워크에서는 항상 Big Endian을 사용하는 것으로 통일했다.

UNIX는 네트워크 바이트 순서와 호스트 바이트 순서 간 변환을 위해 다음 함수들을 제공한다.

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// Returns: value in network byte order

uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(unit16_t netshort);
// Returns: value in host byte order
  • htonl : 부호 없는 32비트 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환한다.
  • ntohl : 부호 없는 32비트 정수를 네트워크 바이트 순서에서 호스트 바이트 순서로 변환한다.
  • htons : 부호 없는 16비트 정수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환한다.
  • ntohs : 부호 없는 16비트 정수를 네트워크 바이트 순서에서 호스트 바이트 순서로 변환한다.

  • Dotted-decimal notation
    • IP 주소를 사람이 읽기 쉽게 표현하는 방법
    • 각 바이트를 십진수 값으로 나타내고, 바이트들 사이를 마침표(.)로 구분한다.
    • 예를 들어, 128.2.194.242는 주소 0x8002c2f2의 dotted-decimal 표현이다.
  • 자신의 호스트 IP 확인 (Linux)
    • 리눅스 시스템에서는 hostname -i 명령어를 사용하여 자신의 호스트의 점선 표기법 주소를 확인할 수 있다.
    • 예시 출력: 128.2.210.175

응용프로그램은 IP 주소와 dotted-decimal 표기 문자열 간에 inet_ptoninet_ntop 함수를 사용하여 변환할 수 있다.

#include <arpa/inet.h>
int inet_pton(AF_INET, const char *src, void *dst);
// Returns: 1 if OK, 0 if src is invalid dotted decimal, −1 on error

const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);
// Returns: pointer to a dotted-decimal string if OK, NULL on error
  • 함수 이름의 의미
    • 함수 이름에서 'n'은 network를, 'p'는 presentation을 의미한다.
    • 이 함수들은 32비트 IPv4 주소(AF_INET) 또는 128비트 IPv6 주소(AF_INET6)를 다룰 수 있다.
  • inet_pton
    • dotted-decimal 표기 문자열(src)을 네트워크 바이트 순서의 바이너리 IP 주소(dst)로 변환한다.
    • src가 유효한 dotted-decimal 표기 문자열을 가리키지 않으면 0을 반환한다.
    • 다른 오류 발생 시 -1을 반환하고 errno를 설정한다.
  • inet_ntop
    • 네트워크 바이트 순서의 바이너리 IP 주소(src)를 해당하는 dotted-decimal 표기 표현으로 변환한다.
    • 결과로 생성된 널 종료 문자열을 dst에 최대 size 바이트만큼 복사한다.

11.3.2 Internet Domain Name

🔹 왜 도메인 네임이 필요한가?
 → 사람에게 IP 주소(숫자)는 기억하기 어려움 → 도메인 네임 시스템(DNS) 도입
🔹 도메인 구조
 ├ 계층적 이름 체계: 예) whaleshark.ics.cs.cmu.edu
 └ .로 구분된 각 수준은 계층 구조의 한 노드
    ├ edu: 최상위 도메인 (TLD)
    ├ cmu.edu: 2차 도메인 (기관)
    └ cs.cmu.edu, ics.cs.cmu.edu: 하위 도메인
🔹 도메인 ↔ IP 매핑:
 ├ 과거: HOSTS.TXT 파일로 수동 관리
 └ 현재: DNS(Domain Name System)를 통해 전 세계적으로 분산 관리 nslookup 명령어로 확인 가능
🔹 매핑의 다양한 형태
 ├ 1 대 1 매핑: whaleshark.ics.cs.cmu.edu → 128.2.210.175
 ├ 다 대 1 매핑: cs.mit.edu, eecs.mit.edu → 동일한 IP
 └ 1 대 다 매핑: twitter.com → 여러 IP 주소

인터넷 도메인 네임 계층구조의 일부

  • DNSDomain Naming System
    • 인터넷은 도메인 네임의 집합과 IP 주소 사이의 매핑을 DNS라고 하는 전 세계에 분산된 DB로 관리한다.
    • 개념적으로 개발자들은 DNS DB를 수백 단위의 host entries의 집합으로 볼 수 있다.
      • 각 호스트 엔트리는 도메인 네임의 집합과 IP 주소 사이의 매핑을 정의한다.
      • 수학적인 의미에서 호스트 엔트리는 도메인 네임과 IP 주소의 eqivalence class(동일성 클래스)다.
  • DNS 매핑의 특징
    • nslookup을 사용해 DNS 매핑의 특성을 볼 수 있다.
    • 각 호스트는 지역적으로 정의된 도메인 네임 localhost를 가지는데, 이는 항상 loopback address 127.0.0.1에 매핑된다.
    • hostname을 사용해서 로컬 호스트의 실제 도메인 네임을 알 수 있다.
$ nslookup localhost
Address: 127.0.0.1
$ hostname
Address: whaleshark.ics.cs.cmu.edu
  • 일대일 매핑 : 가장 간단한 경우
$ nslookup whaleshark.ics.cs.cmu.edu
Address: 128.2.210.175
  • 다대일 매핑 : 다수의 도메인 네임이 동일 IP 주소에 매핑된 경우
$ nslookup cs.mit.edu
Address: 18.62.1.6
$ nslookup eecs.mit.edu
Address: 18.62.1.6
  • 다대다 매핑 : 가장 일반적인 경우
$ nslookup www.x.com
Address: 199.16.156.6
Address: 199.16.156.70
Address: 199.16.156.102
Address: 199.16.156.230

$ nslookup x.com
Address: 199.16.156.102
Address: 199.16.156.230
Address: 199.16.156.6
Address: 199.16.156.70
  • 유효한 도메인 네임이 어떤 IP 주소에도 매핑되지 않은 경우도 있다.
$ nslookup ics.cs.cmu.edu
*** Can't find ics.cs.cmu.edu: No answer

11.3.3 인터넷 연결

🔹 연결의 개념
 ├ 연결(connection): 클라이언트 프로세스 ↔ 서버 프로세스 간의 바이트 스트림
 ├ Point-to-Point: 특정 두 프로세스 사이에만 연결됨
 ├ Full-duplex: 양방향 통신 가능
 └ Reliable: 순서 보장, 손실 없는 전송 (TCP 기반)
🔹 소켓 주소 구조
 ├ 소켓 = (IP 주소, 포트 번호)
 ├ 클라이언트: 128.2.194.242:51213 (ephemeral port)
 └ 서버: 208.216.181.15:80 (well-known port)
🔹 포트의 역할
 ├ 클라이언트 포트는 커널이 임시 할당
 ├ 서버 포트는 서비스와 연결됨 (80: HTTP, 25: SMTP)
 └ /etc/services 파일에 이름-포트 매핑 존재
🔹 연결 식별자
 ├ 하나의 연결은 두 소켓 주소 쌍으로 고유 식별됨 (cliaddr:cliport, servaddr:servport)
 └ 이 구조 덕분에 여러 클라이언트가 동시에 같은 서버에 접속 가능

 

클라이언트와 서버는 연결connection을 통해 바이트 스트림을 전송하여 통신한다.

  • 연결의 속성
    • Point-to-point: 두 프로세스 쌍을 연결한다.
    • Full-duplex: 데이터가 양방향으로 동시에 흐를 수 있다.
    • Reliable: 소스에서 전송된 바이트 스트림은 대상에게 전송된 순서와 동일하게 최종적으로 수신된다.
  • 소켓Socket : 연결의 엔드 포인트다.
    • 소켓 주소Socket address는 IP 주소와 포트의 쌍(IPaddress:port)이다.
더보기

책에서는 소켓의 정의가 연결의 종착점이라고만 적혀 있다. 그래서 소켓이라는 것이 뭐하는 친구라는 걸까?

  • 소켓의 정의
    • 소켓은 네트워크 상에서 데이터를 주고받기 위한 인터페이스다.
    • 컴퓨터 네트워크에서 양쪽 프로그램 간의 통신을 위한 출입구라고 생각하면 된다.
  • 소켓 핵심 개념
요소 설명
IP 주소 상대방 컴퓨터의 "주소" (ex: 192.168.0.1)
포트 번호 해당 컴퓨터에서 어떤 프로그램과 통신할 것인지 지정하는 "우편함 번호" (ex: 80)
소켓 IP 주소 + 포트 번호의 조합으로, 네트워크 통신의 종착지 또는 출발지 역할을 함
  • 소켓 관련 함수
역할 함수
소켓 만들기 socket()
주소 할당 bind()
대기열 설정 listen()
클라이언트 받기 accept()
데이터 주고받기 read(), write()
연결 종료 close()
  • 통신 흐름 (서버)
/* 1️⃣ 소켓 생성 */
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
// → 통신용 출입구(소켓)를 만듦

/* 2️⃣ 주소와 포트 지정 */
struct sockaddr_in serveraddr;
bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
// → 이 출입구에 "우리 집 주소(127.0.0.1)"와 "우편함 번호(포트 8000)"를 붙임

/* 3️⃣ 연결 대기 */
listen(listenfd, 10);
// → 누가 올지 모르니 대문 앞에서 기다림

/* 4️⃣ 클라이언트 연결 수락 */
int connfd = accept(listenfd, ...);
// → 누군가 벨을 누르면 문 열고 접속을 허용함
// → 여기서 listenfd ≠ connfd
// → listenfd는 계속 대기용, connfd는 실제 통신용

/* 5️⃣ 데이터 송수신 */
read(connfd, buf, MAXLINE);
write(connfd, response, strlen(response));

/* 6️⃣ 연결 종료 */
close(connfd);
  • 통신 흐름 (클라이언트)
/* 1️⃣ 소켓 생성 */
int clientfd = socket(AF_INET, SOCK_STREAM, 0);

/* 2️⃣ 서버에 연결 요청 */
connect(clientfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

/* 3️⃣ 데이터 송수신 */
write(clientfd, request, strlen(request));
read(clientfd, buf, MAXLINE);

/* 4️⃣ 종료 */
close(clientfd);
  • 통신 흐름 요약
클라이언트                        서버
    | ------ connect() ----------> |
    | ------ GET /index.html ----> |
    | <----- 200 OK + html ------- |
    | ----------- close() -------> |
  • 포트Port
    • 프로세스를 식별하는 16비트 정수
    • Ephemeral port (단기 포트) : 클라이언트가 연결 요청을 할 때 클라이언트 커널에 의해 자동으로 할당된다.
    • Well-known port (잘 알려진 포트) : 서버가 제공하는 특정 서비스와 연결된다(예: 포트 80은 웹 서버와 연결된다).
    • Well-known ports와 Well-known service names 간의 매핑은 각 리눅스 머신의 /etc/services 파일에 보관되어 있다.

연결은 두 end points의 소켓 주소에 의해 고유하게 식별된다. 이 소켓 주소 쌍은 소켓 쌍socket pair이라고 알려져 있으며 다음과 같은 튜플로 표기된다: (cliaddr:cliport, servaddr:servport)

  • cliaddr : 클라이언트의 IP 주소
  • cliport : 클라이언트의 포트
  • servaddr : 서버의 IP 주소
  • servport : 서버의 포트

인터넷 연결의 구조

  • 웹 클리아언트의 소켓 주소 : 128.2.194.242:51213 (51213 : 커널이 할당한 ephemral port)
  • 웹 서버의 소켓 주소 : 208.216.181.15:80 (80 : well-known port)
  • 소켓 쌍 : (128.2.194.242:51213, 208.216.181.15:80)


11.4 소켓 인터페이스

🔹 주요 소켓 함수
 ├ socket() : 소켓 디스크립터 생성
 ├ connect() : 클라이언트가 서버에 연결 요청
 ├ bind() : 서버가 로컬 주소를 소켓에 바인딩
 ├ listen() : 서버 소켓을 수신 상태로 변경
 ├ accept() : 클라이언트 연결 수락
 ├ read()/write() : 데이터 송수신
 └ close() : 연결 종료
🔹 모든 함수는 파일 디스크립터 기반
🔹 sockaddr, sockaddr_in 등 다양한 주소 구조체 사용
🔹 getaddrinfo()로 주소 정보를 자동 생성하여 프로토콜 독립성 확보

 

소켓 인터페이스Socket interface는 네트워크 애플리케이션을 만들기 위해 UNIX I/O 함수들과 함께 사용되는 함수들의 집합이다.

소켓 인터페이스 기반 네트워크 응용프로그램의 개요


11.4.1 소켓 주소 구조체

/* IP socket address structure */
struct sockaddr_in {
    uint16_t sin_family; /* Protocol family (always AF_INET) */
    uint16_t sin_port; /* Port number in network byte order */
    struct in_addr sin_addr; /* IP address in network byte order */
    unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
};

/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
    uint16_t sa_family; /* Protocol family */
    char sa_data[14]; /* Address data */
};

  • sockaddr_in 구조체
    • sin_family : 프로토콜 종류 (IPv4 → AF_INET)
    • sin_port : 서버/클라이언트 포트번호
    • sin_addr : 실제 IP 주소 (struct in_addr 타입)

  • sockaddr 구조체 (범용용도)
    • connect, bind, accept는 sockaddr *를 받기 때문에 sockaddr_in *를 sockaddr *로 캐스팅해야 함
    • typedef struct sockaddr SA; 로 간단하게 사용함

TCP 소켓 프로토콜 동작 과정

11.4.2 socket 함수

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// Returns: nonnegative descriptor if OK, −1 on error
  • 목적: 소켓 식별자socket descripter 생성
  • 예: clientfd = socket(AF_INET, SOCK_STREAM, 0)
    • AF_INET : 32비트 IP 주소
    • SOCK_STREAM : 연결의 엔드 포인트
  • 반환값: 성공 시 디스크립터, 실패 시 -1
  • 이 함수만으로는 연결이 완성되지 않으며, 이후 connect() 또는 bind()와 함께 사용됨

11.4.3 connect 함수

클라이언트는 connect 함수를 호출해서 서버와 연결한다.

#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, −1 on error
  • 클라이언트가 서버에 연결 요청
  • 차단형 호출: 연결 완료되거나 오류 발생 시 반환
  • 연결이 성공하면 소켓은 read/write 가능 상태가 됨
  • 연결 식별자: (client IP:port, server IP:port) → (x:y, addr.sin_addr:addr.sin_port)

11.4.4 bind 함수

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, −1 on error

 

나머지 소켓 함수 bind, listen, accept는 서버에서 클라이언트와 연결하기 위해 사용된다.

  • 소켓 디스크립터에 로컬 주소(IP:port)를 할당
  • 서버가 클라이언트 요청을 받을 수 있게 만듦
  • 실패 시 이미 포트 사용 중일 수 있음 (해결법: SO_REUSEADDR)

11.4.5 listen 함수

클라이언트는 연결 요청을 시작하는 능동적인 개체이다. 서버는 클라이언트로부터 연결 요청을 기다리는 수동적인 개체이다. 기본적으로 커널은 socket 함수에 의해 생성된 디스크립터가 연결의 클라이언트 측에 위치할 능동적인 소켓에 해당한다고 가정한다. 서버는 listen 함수를 호출하여 디스크립터가 클라이언트가 아닌 서버에 의해 사용될 것임을 커널에 알린다.

#include <sys/socket.h>
int listen(int sockfd, int backlog);
// Returns: 0 if OK, −1 on error
  • 서버 소켓을 수신 대기 상태로 전환
  • backlog: 커널이 큐에 쌓을 수 있는 최대 연결 요청 수
  • 호출 후 accept() 가능

11.4.6 accept 함수

서버는 accept 함수를 호출해서 클라이언트로부터의 연결 요청을 기다린다.

#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
// Returns: nonnegative connected descriptor if OK, −1 on error

 

  • 클라이언트 연결 수락
  • 새 연결 소켓 디스크립터 connfd를 반환 (클라이언트와 1:1 통신)
  • 연결이 들어오기 전까지 차단(blocking) 상태
  • 수신된 클라이언트 주소 정보를 addr에 저장 가능
  • listenfd: 수신 대기용
  • connfd: 실제 데이터 송수신용

listening과 연결 식별자의 역할

위 그림은 리스닝 디스크립터와 연결된 디스크립터의 역할을 보여준다.

  • 1️⃣ 서버의 accept 호출
    • 서버는 accept를 호출한다.
      accept는 리스닝 디스크립터(예: 디스크립터 3)에 연결 요청이 도착하기를 기다린다.
    • (디스크립터 0-2는 표준 파일에 예약되어 있다.)
  • 2️⃣ 클라이언트의 connect 호출
    • 클라이언트는 connect 함수를 호출하여 리스닝 디스크립터(listenfd)로 연결 요청을 보낸다.
  • 3️⃣ accept 함수의 새로운 디스크립터 생성 및 연결 설정
    • accept 함수는 새로운 연결된 디스크립터(connfd, 예: 디스크립터 4)를 연다.
    • 클라이언트의 clientfd와 서버의 connfd 사이에 연결을 설정한다.
    • 그리고 connfd를 서버 애플리케이션에게 반환한다.
  • 클라이언트와 서버의 데이터 통신
    • 클라이언트도 connect 호출에서 반환된다.
    • 이 시점부터 클라이언트와 서버는 각각 clientfd와 connfd를 읽고 써서 데이터를 주고받을 수 있다.
더보기

1. socket()

int socket(int domain, int type, int protocol);
  • 역할: 소켓을 생성한다.
  • 인자
    • domain: 주소 체계 (예: AF_INET = IPv4, AF_INET6 = IPv6)
    • type: 소켓 타입 (예: SOCK_STREAM = TCP, SOCK_DGRAM = UDP)
    • protocol: 일반적으로 0 (자동으로 적절한 프로토콜 선택)
  • 반환값: 성공 시 소켓 디스크립터, 실패 시 -1

예시:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

2. bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 역할: 소켓에 IP 주소와 포트를 할당한다.
  • 서버 측에서만 사용하며, 클라이언트는 일반적으로 생략 가능.
  • addr에는 IP와 포트를 담은 구조체(sockaddr_in)를 캐스팅해서 넘겨준다.

예시:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8080);

bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

3. listen()

int listen(int sockfd, int backlog);
  • 역할: 서버 소켓을 연결 요청 대기 상태로 만든다.
  • 클라이언트가 접속할 수 있도록 준비하는 단계.
  • backlog: 대기열 크기

예시:

listen(sockfd, 5);  // 최대 5개까지 연결 요청을 대기열에 저장

4. accept()

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 역할: 클라이언트의 연결 요청을 수락하고, 새로운 소켓 생성
  • 리턴된 소켓 디스크립터를 통해 클라이언트와 통신하게 된다.

예시:

int clientfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);

5. connect()

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 역할: 클라이언트 측에서 서버에 연결 요청을 보낸다.
  • 서버의 IP와 포트를 설정하여 연결한다.

예시:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

6. close()

int close(int sockfd);
  • 역할: 사용한 소켓 디스크립터를 닫는다.
  • 리소스를 반환하기 위해 반드시 호출해야 한다.

전체 흐름 요약

[서버]
socket() → bind() → listen() → accept() → (read/write) → close()

[클라이언트]
socket() → connect() → (read/write) → close()

💻 예제 코드

🔹 TCP 서버 예제

// tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len;
    char buffer[BUFFER_SIZE];

    // 1. 소켓 생성
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 서버 주소 정보 설정
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;           // IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;   // 모든 IP에서 접속 허용
    server_addr.sin_port = htons(PORT);         // 포트번호 지정

    // 3. 바인드
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 연결 대기 상태로 전환
    if (listen(server_fd, 5) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("서버가 포트 %d에서 대기 중입니다...\n", PORT);

    // 5. 클라이언트 연결 수락
    client_addr_len = sizeof(client_addr);
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_fd < 0) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 6. 데이터 수신 및 전송
    memset(buffer, 0, BUFFER_SIZE);
    read(client_fd, buffer, BUFFER_SIZE);
    printf("클라이언트로부터 받은 메시지: %s\n", buffer);

    const char* response = "서버에서 응답: 안녕하세요!";
    write(client_fd, response, strlen(response));

    // 7. 소켓 닫기
    close(client_fd);
    close(server_fd);

    return 0;
}

🔹 TCP 클라이언트 예제

// tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 1. 소켓 생성
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 서버 주소 정보 설정
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);  // 문자열 IP를 네트워크 바이트로 변환

    // 3. 서버에 연결 요청
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 4. 메시지 전송 및 수신
    const char* message = "안녕하세요, 서버!";
    write(sockfd, message, strlen(message));

    memset(buffer, 0, BUFFER_SIZE);
    read(sockfd, buffer, BUFFER_SIZE);
    printf("서버로부터 받은 응답: %s\n", buffer);

    // 5. 소켓 닫기
    close(sockfd);

    return 0;
}

 

🔹 TCP/IP : network layer, TCP 위의 HTTP, TCP 옆의 UDP
🔹 Sockets : the low level endpoint used for processing information across a network
🔹 Server:
 ├ Create a socket with the socket()
 ├ Bind the socket to an address using the bind()
 ├ Listen for connections with the listen()
 ├ Accept a connection with the accept()
 └ To send and receive data, use the write() and read()
🔹 Client:
 ├ Create a socket with the socket()
 ├ Connect the socket to the address of the server using the connect()
 └ To send and receive data, use the write() and read()
더보기

파일 디스크립터

  • 리눅스/유닉스 시스템에서 프로세스가 파일을 다룰 때 사용하는 정수값
  • 시스템이 파일이나 자원에 할당하는 고유한 번호
  • 파일, 디렉터리, 소켓, 파이프, 장치 등 '파일처럼' 다뤄지는 모든 것에 사용됨
  • 프로세스가 open(), socket() 등으로 자원을 열 때 시스템이 할당해 줌
  • 각 프로세스마다 독립적인 파일 디스크립터 테이블을 가짐
  • 표준 입출력에 미리 할당된 번호가 있음
    • 0: 표준 입력 (stdin)
    • 1: 표준 출력 (stdout)
    • 2: 표준 에러 (stderr)
  • read(), write(), close() 등 입출력 함수에서 이 번호를 사용함
  • 자원에 접근하고 데이터를 읽고 쓰는 데 필요한 '핸들' 역할
  • 자원을 효율적으로 관리하고 추상화하는 시스템의 핵심 메커니즘 중 하나
더보기
  • 소켓
    • OS를 통해 네트워크 통신을 하는 표준 방법
    • 서버와 클라이언트가 데이터를 주고받을 때 사용하는 함수 (데이터는 세션 계층에서 전송됨)
    • 네트워크 상에서 데이터를 주고받기 위한 인터페이스
    • 컴퓨터 네트워크에서 양쪽 프로그램 간의 통신을 위한 출입구 역할
  • 데이터그램 소켓 (UDP 소켓)
    • 단방향, 비신뢰성, 비연결형
    • 연결 설정 및 해제에 따른 시간과 자원을 절약해 데이터 전송이 더 빠름
    • 온라인 게임, 실시간 방송, DNS 등에서 사용
  • 스트림 소켓 (TCP 소켓)
    • 양방향, 신뢰성, 연결형
    • 패킷을 오류 없이 순서대로 도착하도록 설계된 TCP 표준 통신 프로토콜 사용
    • 웹 브라우징, 이메일 전송(SMTP), 파일 전송(FTP) 등에서 사용

11.4.7 호스트와 서비스 변환

🔹 getaddrinfo 함수

  • 호스트 이름, 호스트 주소, 포트, 서비스 이름의 스트링 표현을 소켓 주소 구조체로 변환하는 현대적인 방식
  • 예전의 gethostbynamegetservbyname을 대체한 것
  • 장점
    • 재진입성Reentrant : 둘 이상의 쓰레드에서 호출되어도 안전하게 사용 가능 (참고)
    • 모든 프로토콜에 대해 동작 가능
  • 단점
    • 아무튼 복잡함
    • 그래도 다행인 점은 대부분의 경우에는 몇 개의 사용 패턴만 쓰인다는 것
#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);
// Returns: 0 if OK, nonzero error code on error

void freeaddrinfo(struct addrinfo *result);
// Returns: nothing

const char *gai_strerror(int errcode);
// Returns: error message
  • 문자열(도메인명, 서비스명)을 소켓 주소 구조체 리스트로 변환
  • 멀티 프로토콜 지원 (IPv4/IPv6), 재진입 가능 함수
  • 결과 리스트는 반드시 freeaddrinfo()로 해제해야 함
  • host: 도메인명 또는 dotted-IP,
  • service: 포트 번호 또는 서비스명("http", "ftp")

getaddrinfo가 리턴하는 자료구조 (result)

  • 클라이언트: socket 및 connect 호출이 성공할 때까지 각 소켓 주소를 차례대로 시도하면서 리스트를 순회한다.
  • 서버: socket 및 bind 호출이 성공할 때까지 리스트를 순회한다
/* getaddrinfo가 사용하는 addrinfo 구조체 */
struct addrinfo {
    int              ai_flags;      // 힌트 플래그
    int              ai_family;     // 주소 체계 (AF_INET 등)
    int              ai_socktype;   // 소켓 타입 (SOCK_STREAM 등)
    int              ai_protocol;   // 프로토콜 (보통 0)
    size_t           ai_addrlen;    // ai_addr의 크기
    struct sockaddr *ai_addr;      // 실제 주소 정보
    char            *ai_canonname;  // 호스트의 정식 이름
    struct addrinfo *ai_next;       // 다음 항목 포인터 (연결 리스트)
};
  • ai_family, ai_socktype, ai_protocol은 socket() 호출에 직접 사용 가능
  • ai_addr, ai_addrlen은 connect() 또는 bind()에 그대로 사용 가능
  • ai_next를 따라가며 여러 주소를 순차적으로 시도 가능
  • ai_flags는 동작을 제어하는 옵션을 제공함
    • AI_PASSIVE : 서버에서 수신 대기용 주소를 반환. host는 NULL로 설정
    • AI_CANONNAME : 첫 번째 구조체의 ai_canonname에 정식 호스트 이름 저장
    • AI_NUMERICSERV : service 문자열을 숫자로 해석하도록 강제
    • AI_ADDRCONFIG : 로컬 시스템에 IPv4 또는 IPv6가 설정된 경우에만 해당 주소 반환
    • 여러 플래그는 비트 OR(|) 연산으로 함께 지정 가능

🔹 getnameinfo 함수

  • getnameinfo의 역함수 : 소켓 주소 구조체를 문자열 형식의 호스트명 / 서비스명으로 변환
  • 예전의 gethostbyname getservbyname을 대체한 것
  • 재진입 가능 함수, 프로토콜 독립적임
#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);
// Returns: 0 if OK, nonzero error code on error
  • sa: 변환할 주소 정보
  • host: 호스트 이름이 저장될 버퍼
  • service: 서비스명 (ex: 포트번호 또는 "http")
  • flags: 출력 형식 제어
    • NI_NUMERICHOST : 도메인 대신 IP 문자열 반환
    • NI_NUMERICSERV : 서비스명 대신 포트 번호 반환
    • 하나만 필요하면 나머지 인자는 NULL, 0으로 설정 가능

🔹 예제: HOSTINFO

#include "csapp.h"

int main(int argc, char **argv) {
    struct addrinfo *p, *listp, hints;
    char buf[MAXLINE];
    int rc, flags;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
        exit(0);
    }

    /* Get a list of addrinfo records */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET; /* IPv4 only */
    hints.ai_socktype = SOCK_STREAM; /* Connections only */
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
        exit(1);
    }

    /* Walk the list and display each IP address */
    flags = NI_NUMERICHOST; /* Display address string instead of domain name */
    for (p = listp; p; p = p->ai_next) {
        Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
        printf("%s\n", buf);
    }

    /* Clean up */
    Freeaddrinfo(listp);

    exit(0);
}
  • hostinfo 프로그램의 목적
    • getaddrinfo와 getnameinfo 함수를 사용하여 도메인 이름과 연결된 IP 주소 매핑을 표시한다.
    • nslookup 프로그램과 유사한 기능
  • hints 구조체 초기화
    • getaddrinfo가 원하는 주소(이 경우 32비트 IPv4 주소, 라인 16)와 연결 종단점으로 사용될 수 있는 주소(라인 17)를 반환하도록 hints 구조체를 초기화한다.
    • 도메인 이름만 변환하도록 getaddrinfo를 호출할 때는 서비스 인자에 NULL을 전달한다.
  • getaddrinfo 호출 및 결과 처리
    • getaddrinfo 호출 후 반환된 addrinfo 구조체 리스트를 순회한다.
    • getnameinfo를 사용하여 각 소켓 주소를 점선 표기 주소 문자열로 변환한다.
  • 메모리 해제
    • 리스트 순회 후 freeaddrinfo를 호출하여 리스트를 해제하는 것이 중요하다.
    • (이 프로그램처럼 간단한 프로그램의 경우 필수는 아니라고 한다.)
  • 실행 결과 예시
    • hostinfo를 실행하면 x.com이 네 개의 IP 주소에 매핑되는 것을 확인할 수 있으며, 이는 11.3.2에서 nslookup을 사용했을 때와 동일한 결과가 출력된다.
$ ./hostinfo x.com
199.16.156.102
199.16.156.230
199.16.156.6
199.16.156.70
목적 함수 역할
문자열 → 주소 구조체 getaddrinfo() 호스트명/포트명 → sockaddr 변환
주소 구조체 → 문자열 getnameinfo() sockaddr → 호스트명/포트명 문자열
구조체 해제 freeaddrinfo() 반환된 addrinfo 리스트 메모리 해제
에러 문자열 gai_strerror() getaddrinfo/getnameinfo의 에러 메시지 조회

11.4.8 소켓 인터페이스를 위한 도움 함수들

  • 복잡한 소켓 설정 절차를 캡슐화한 도움 함수
  • 내부적으로 getaddrinfo, socket, connect/bind, listen 호출

🔹 open_clientfd 함수

#include "csapp.h"
int open_clientfd(char *hostname, char *port);
// Returns: descriptor if OK, −1 on error

int open_clientfd(char *hostname, char *port) {
    int clientfd;
    struct addrinfo hints, *listp, *p;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; /* Open a connection */
    hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
    hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
    Getaddrinfo(hostname, port, &hints, &listp);

    /* Walk the list for one that we can successfully connect to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
        continue; /* Socket failed, try the next */

        /* Connect to the server */
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
            break; /* Success */
        Close(clientfd); /* Connect failed, try another */
    }

    /* Clean up */
    Freeaddrinfo(listp);
    if (!p) /* All connects failed */
    	return -1;
    else /* The last connect succeeded */
    	return clientfd;
}
  • 서버에 연결
  • 성공 시 소켓 디스크립터 반환

🔹 open_listenfd 함수

#include "csapp.h"
int open_listenfd(char *port); // Returns: descriptor if OK, −1 on error

int open_listenfd(char *port) {
    struct addrinfo hints, *listp, *p;
    int listenfd, optval=1;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; /* Accept connections */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
    Getaddrinfo(NULL, port, &hints, &listp);

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
        	continue; /* Socket failed, try the next */

        /* Eliminates "Address already in use" error from bind */
        Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
        	break; /* Success */
        Close(listenfd); /* Bind failed, try the next */
    }

    /* Clean up */
    Freeaddrinfo(listp);
    if (!p) /* No address worked */
        return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0) {
        Close(listenfd);
        return -1;
    }
    return listenfd;
}
  • 서버용 수신 소켓 생성 및 listen 상태 진입
  • open_clientfdopen_listenfd 둘 다 IP 버전에 독립적이다.

11.4.9 예제 Echo 클라이언트와 서버

🔹 Echo Client: echoclient.c

#include "csapp.h"

int main(int argc, char **argv) {
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;

    if (argc != 3) {
        fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
        exit(0);
    }

    host = argv[1];
    port = argv[2];

    clientfd = Open_clientfd(host, port);           // 서버 연결
    Rio_readinitb(&rio, clientfd);                  // 버퍼 초기화

    while (Fgets(buf, MAXLINE, stdin) != NULL) {    // 사용자 입력 읽기
        Rio_writen(clientfd, buf, strlen(buf));     // 서버로 전송
        Rio_readlineb(&rio, buf, MAXLINE);          // 서버 응답 수신
        Fputs(buf, stdout);                         // 출력
    }

    Close(clientfd);
    exit(0);
}
  • 주요 흐름
    1. 커맨드라인 인자: 호스트와 포트 (예: ./echoclient localhost 1234)
    2. Open_clientfd: 서버와 연결 (내부적으로 getaddrinfo, socket, connect)
    3. Rio_*: robust I/O 사용으로 안정적 입출력
    4. 사용자 입력 → 서버로 전송 → echo 응답 수신 → 출력
    5. EOF (Ctrl+D) 시 루프 종료 및 연결 해제
  • 루프 종료 이후
    1. 클라이언트의 디스크립터 종료: 루프 종료 후 클라이언트가 디스크립터를 닫는다.
    2. EOF 알림 전송: 클라이언트가 디스크립터를 닫으면 서버에게 EOF 알림이 전송된다.
    3. 서버의 EOF 감지: 서버는 rio_readlineb 함수가 0을 반환할 때 EOF를 감지한다.
    4. 클라이언트 종료: 디스크립터를 닫은 후 클라이언트는 종료된다.
    5. 커널의 자동 종료: 클라이언트 커널은 프로세스 종료 시 모든 열린 디스크립터를 자동으로 닫는다.
  • 명시적 close의 필요성: 커널의 자동 종료 기능 때문에 라인 24의 close가 필수는 아니지만, 열었던 디스크립터를 명시적으로 닫는 것이 좋다.

🔹 Echo Server: echoserveri.c

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;  // 클라이언트 주소 저장

    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(0);
    }

    listenfd = Open_listenfd(argv[1]);  // 수신 소켓 생성
    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        echo(connfd);                   // 클라이언트 처리
        Close(connfd);                 // 연결 종료
    }
}

void echo(int connfd) {
    size_t n;
    char buf[MAXLINE];
    rio_t rio;

    Rio_readinitb(&rio, connfd);              // 버퍼 초기화
    while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
        Rio_writen(connfd, buf, n);           // 입력된 줄을 그대로 반환
    }
}
  • 서버는 EOF(end-of-file) 상태가 될 때까지 텍스트 라인을 읽고 전송하기 위해 RIO(robust I/O)를 사용한다.
  • 주요 흐름
    1. 각 반복에서 클라이언트의 연결 요청을 기다린다.
    2. 연결된 클라이언트의 도메인 이름과 포트를 출력한다.
    3. 클라이언트를 서비스하는 echo 함수를 호출한다.
    4. echo 루틴이 반환되면 연결된 디스크립터를 닫는다.
  • clientaddr 변수 (9번 줄)
    • accept 함수에 전달되는 소켓 주소 구조체
    • accept가 반환되기 전에 연결 상대편 클라이언트의 소켓 주소로 채워진다.
  • sockaddr_storage 사용 이유
    • struct sockaddr_in 대신 struct sockaddr_storage 타입으로 선언된다.
    • sockaddr_storage 구조체는 정의상 어떤 타입의 소켓 주소라도 저장할 수 있을 만큼 충분히 커서, 코드를 프로토콜 독립적으로 유지할 수 있다.
함수 설명
Open_clientfd 클라이언트 소켓 생성 및 서버 연결
Open_listenfd 서버 수신용 소켓 생성
Accept 클라이언트 연결 수락
Rio_* 버퍼드 I/O: short count, EOF, signal-safe

 

항목 설명
단순 구조 iterative 방식 → 한 번에 한 클라이언트만 처리
프로토콜 독립 getaddrinfo 사용 → IPv4/IPv6 모두 지원
루프 내 연결 처리 accept → echo → close 순서로 직관적 흐름
에러처리와 리소스 해제 실패 시 close 호출, freeaddrinfo 사용 등 메모리/리소스 누수 방지

echo 프로그램 요약