13 minute read

Computer Systems : A Programmer’s Perspective (CSAPP)



11.1 The Client-Server Programming Model

모든 네트워크 응용 프로그램은 client-server model 에 기반한다. 이 모델에서 응용 프로그램은 한 개의 서버 프로세스와 여러 개의 클라이언트 프로세스로 구성된다. 서버는 리소스를 잘 관리해서 클라이언트에게 서비스를 제공한다. 예를 들어 FTP 서버는 클라이언트를 위해 디스크 파일들을 저장하고 검색한다.

클라이언트-서버 모델의 가장 기본적인 동작은 transaction 이다. image

다음 4개 단계로 구성된다.

  1. 클라이언트는 서비스가 필요할 때 서버에게 request를 보내서 transaction을 개시한다. 예를 들면 웹 브라우저가 파일이 필요하면 웹 서버에게 요청을 보낸다.
  2. 서버가 요청을 받으면 그걸 해석한 후 적절하게 리소스를 처리한다. 예를 들면 웹 서버는 브라우저의 요청을 받고 저장하고 있는 디스크 파일을 읽는다.
  3. 서버는 클라이언트에게 response를 보내고 다음 request를 기다린다.
  4. 마지막으로, response를 받은 클라이언트는 그걸 잘 처리한다. 예를 들면 웹 브라우저는 서버의 응답을 받아서 웹 페이지로 만들어 화면에 띄운다.


여기서 말하는 클라이언트와 서버는 프로세스이지, 기계나 흔히 말하는 호스트가 아니라는 사실에 주목해야 한다. 하나의 호스트는 여러 개의 클라이언트와 서버를 동시에 실행시킬 수 있고, 클라이언트와 서버 간의 transaction도 같은 host 내에서 이뤄질 수도 있고 다른 host 간에 일어날 수도 있다.

11.2 Networks

클라이언트와 서버는 주로 별개의 호스트에서 실행되어 computer network 라고 불리는 하드웨어/소프트웨어 리소스를 이용해 통신한다. 네트워크는 무척 복잡한 시스템으로, 여기에서는 개략적으로만 다룬다다. 현재의 목표는 프로그래머의 관점에서 적당한 멘탈 모델(심성 모델)만 제공하는 것이다.

호스트 입장에서 네트워크는 데이터의 source와 sink의 역할을 하는 그냥 또 하나의 입출력 장치이다. image

위 그림을 보면, 기존의 I/O bus에 추가적으로 달린 슬롯이 있고 그곳을 통해 네트워크와의 물리적인 인터페이스가 제공된다. 네트웨크에서 들어온 데이터는 어댑터에서 복사되어 I/O bus와 memory bus를 타고 전달된다. 반대 방향도 비슷하게 메모리에서 네트워크로 데이터가 복사될 수 있다.

물리적으로, 네트워크는 지리적 근접성으로 구성된 계층적인 시스템이다. 가장 낮은 레벨은 LAN으로, LAN의 범위는 한 건물 혹은 한 캠퍼스 정도이다. 가장 보편적인 LAN 기술은 1970년대에 개발된 우리에게 익숙한 Ethernet이다.
Ethernet segment는 하나의 허브와 회선(주로 twisted pair)로 구성되어 있다. 한 이더넷 세그먼트는 건물의 한 층이나 하나의 방 정도를 커버한다. 각 회선은 동일한 최대 비트 대역폭(주로 100Mb/s나 1Gb/s)를 갖는다. 한쪽 끝은 호스트의 어댑터에, 반대쪽 끝은 허브에 있는 port에 연결되어 있다. 허브는 각 포트를 통해 들어온 비트들을 모든 다른 포트로 단순하게 복사한다. 즉, 모든 호스트들은 허브를 거치는 모든 비트를 볼 수 있다.

각 이더넷 어댑터들은 각자의 비휘발성 메모리에 저장된 고유의 48비트 주소가 있다. 호스트가 같은 구역의 다른 호스트에게 보내는 비트는 frame이라고 하는데, 각 프레임은 header 라고 부르는 고정된 길이의 비트가 있다. 프레임 헤더에는 프레임의 src외 dst를 구분할 수 있는 정보와 프레임의 길이가 담겨 있고, 헤더 다음에 프레임의 진짜 내용(payload)가 붙어 있다. 각 호스트의 어댑터는 프레임을 볼 수 있지만 실제로 해당 프레임을 읽는 것은 종착지(dst)의 호스트가 된다.

여러 개의 이더넷 구역(segment)들은 더 커다란 LAN으로 연결될 수 있고 이를 bridged Ethernet이라고 한다. 다음 그림과 같이 bridge라고 불리는 작은 박스와 여러 회선들을 사용해서 이를 만든다. image

Bridged LAN은 이제 건물 전체나 캠퍼스 전체를 커버할 수 있다. 브릿지 간에 연결된 회선도 있고 브릿지와 허브 간에 연결된 회선도 있는데 회선들의 대역폭은 각각 다를 수 있다. 위 그림에서는 브릿지 간 회선은 1Gb/s의 대역폭을 가지고 4개의 허브-브릿지 회선은 100Mb/s의 대역폭을 갖는다.

브릿지는 회선의 대역폭을 허브보다 더 효율적으로 사용한다. 알고리즘을 통해서 어떤 포트로 보내야 어떤 호스트에게 닿을 수 있는지를 시간이 지나며 자동으로 배워 가고, 포트 간에 프레임을 복사할 때 딱 필요한 것만 선별적으로 복사한다. 예를 들어 위 그림의 호스트 A가 B에게 프레임을 보내면 이 둘은 같은 구역에 있기 때문에 브릿지 X는 이 프레임을 봐도 그 너머로 복사하지 않고 버려서 다른 구역이 대역폭을 절약할 수 있게 한다. 만약 호스트 A가 C에게 프레임을 보낸다면, 브릿지 X는 이번에는 프레임을 복사해서 Y에게 전달하게 되고 Y는 C가 있는 구역으로 가는 포트에만 프레임을 복사할 것이다.

이제 더 높은 곳에서 살펴보기 위해 LAN을 간략화한다. 이제 허브, 브릿지, 그리고 회선들은 뭉뚱그려서 아래 그림과 같은 수평의 선으로 표시한다. image

계층 구조에서 한 칸 위로 올라간다.
여러 개의 호환되지 않는 LAN들은 router라고 불리는 특별한 컴퓨터를 통해 상호 연결되어서 인터넷(internet, 고유명사 아님)을 구성한다. 각 라우터는 연결된 각 네트워크를 위한 어댑터(포트)가 있다.
라우터는 고속의 point-to-point 전화선들을 연결하기도 하는데, 이는 WAN이라고 부르는 LAN보다 광범위한 영역에 걸쳐 있는 네트워크의 예시 중 하나이다.
일반적으로 라우터는 임의의 LAN들과 WAN들 간의 internet(역시 고유명사 아님)을 구축하는데 사용된다. 아래 그림에서는 두 개의 LAN과 WAN들이 3개의 라우터를 통해 연결되어 있다. image

인터넷의 가장 중요한 특성은, 근본적으로 다르고 호환되지 않는 기술을 쓰는 여러 LAN과 WAN들로 구성된다는 것이다. 호스트가 물리적으로 다른 호스트와 연결되어 있다고 해도 어떻게 source host에서 destination host까지 이 모든 호환 불가능한 네트워크들을 거쳐서 비트를 주고받을 수 있을까?

해답은 각 호스트와 라우터에서 네트워크 간의 차이를 없애기 위해 실행하는 protocol software 계층이다. 이 소프트웨어는 호스트와 라우터가 데이터를 잘 전송하기 위해 서로 어떻게 협동해야 하는지를 관장하는 프로토콜을 실행한다. 프로토콜은 두가지 기본적인 기능이 있다.

  • Naming Schemes
    서로 다른 LAN 기술들은 호스트에게 주소를 부여하는 각자의 방법을 가지고 있고, 이는 다른 네트워크와 호환되지 않는다. 따라서 인터넷 프로토콜은 이런 차이를 없애기 위해 호스트 주소를 고유의 포맷으로 정의한다. 각 호스트는 고유하게 식별될 수 있는 internet address를 적어도 한 개 할당받게 된다.
  • Delivery mechanism
    서로 다른 네트워킹 기술들은 회선을 따라 흐르는 비트를 인코딩하는 방법도 서로 다르고 비트를 프레임으로 포장하는 방식도 달라서 서로 호환되지 않는다. 인터넷 프로토콜은 이 차이를 없애기 위해서 데이터의 비트들을 모아서 덩어리로 만들 수 있는 packet이라는 일관된 방식을 정의한다. 패킷은 출발/도착 호스트와 패킷의 크기 정보들 담은 헤더와 실제 데이터 비트인 payload로 구성된다.


아래 그림은 호스트들과 라우터들이 호환되지 않는 LAN 간에 데이터를 주고받기 위해 인터넷 프로토콜을 어떻게 사용하는지를 보여준다. image

  1. 호스트 A의 클라이언트(=프로세스)는 가상 메모리 공간에서 커널의 버퍼로 데이터를 복사하는 시스템 콜을 요청한다.
  2. 호스트 A의 프로토콜 소프트웨어는 인터넷 헤더와 LAN1식 프레임 헤더를 붙여서 LAN1 프레임을 만든다. 인터넷 헤더에는 전체 네트워크에서 종착지인 호스트 B의 인터넷 주소가 담겨 있고, LAN1 프레임 헤더에는 LAN1 내에서의 종착지인 라우터(그림의 가운데)로의 주소가 담겨 있다. 이렇게 만들어진 프레임은 호스트 A의 LAN1 어댑터로 전달된다. 이때 전달되는 데이터는 두 단계로 encapsulation되어 있고 이것이 internetworking의 근본적인 개념이 된다.
  3. 호스트 A의 LAN1 어댑터는 프레임을 네트워크에 복사한다.
  4. LAN1 프레임 헤더의 정보를 이용해서 무사히 라우터에 도달하면, 라우터의 LAN1 어댑터는 이를 읽어온 후 프로토콜 소프트웨어에게 전달한다.
  5. 라우터는 인터넷 패킷에서 도착지의 인터넷 주소를 얻고, 이것을 routing table의 인덱스로 사용해서 어느 쪽으로 패킷을 forwarding해야 할지 결정한다. 이 경우에는 LAN2 쪽으로 보내는 것으로 결정하게 되어 LAN1의 프레임 헤더를 떼고 LAN2의 프레임 헤더를 맨 앞에 붙여 LAN2 어댑터에게 전달한다. 인터넷 헤더에는 그대로 호스트 B의 주소가 담겨 있다.
  6. 라우터의 LAN2 어댑터는 프레임을 네트워크에 복사한다.
  7. 호스트 B에 도달하면 호스트 B의 LAN2 어댑터가 프레임을 읽고 프로토콜 소프트웨어에게 전달한다.
  8. 마지막으로, 호스트 B의 프로토콜 소프트웨어는 패킷 헤더와 프레임 헤더를 제거하고 최종적으로 데이터(payload)를 B의 가상 주소공간에 복사한다. 그리고 서버는 이 데이터를 읽기 위한 시스템 콜을 요청한다.

이 과정 중에는 수많은 문제들이 있지만 우리는 그것들을 숨기고 있다. 서로 다른 네트워크의 최대 프레임 크기가 다르면 어떻게 할 것인지? 라우터는 프레임을 어디로 forwarding해야 할지 어떻게 아는지? 라우터는 네트워크의 topology가 바뀌면 이걸 어떻게 통보받는지? 패킷이 손실되면 어떻게 하는지? 등등의 문제가 있다.

하지만 위의 예시는 단순히 인터넷이란 것의 근본적인 아이디어만 보여준 것이고, encapsulation의 개념만 알아가면 된다.

11.3 The Global IP Internet

internet을 가장 성공적으로, 그리고 널리 실현한 것이 바로 global IP Internet이다. 이제부터 인터넷(Internet)은 고유명사이다.
1960년대부터 존재하기 시작했고, 내부 구조는 무척 복잡하고 계속 변해왔지만 1980년대 초부터 client-server application 구조는 쭉 안정적으로 남아 있다.
아래 그림은 인터넷의 client-server application의 하드웨어, 소프트웨어 조직을 간단하게 나타낸 그림이다.

image

각 호스트는 TCP/IP 프로토콜을 수행하는 소프트웨어를 실행하는데, TCP/IP 프로토콜은 대부분의 현대 컴퓨터 시스템에서 지원하는 프로토콜이다. 클라이언트와 서버는 소켓 인터페이스 함수와 유닉스 I/O 함수를 이용해 통신한다. 각 소켓 함수들은 커널로 trap하는 시스템 콜을 부르고, TCP/IP의 여러 커널 모드 함수들을 부르게 된다.

TCP/IP는 각자의 기능이 있는 여러 protocol의 집합이다.
예를 들면 IP는 기본적인 naming scheme과 호스트 간 datagram의 전달 매커니즘을 제공한다. 하지만 IP 매커니즘만 가지고는 reliable한 통신을 할 수 없는데, 네트워크에서 데이터그램이 소실되거나 중복되었을 때 복구를 해 주지 않기 때문이다.
UDP는 IP에서 약간 확장되어서, 데이터그램이 호스트에서 호스트로 전달되지 않고 프로세스에서 프로세스로 전달된다. TCP는 IP를 기반으로, 프로세스 간 reliable full-duplex 연결을 제공하는 복잡한 프로토콜이다.
우리는 여기서 논의를 단순화하기 위해서 TCP/IP를 단일 프로토콜이라고 생각하고 내부적인 동작은 다루지 않는다. TCP와 IP가 응용 프로그램에게 제공하는 몇가지 기본적인 기능에 대해서만 다룰 것이고, UDP는 다루지 않는다.

프로그래머의 관점에서, 인터넷은 다음 성질을 만족하는 전세계 호스트들의 집합으로 볼 수 있다.

  • 각 호스트는 32비트 IP 주소와 대응된다.
  • 각 IP 주소는 Internet domain name이라는 identifier과 대응된다.
  • 하나의 인터넷 호스트의 프로세스는 연결된 다른 모든 인터넷 호스트와 통신할 수 있다.

11.3.1 IP Addresses

IP 주소는 unsigned 32-bit integer이다. 네트워크 프로그램은 아래와 같은 IP address structure에 IP 주소를 저장한다.

struct in_addr {
    uint32_t s_addr; /* Address in network byte order (big-endian)
};

인터넷 호스트들은 각자 byte order가 다르기 때문에 TCP/IP는 모든 정수 데이터에 적용되는 network byte order(Big-endian)을 정의한다. IP 주소 구조체의 주소들은호스트가 little endian을 사용하더라도 항상 network byte order로 저장된다. Unix는 둘 간의 변환을 위한 함수도 제공한다.

#include <arpa/inet.h>
    /* Returns: value in network byte order */
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    
    /* Returns: value on host byte order */
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(unit16_t netshort);

htonl 함수는 unsigned 32-bit integer을 Little-endian에서 Big-endian으로 변환해 주고, ntohl 함수는 반대의 변환을 해 준다. htonsntohs는 unsigned 16-bit integer을 변환해 준다.

사람들에게 친숙한 IP 주소는 dotted-decimal notation 이라고 알려진 형태이다. 예를 들어 182.2.194.242는 주소 0x8002c2f2를 dotted-decimal notation으로 나타낸 것이다. 리눅스 스스템에서 터미널에 hostname -i 를 치면 자신의 호스트 IP 주소를 볼 수 있다.

image

응용 프로그램은 아래 함수를 이용해서 IP주소의 형태를 정수와 dotted-decimal string 간 변환할 수 있다.

#include <arpa/inet.h>
    /* Returns: 1 if OK, 0 if src is invalid dotted decimal, −1 on error */
    int inet_pton(AF_INET, const char *src, void *dst);
    
    /* Returns: pointer to a dotted-decimal string if OK, NULL on error */
    const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);

함수 이름에서 n는 network를 의미하고, p는 presentation을 의미한다. pton이면 dotted-decimal string에서 정수로 바꾸는 것이고, ntop는 그 반대가 된다. 이 함수는 32 비트 IP주소인 IPv4와 128비트 IP주소인 IPv6 모두 다룰 수 있다.
inet_pton에서 만약 src로 들어온 dotted-decimal string의 형식이 맞지 않는다면 0을 반환하고, 다른 에러는 -1을 반환하고 errno의 값을 설정한다.

11.3.2 Internet Domain Names

인터넷 클라이언트와 서버는 서로 통신할 때 IP 주소를 사용한다. 하지만 32비트짜리 거대한 정수를 사람들이 외우기는 힘들기 때문에 인터넷은 사람 친화적인 별도의 domain name 을 정의했고, IP 주소와 도메인 이름를 대응시키는 매커니즘도 정의했다.
도메인 이름은 온점으로 구분된 단어(문자, 숫자, ‘-‘)들로 되어 있다. my.snu.ac.kr 과 같이 우리에게 무척 익숙하다.

도메인 이름들은 계층 구조로 되어 있고, 각 도메인 이름은 계층 구조 내에서의 자신의 위치를 인코딩한 것이다. 아래 그림과 함께 살펴본다.

image

Subtree는 subdomain이라고 불리고, 전체 트리의 root는 unnamed root아고 부른다.
트리 구조에서 각 노드는 각 도메인 이름을 나타내고, root까지 되돌아 가는 일에 있는 이름들을 모아 구성할 수 있다. Depth 1에 있는 노드들은 first-level domain name이라고 부르고, 비영리단체 ICANN이 정한 것이다. First-level domain에는 .com, .edu, .gov, .org, .net 등이 있다.

인터넷은 도메인 이름과 IP 주소 간의 매핑을 정의하고 있다. 1988년까지는 하나의 텍스트 파일에 수동으로 저장하고 관리했지만 이제는 DNS(Domain Name System) 라고 불리는 전세계의 분산 데이터베이스에 저장되고 관리된다. 개념적으로 DNS 데이터베이스는 수백만 개의 host entry 들을 가지고 있어서 각 entry마다 domain name에 대응하는 IP 주소가 들어 있다.

각 인터넷 호스트는 localhost 라는 국지적으로 정의된 도메인 이름을 가지고, 항상 loopback address 127.0.0.1 로 대응된다. localhost를 사용하면 같은 장치에서 클라이언트와 서버를 모두 돌려볼 수 있고, 이는 특히 디버깅에서 좋다. 아래와 같이 nslookup localhost를 입력하면 localhost의 IP 주소를 볼 수 있고, hostname 명령어를 쓰면 실제 도메인 이름을 볼 수 있다.

image

11.3.3 Internet Connections

클라이언트와 서버는 연결(connection)을 통해 byte stream을 주고받는다. Connection은 프로세스와 프로세스를 연결하는 점대점 연결로, 동시에 데이터가 양방향으로 이동할 수 있는 full-duplex이다. 또한 reliable한데, 이는 굴삭기가 통신선을 끊어버린다던가 하는 대형 참사가 아니라면 byte stream은 보낸 순서대로 도착점에 도착한다는 의미이다.

소켓은 연결의 말단이다. 각 소켓은 socket address가 있는데, 이는 인터넷 주소와 16비트짜리 정수인 port로 구성된다. 즉 address::port 의 형태이다.

클라이언트 소켓 주소의 포트는 클라이언트가 연결 요청을 할 때 커널에 의해 자동으로 할당되고, 이를 ephemeral port라고 부른다. 반대로 서버 소켓의 port는 보통 제공하는 서비스와 영구적으로 연관되어 있는 well-known 번호를 사용한다. 예를 들면 웹 서버는 포트 80번을 사용하고, 이메일 서버는 포트 25번을 사용한다. 이처럼 well-known 포트와 연관된 서비스들을 well-known service name이라고 한다. Web 서비스의 well-known name은 http이고, 이메일 서비스의 well-known name은 smtp이다. Well-known name과 well-known port들은 /etc/services라는 파일에 들어 있다.

Connection은 양쪽 말단의 소켓 주소로 유일하게 식별된다. 소켓 주소의 쌍을 socket pair라고 부르고, (cliaddr::cliport, servaddr::servport)와 같다.

image

아래 그림에서 클라이언트 소켓의 포트 번호 51213은 ephemeral port이고, 서버 소켓 포트 번호 80은 웹 서버의 well-known port이다.

11.4 The Socket Interfaces

소켓 인터페이스는 네트워크 응용 프로그램을 만들기 위해 유닉스 I/O 함수와 함께 사용되는 함수들이다. 윈도우나 매킨토시 같은 유닉스 기반의 시스템을 포함해서 대부분의 현대 시스템에서 실행되고 있다. 아래 그림은 client-server transaction의 맥락에서 본 소켓 인터페이스를 나타낸 그림이다. 이 그림을 따라가며 개별 함수를 살펴볼 것이다.

image

리눅스 커널의 관점에서 보면 소켓은 통신의 말단이다. 그리고 리눅스 프로그램 관점에서 보면 소켓은 file descriptor를 가지는 open file이다.

인터넷 소켓 주소는 16바이트의 크기를 갖는 sockaddr_in 구조체에 저장된다.

/* 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 */
};

인터넷 응용 프로그램에게 sin_family가 AF_INET(매크로, IPv4라는 뜻)이고 sin_port는 16비트의 포트 번호, in_addr은 32비트 IP 주소이다. IP 주소와 포트 번호는 항상 Big-endian으로 저장되어 있다.

소켓 인터페이스의 함수들(connect, bind, accept 등)에서 프로토콜마다 위 구조체의 포인터를 필요로 했지만, 어떤 종류의 구조체도 받을 수 있도록 함수를 정의해야 하는 문제에 직면했다. 지금이야 void * 같은 generic pointer로 할 수 있지만 그 당시에는 C에 void *가 없었다.
위의 sockaddr_in 구조체는 IP를 기반으로 하는 우리의 특별한 Internet을 위한 구조체이기 때문에, 다른 인터넷(internet) 시스템에서는 다른 구조체를 사용할 수도 있다. 그래서 모두가 사용할 수 있는 sockaddr이라는 generic한 구조체를 만들었다. connect, bind, accept는 모두 struct sockaddr *을 받으므로, struct sockaddr_in *을 항상 struct sockaddr *로 casting 해서 사용해야 한다.

11.4.2 The socket Function

서버와 클라이언트는 socket 함수를 이용해서 socket descriptor를 만든다.

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

int socket(int domain, int type, int protocol);

소켓이 connection의 말단이 되게 하고자 한다면 socket(AF_INET, SOCK_STREAM, 0)로 하면 된다.
첫 번째 파라미터 domain에서는 어떤 종류의 프로토콜을 사용중인지를 선택한다. 우리는 32비트 IP 주소를 쓰는 IPv4를 쓰므로 AF_INET을 입력한다. man socket을 해서 보면 20개가 넘는 protocol family들이 있는 것도 구경할 수 있다.
두 번째 파라미터 type은 통신의 semantic을 결정한다. SOCK_STREAM의 설명은 다음과 같이 되어 있다.

Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmis‐sion mechanism may be supported.

세 번째 파라미터는 protocol family 내에서 어떤 프로토콜을 쓸 건지 지정하는 것으로 보이는데, 우리가 사용하는 TCP/IP에서는 그냥 0으로 하면 되는 것으로 보인다.

반환값은 clientfd descriptor이다. 하지만 아직 이 소켓은 완전히 열리지 않았고 읽기/쓰기도 아직 할 수 없다. 완전히 여는 것은 우리가 클라이언트냐, 서버냐에 따라 조금 과정이 달라진다.

11.4.3 The connect Function

클라이언트는 socket으로 file descriptor를 받아낸 후, connect 함수를 이용해서 서버와 connection을 establish한다. (좋은 우리말 추천 받음)

#include <sys/socket.h>

int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);

이 함수를 실행하면 파라미터 addr에 해당하는 서버와 Internet connection을 establish하려고 시도한다. 이 함수는 성공적으로 establish되거나 에러가 발생할 때까지 block하게 되고, 성공했다면 이제 clientfd descriptor는 읽고 쓸 준비가 완료된다.
완성된 connection은 socket pair을 가지게 되고, (x:y, addr.sin_addr:addr.sin_port) 꼴이 된다. x는 클라이언트의 IP 주소, y는 서버로부터 부여받은 ephemeral 포트 번호이다. 이 포트 번호로 클라이언트 호스트에서 해당 프로세스를 고유하게 식별할 수 있다.

11.4.4 The bind Function

지금부터 3개의 함수(bind, listen, accept)는 서버가 클라이언트와 connection을 establish하기 위해서 사용한다.

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

socket의 반환값으로 받은 listenfd를 sockfd 파라미터로 넣어 준다.
함수를 실행하면 addr 내의 서버의 소켓 주소와 sockfd를 연결하도록 커널에게 요청하게 된다.

11.4.5 The listen Function

#include <sys/socket.h>

int listen(int sockfd, int backlog);

클라이언트는 능동적으로 연결 요청을 개시하고, 서버는 수동적으로 클라이언트의 연결 요청을 기다린다. 기본적으로, 커널은 socket 함수로 생성된 descriptor는 클라이언트 쪽 말단에서 active socket일 것이라고 가정한다. listen 함수를 실행하면, 서버는 커널에게 해당 descriptor를 클라이언트 대신 서버가 사용하겠다고 전달한다.

파라미터로 들어간 sockfd를 active socket에서 listening socket으로 전환한다. Listening socket이 된 이 descriptor는 이제 클라이언트들로부터 connection request를 받아서 accept를 하는 데 사용될 것이다.

공식 문서의 설명은 다음과 같다.

listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).

backlog 파라미터는 연결 요청의 대기 큐의 크기를 설정하는 것인데, TCP/IP를 더 자세히 알아야 해서 여기서는 다루지 않는다.

11.4.6 Accept Function

listen을 통해 listening descriptor에 해당하는 소켓을 하나 만들었으니 이제 클라이언트로부터 접속 요청이 올 때까지 대기한다.
요청이 왔으면, 클라이언트가 보내온 struct sockaddr *addr에 클라이언트의 포트 번호를 부여해서 socket address를 채워 준다. 그리고 connected descriptor를 반환해서 클라이언트로 하여금 앞으로 유닉스 I/O 함수들을 이용해서 이곳을 통해 통신하도록 한다.

Listening descriptor과 connected descriptor의 차이는 다음과 같다.
먼저 수명 주기에 차이가 있다. listening descriptor는 서버의 수명 주기와 동일하고, 한번 생기면 서버가 완전 끝날때까지 쭉 가는 반면 connected descriptor는 클라이언트와 connection을 할 때마다 죽었다가 다시 살아난다. 또한 listening descriptor는 connection request 전용 말단이고, connected descriptor는 서버와 클라이언트 간에 established된 connection의 말단이 된다.

아래 그림은 accept의 과정을 보여준다.

image

1번은 클라이언트는 socket, 서버는 socketbind, listen까지 끝낸 상황에서 서버가 accept를 실행한 시점이다. 서버는 3번 descriptor를 사용하는 listenfd 소켓을 listening socket으로 해 놓고 클라이언트가 connection을 요청하기를 기다리고 있다.
2번에서 client는 clientfd 소켓을 통해 connection request를 보냈다. 그리고 3번에서 서버의 connfd와 클라이언트의 clientfd 간에 connection이 establish되었다.

11.4.7 Host and Service Conversion

Tags:

Categories:

Updated: