11. 네트워크 프로그래밍
- 모든 네트워크 응용들은 동일한 기본적인 프로그래밍 모델에 기초하고 있으며, 이들은 비슷한 전체 논리구조를 가지고 동일한 인터페이스를 사용한다.
- 네트워크 응용은 시스템에 대한 많은 개념들에 의존하고 있다.
ex) 프로세스, 시그널, 바이트 순서, 메모리 매핑, 동적 저장장치 할당은 모두 중요한 역할을 한다.
11.1 클라이언트- 서버 프로그래밍 모델
- 모든 네트워크 응용프로그램은 클라이언트 - 서버 모델에 기초하고 있다. 이 모델을 사용해서 응용은 한 개의 서버 프로세스와 한개 이상의 클라이언트 프로세스로 구성된다. 서버는 일부 리스소를 관리하고, 이 리소스를 조작해서 클라이언트를 위한 일부 서비스를 제공한다.
- 예를들어
- 웹 서버는 디스크 파일들을 관리하고, 클라이언트를 대신해서 이들을 관리하고 실행한다.
- FTP서버는 클라이언트를 위해 저장하고 읽어오는 디스크 파일들을 관리한다.
- 이메일 서버는 클라이언트를 위해서 읽고 갱신하는 스풀 파일을 관리한다.
- 클라이언트 - 서버 모델에서 근복적인 연산은 트랜잭션이다. 클라이언트 - 서버 트랜잭션은 네 단계로 구성된다.
- 클라이언트가 서비스를 필요로 할 때, 클라이언트는 한 개의 요청을 서버에 보내는 것으로 트랜잭션을 개시한다. ex) 웹 브라우저가 파일을 필요로 할 때, 웹 서버로 요청을 보낸다.
- 서버는 요청을 받고, 해석하고, 자신의 자원들을 적절한 방법으로 조작한다. ex) 웹 서버가 브라우저로 부터 요청을 받을 때, 디스크 파일을 읽는다.
- 서버는 응답을 클라이언트로 보내고, 그 후에 다음 요청을 기다린다. ex) 웹 서버는 이 파일을 다시 클라이언트로 돌려보낸다.
- 클라이언트는 응답을 받고 이것을 처리한다. ex) 웹 브라우저가 서버로부터 페이지를 한 개 받은 후, 이것을 스크린에 디스플레이한다.
- 클라이언트와 서버는 프로세스이며, 머신이나 호스트가 아니다. 한 개의 호스트는 서로 다른 많은 클라이언트와 서버를 동시에 실행할 수 있으며, 클라이언트와 서버 트랜잭션은 동일하거나 다른 호스트에 존재 할 수 있다. 클라이언트 - 서버 모델은 클라이언트와 서버의 호스트로의 매핑에 관계 없이 동일하다.
11.2 네트워크
- 클라이언트와 서버는 종종 별도의 호스트에서 돌아가며, 컴퓨터 네트워크의 하드웨어 및 소프트웨어 자원을 사용해서 통신한다.
- 호스트에게 네트워크는 단지 또 다른 I/O 디바이스이며, 데이터를 위한 소스와 싱크로 서비스한다. I/O 버스의 확장 슬롯에 꽂혀있는 어댑터는 네트워크에 물리적인 인터페이스를 제공한다. 네트워크에서 수신한 데이터는 I/O와 메모리 버스를 거쳐서 어댑터에서 메모리로, 대개 DMA 전송으로 복사된다. 비슷하게 데이터는 또한 메모리에서 네트워크로 복사 될 수 있다.
- 물리적으로 네트워크는 기하적으로 구성된 계층구조 시스템이다. 하위수준은 LAN(Local Area Network)으로 빌딩이나 캠퍼스에 설치된다. 가장 대중적인 LAN 기술은 현재 이더넷(Ethernet)이다.
- 이더넷 세그먼트는 몇 개의 전선들과 허브라고 부르는 작은 상자로 구성된다. 이더넷 세그먼트는 대개 방이나 빌딩의 층과 같이 작은 지역에 설치한다. 각 전선은 동일한 최대 비트 대역폭을 가지며, 한쪽 끝은 호스트의 어댑터에 나머지 한쪽은 허브의 포트에 연결된다. 허브는 각 포트에서 수신한 비트를 종속적으로 다른 모든 포트로 복사한다. 그래서 모든 호스트는 모든 비트로 볼 수 있다.
- 각 이더넷 어댑터는 어댑터의 비휘발성 메모리에 저장된 전체적으로 고유한 48비트주소를 가진다. 호스트는 프레임이라고 부르는 비트들을 세그먼트의 다른 호스트에 보낼수 있다. 각 프레임은 프레임의 소스와 목적지, 프레임의 길이를 식별할 수 있는 고정된 헤더 비트를 가지고 있으며, 그 뒤에 데이터비트가 이어진다. 모든 호스트 어댑터는 이 프레임을 볼 수 있지만 목적지 호스트 만이 실제로 이것을 읽어들인다.
- 전선들과 브릿지라고 하는 작은 상자들을 사용해서 다수의 이더넷 세그먼트가 연결되어 브릿지형 이더넷이라고 하는 더 큰 LAN을 구성할 수 있다. 브릿지 이더넷은 전체 빌딩이나 캠퍼스 규모로 설치될 수 있다. 브릿지형 이더넷에서 일부 선은 브릿지를 비릿지로 연결하고, 다른 선들은 브릿지를 허브로 연결한다. 각 선의 대역폭은 다를수 있다.
그림추가
- 예를 들어, 만일 호스트 A가 같은 세그먼트에 있는 호스트 B로 한 프레임을 전송하려면 브릿지 X는 이 프레이이 입력포트에 도달할 때 이것을 버리고, 그래서 다른 세그먼트에서의 대역폭을 절약한다. 그렇지만, 만일 호스트 A가 다른 세그먼트의 호스트 C로 프레임을 전송한다면 브릿지 X는 이 프레임을 Y에 연결된 포트로만 복사하고, 이것은 호스트 C의 세그먼트에 연결된 포트로만 이 프레임을 복사하게 된다.
- 브릿지는 허브보다 더 높은 전선의 대역폭을 가진다. 우수한 분산 알고리즘을 사용해서 이들은 자동으로 어떤 호스트가 어떤 포트에서 도달 가능한지 장기간에 걸쳐 학습하고, 그 후 필요한 경우 선택적으로 하나의 포트에서 다른 포트로 프레임을 복사한다.
- 계층구조의 상부에서 다수의 비호환성 LAN들은 라우터라고 부르는 특별한 컴퓨터에 의해서 연결될 수 있으며, 라우터는 네트워크간 연결을 구성한다.(상호연결 네트워크) 각 라우터는 이들이 연결되는 각 네트워크에 대해 어댑터(포트)를 가지고 있다. 라우터는 고속의 point-to-point 전화연결을 할 수 있으며, 이들은 WAN이라고 하는 네트워크의 사례이다. 일반적으로 라우터는 임의의 LAN과 WAN들로부터 internet을 만들기 위해서 사용될 수 있다.
- internet의 중요한 특성은 이것이 매우 다르고 비호환적인 기술을 갖는 여러가지 LAN과 WAN들로 이루어져 있다는점이다. 소스 호스트가 비호환적인 네트워크들을 지나 데이터 비트를 다른 목적지 호스트로 전송할 수 있는 이유는 여러가지 네트워크간의 차이를 줄여주는 각 호스트와 라우터에서 돌고 있는 프로토콜 소프트웨어 계층 때문이다. 이 프로토콜은 두 가지 기본 기능을 제공해야 한다.
- 명명법(Naming Scheme) : 서로 다른 LAN 기술은 주소를 호스트에 할당하는 서로 다른 비호환성을 갖는 방법을 사용한다. internet 프로토콜은 호스트 주소를 위한 통일된 포맷을 정의해서 이 차이점을 줄인다. 각 호스트는 자신을 유일하게 식별하는 internet 주소 최소한 한 개가 할당된다.
- 전달기법(Delivery mechanism) : 서로 다른 네트워킹 기술은 서로 다른 비호환성을 갖는 비트 인코딩 방법과 프레임 내에 이들을 패키징하는 방법을 가지고 있다. internet 프로토콜은 데이터 비트를 패킷이라고 부르는 비연속적인 단위로 묶는 통일된 방법을 정의해서 이 차이점을 줄인다. 패킷은 패킷 크기와 소스 및 목적지 호스트 주소를 포함하는 헤더와 데이터 비트를 포함하는 데이터로 구성된다.
- Internet에서 데이터가 하나의 호스트에서 다른 호스트로 이동하는 방법
- 그림 11.7은 호스트와 라우터가 호환성이 없는 LAN을 통해서 데이터를 전송하기 위해 Internet 프로토콜을 어떻게 사용하는지 관해 보여준다.
- Internet은 하나의 라우터에 연결된 두 개의 LAN으로 구성되며, 호스트 A에서 돌아가는 LAN1에 연결도니 클라이언트는 LAN2에 연결된 호스트 B에서 돌고 있는 서버로 일련의 데이터 바이트를 전송한다.
- 8개의 기본단계
- 호스트의 A의 클라이언트는 클라이언트의 가상 주소공간에서 커널 버퍼로 데이터를 복사하는 시스템 콜을 호출한다.
- 호스트 A의 프로토콜 소프트웨어는 Internet 헤더와 LAN1 프레임 헤더를 데이터에 추가해서 LAN1 프레임을 생성한다. Internet헤더는 Internet 호스트 B로 주소가 지정된다. LAN1 프레임 헤더는 라우터로 주소가 지정된다. 호스트 A는 이 프레임을 어댑터로 전달한다. LAN1 프레임의 데이터가 Internet 패킷이고, 그 데이터는 실제 사용자 데이터라는 점에 유의하라. 이런 종류의 캡슐화(encapsulation)는 Internetwoking의 근본적인 통찰들 중의 하나다.
- LAN1 어댑터는 이 프레임을 네트워크로 복사한다.
- 프레임이 라우터에 도달하면, 라우터의 LAN1 어댑터는 전선에서 이것을 읽어서 프로토콜 소프트웨어로 전달한다.
- 라우터는 Internet 패킷 헤더에서 목적지 Internet 주소를 가져와서 패킷을 전달할 곳을 결정하기 위해, 이 경우에는 LAN2 라우팅 테이블에서의 인덱스로 이것을 사용한다. 이전의 LAN1프레임 헤더를 벗겨내고, 호스트 B의 주소를 갖는 새로운 LAN2 프레임 헤더를 앞에 붙여서 이것을 어댑터로 전달한다.
- 라우터의 LAN2 어댑터는 이 프레임을 네트워크로 복사한다.
- 이 프레임이 호스트 B에 도착하면 어댑터는 이 프레임을 전선에서 읽어들이고, 이것을 프로토콜 소프트웨어로 넘긴다.
- 마지막으로, 호스트 B의 프로토콜 소프트웨어는 패킷 헤더와 프레임 헤더를 벗겨낸다. 프로토콜 소프트웨어는 최종적으로 이 데이터를 서버가 이 데이터를 읽는 시스템콜을 호출할 때 서버의 가상 주소공간으로 복사한다.
11.3 글로벌 IP Internet
- 각 인터넷 호스트는 TCP/IP 프로토콜(transmission control protocol/internet protocol)을 구현한 소프트웨어를 실행하며, 이것은 거의 모든 현대 컴퓨터 시스템에서 지원되고 있다. 인터넷 클라이언트와 서버는 소켓 인터페이스와 Unix I/O 함수들의 혼합을 사용해서 통신한다. 소켓 함수들은 일반적으로 시스템 콜들로 구현되는데, 이 시스템 콜은 커널에서 트랩을 발생시키며, TCP/IP에서 다양한 커널모드 함수들을 호출한다.
- TCP/IP는 실제로 프로토콜의 집합으로 각각 서로 다른 기능을 제공한다.
- IP는 기본 명명법과 데이터그램이라고 하는 패킷을 한 인터넷 호스트에서 다른 호스트로 보낼 수 있는 배달 메커니즘을 제공한다. IP 메커니즘은 만일 데이터그램을 잃어버리거나 네트워크내에서 중복되는 경우에 복구하려고 노력하지 않는다는 점에서 안정적이지 못하다.
- UDP는 IP를 약간 확장하여, 데이터그램이 호스트 간이 아닌 프로세스 간에 전송될 수 있도록 한다.
- TCP는 IP 위에 구현한 복잡한 프로토콜로 프로세스들 간에 완전 양방향 연결을 제공한다.
- 프로그래머 관점에서 인터넷은 다음과 같은 특징을 같는 전 세계적인 호스트의 집합이다.
- 호스트의 집합은 32비트 IP주소 집합에 매핑된다.
- IP주소의 집합은 인터넷 도메인 네임이라고 부르는 식별자의 집합에 매핑된다.
- 하나의 인터넷 호스트의 프로세스는 연결을 통해서 다른 인터넷 호스트의 프로세스와 통신할 수 있다.
11.3-1 IP주소
- IP주소는 비부호형 32비트 정수이다. 네트워크 프로그램은 IP주소를 IP주소 구조체에 저장한다.
- 인터넷 호스트들이 서로 다른 호스트 바이트 순서를 가질 수 있기 때문에 TCP/IP는 네트워크 패킷 헤더에 포함되는 IP주소같은 모든 정수형 데이터 아이템에 대해서 통일된 네트워크 바이트 순서(빅 엔디안 바이트 순서)를 정의한다. IP주소 구조체의 주소는 호스트 바이트 순서가 리틀 엔디안인 경우에도 항상 네트워크 바이트 순서(빅엔디안)로 저장된다.
- IP주소는 대개 사람들에게 dotted-decimal 표기라고 하는 형식으로 제시되며, 이것은 각 바이트가 10진수 값을 사용하고, 다른 바이트들과는 점을 사용해서 구분된다.
- 128.2.194.242 는 0x8002c2f2의 dotted-decimal 표현이다.
- hostname 명령을 사용해서 자신의 호스트에 대한 dotted-decimal 주소를 결정할 수 있다.
linux> hostname -i
128.2.210.xxx
11.3-2 인터넷 도메인 이름
- 인터넷 클라이언트와 서버는 서로 통신할 때 IP주소를 사용한다. 그렇지만 크기가 큰 정수는 사람들이 기억하기 어려워서 인터넷에서는 도메인 이름들의 집합을 IP주소의 집합으로 매핑하는 메커니즘과 함께 사람들에게 친숙한 별도의 도메인 이름 집합을 정의한다.
- 도메인 이름은 단어들의 배열(문자, 숫자, -문자)로 whaleshark.ics.cs.cmu.edu와 같이 점으로 구분한다.
그림11.10추가
- 도메인 이름들의 집합은 계층구조를 형성하고 있으며, 각각의 도메인 이름은 계층구조에서 자신의 위치를 인코드한다. 계층구조는 트리로 나타낸다. 트리의 노드들은 루트로 돌아가는 경로로 형성되는 도메인 이름을 나타낸다. 서브트리는 서브 도메인이라고 한다. 계층구조의 첫 번째 단계는 이름이 없는 루트 노드이다. 다음계층은 ICANN(Internet Coporation for Assigned Names and Numbers)이라고 하는 비영리조직이 정의한 일 단계 도메인 이름의 집합이다.
- 잘 알려진 1단계 도메인 이름에는 com, edu, gov, org, net이 포함된다.
- 다음 계층에는 2단계 도메인 이름인 cmu.edu 포함되며 이들은 수많은 ICANN이 인정하는 대행사가 요청한 순서에 의해 그 이름이 할당된다. 일단 한 조직이 2단계 이름을 받은 후에는 cs.cmu.edu와 같이 서브도메인 내에서는 어떤 새로운 이름도 자유롭게 생성할 수 있다.
- 인터넷의 도메인 이름의 집합과 IP주소 집합 사이에 매핑을 정의한다.1988년 까지 이 매핑은 한 개의 텍스트 파일(HOST.TXT)로 수동 관리 되었다. 그 이후 매핑은 DNS(Domain Name System)라고 하는 전세계에 분산된 데이터 베이스에 의해 관리된다. 개념적으로 DNS 데이터 베이스는 수 백만 개의 호스트 엔트리로 구성되어 있으며, 이들 각각은 도메인 이름의 집합과 IP주소 사이의 매핑을 정의한다. 수학적인 의미에서 각 호스트 엔트리는 도메인 이름과 IP주소들의 동일성(equivalence) 클래스로 생각할 수 있다.
- 리눅스(NSLOOKUP) 프로그램으로 연관된 IP 주소를 표기하는 DNS 매핑의 일부 특성을 조사할 수 있다.
- 각 인터넷 호스트는 지역적으로 정의된 도메인 이름 localhost를 가지고 있으며, 이것은 항상 루프백 주소 127.0.0.1에 매핑된다.
- localhost 이름은 같은 머신에서 돌고있는 클라이언트와 서버들을 참조하는 편리하고 포터블한 방법을 제공하며, 이것은 특히 디버깅시에 유용하다.
- 실제 로컬호스트의 도메인 이름을 알기 위해서는 hostname을 사용할 수 있다.
linux> hostname
whaleshark.ics.cs.cmu.edu
- 가장 간단한 경우, 도메인 이름과 IP주소 사이에는 일대일 매핑이 존재한다. 그러나 일부 경우에, 다수의 도메인 이름이 동일한 IP주소에 매핑된다. 대부분의 일반적인 경우에, 다수의 도메인 이름들은 다수의 IP주소로 매핑될 수 있다. 일부 유효한 도메인 이름들은 어떤 IP주소에도 매핑되어 있지 않다는 것을 발견했다.
11.3-3 인터넷 연결
- 인터넷 클라이언트와 서버는 연결을 통해서 바이트 스크림을 주고 받는 형식으로 통신한다. 이 연결은 두 개의 프로세스를 연결한다는 점에서 point-to-point 연결이다. 데이터가 동시에 양방향으로 흐를 수 있다는 의미에서 이것은 완전양방향(full-duplex)이다. 그리고 소스 프로세스가 보낸 바이트 스크림이 결국의 보낸것과 동일한 순서로 목적지 프로세스에서 수신된다는 의미에서 안정적이다.
- 소켓(Socket)은 연결의 종단점이다. 각 소켓은 인터넷주소와 16비트 정수 포트로 이루어진 소켓 주소를 가지며, 이것은 address : port 로 나타낸다. 클라이언트 소켓 주소 내의 포트는 클라이언트가 연결 요청을 할 때 커널이 자동으로 할당하며, 이것은 단기(ephemeral) 포트라고 한다. 그러나 서버의 소켓 주소에 있는 포트는 대개 영구적으로 이 서비스에 연결되는 잘 알려진 포트다. ex) 웹서버는 80 사용, 이메일 서버는 25 사용.
- 잘 알려진 포트를 갖는 각 서비스에 연관되어 이에 대응되는 잘 알려진 서비스 이름들이 존재한다. ex) 웹서비스에 대해 잘 알려진 이름은 http이고, 이메일에 대해서는 smtp다.
- 잘 알려진 이름과 포트들 간의 매핑은 /etc/services 파일에 보관되어 있다.
- 연결은 두 개의 종단점의 소켓주소에 의해 유일하게 식별된다. 이 두 개의 소켓 주소는 소켓 쌍이라고 알려져 있으며 tuple로 나타낸다.
- (cliaddr : cliport , servaddr : servport) - (클라이언트 주소 : 클라이언트 포트 , 서버 주소 : 서버 포트)
인터넷의 유래 ARPA NET
11.4 소켓 인터페이스
- 소켓 인터페이스는 네트워크 응용을 만들기 위한 Unix I/O 함수들과 함께 사용되는 함수들의 집합이다.
11.4-1 소켓 주소 구조체
-
리눅스 커널의 관점에서 보면, 소켓은 통신을 위한 끝점이다. Unix프로그램의 관점에서 보면 소켓은 해당 식별자를 가지는 열린파일이다.
_in 접미사는 internet에 대한 축약이며, input에 대한 것은 아니다.
11.4-2 socket함수
- 클라이언트와 서버는 소켓 식별자를 생성하기 위해서 socket함수를 사용한다.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- Returns : nonnegative descript if OK, -1 on error
- 만약 소켓을 끝 점으로 만들고 싶으면, 다음과 같이 하드 코드된 인자로 socket함수를 호출하면 된다.
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
여기서 AF_INET은 우리가 32비트 IP주소를 사용하고 있다는 것을 나타내고, SOCK_STREAM은 소켓이 인터넷 연결의 끝점이 될 것이라는 것을 나타낸다.
- 가장 좋은 습관은 이 매개변수들을 자동으로 생성해서 코드가 프로토콜에 무관하게 되도록 getaddrinfo함수를 이용하는 것이다.
- socket에 의해 리턴된 clientfd식별자는 겨우 부분적으로 열린 것이며, 아직은 읽거나 쓸 수 없다.
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
- connect 함수는 소켓 주소 addr의 서버와 인터넷 연결을 시도하며, addrlen은 sizeof(sockaddr_in)이 된다.
- connect함수는 연결이 성공할 때 까지 블록되어 있거나 에러가 발생한다.
- 만일 성공한다면 clientfd 식별자는 이제 읽거나 쓸 준비가 되었으며, 이 연결은 다음과 같은 소켓 쌍으로 규정된다.
(x:y, addr.sin_addr:addr.sin_port)
- 여기서 x는 클라이언트의 IP주소이고, y는 클라이언트 호스트의 클라이언트 프로세스를 유일하게 식별하는 단기 포트다.
- socket에서 가장 좋은 방법은 getaddrinfo를 이용해서 connect의 인자들을 제공하도록 하는 것이다.
11.4-4 bind함수
- 남아 있는 소켓 함수 - bind, listen, accept -는 서버가 클라이언트와 연결을 수립하기 위해 사용한다.
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Returns: 0 if OK, −1 on error
- bind 함수는 커널에게 addr에 있는 서버의 소켓 주소를 소켓 식별자 sockfd와 연결할 것인지 물어본다.
- addrlen인자는 sizeof(sockaddr_int)이다.
- socket과 connect에서와 마찬가지로, 최상의 습관은 getaddrinfo를 이용해서 bind할 인자들을 제공하는 것이다.
11.4-5 listen 함수
- 클라이언트는 연결 요청을 개시하는 능동적 개체이다.
- 서버는 클라이언트로부터의 연결 요청을 기다리는 수동적 개체이다.
- 커널은 socket함수에 의해 생성된 디스크립터가 연결의 클라이언트 측에서 활성화 상태로 유지될 능동 소켓에 해당된다.
- 서버는 listen함수를 호출해서 이 식별자를 클라이언트 대신에 서버가 사용하게 될 것이라고 알려준다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
Returns: 0 if OK, −1 on error
- listen함수는 sockfd를 능동 소켓에서 듣기 소켓으로 변하며, 듣기 소켓은 클라이언트로부터 연결 요청을 승낙할 수 있다.
- backlog 인자는 커널이 요청들을 거절하기 전에 큐에 저장해야 하는 연결의 수에 대한 정보를 제공한다.
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
-
accept함수는 클라이언트로부터의 연결 요청이 듣기 식별자 listenfd에 도달하기를 기다리고, 그 후에 addr 내의 클라이언트 소켓 주소를 채우고, Unix I/O함수들을 이용해서 클라이언트와 통신하기 위해 사용될 수 있는 연결 식별자를 리턴한다.
-
듣기 식별자는 클라이언트 연결 요청에 대해 끝점으로서의 역할을 한다. -> 대개 한 번 생성되며, 서버가 살아있는 동안 계속 존재한다.
-
연결 식별자는 클라이언트와 서버 사이에 성립된 연결의 끝점이다. -> 이것은 서버가 연결 요청을 수락할 때마다 생성되며, 서버가 클라이언트에 서비스하는 동안에만 존재한다.
-
그림 11.14 그림추가는 듣기 식별자와 연결 식별자의 역할을 나타낸다.
- 서버는 accept를 호출하고, 연결 요청이 읽기 식별자에게 도달하기를 기다리며, 명확하게 하기 위해서 이것을 식별자 3이라고 가정한다.
- 식별자 0~2는 표준 파일들을 위해 배정되어 있다는 점을 기억하라
- 클라이언트는 connect 함수를 호출하고, 이것은 listenfd로 연결 요청을 보낸다.
- accept함수는 새로운 연결 식별자 connfd(이것은 식별자 4라고 가정한다.)를 오픈하고, clienfd와 connfd사이의 연결을 수립하고 애플리케이션에 connfd를 리턴한다. 클라이언트는 또한 connect에서 리턴하고, 이 지점에서 클라이언트와 서버는 clientfd와 connfd를 각각 읽고 쓰는 방법으로 데이터를 주고 받을 수 있다.
11.4-7 호스트와 서비스 변환
- 리눅스는 getaddrinfo와 getnameinfo라고 하는 강력한 함수를 제공한다. 이 함수들은 이진 소켓 주소 구조체들과 호스트이름, 호스트주소, 서비스이름, 포트번호들 사이에 앞 뒤로 변환해준다.
- 소켓 인터페이스와 함께 이용될 때, 이들은 우리가 특정 IP 프로토콜의 버전에 의존하지 않는 네트워크 프로그램을 작성하게 해준다.
getaddrinfo 함수
- 호스트이름, 호스트주소, 서비스이름, 포트번호의 스트링 표시를 소켓 주소 구조체로 변환한다.
- 이 함수는 이제 사용하지 않는 gethostbyname과 getservbyname함수들을 현대적으로 교체한 것이다.
- 재진입이 가능하며, 모든 프로토콜에 대해 동작한다.
#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
- host와 service(소켓 주소의 두 개의 구성요소)가 주어지면 getaddrinfo는 각각이 host와 service에 대응되는 소켓 주소 구조체를 가리키는 addrinfo 구조체의 연결리스트를 가리키는 result를 리턴한다.
- 클라이언트가 getaddrinfo를 호출한 후에 이 리스트를 방문하며, 각 소켓 주소를 socket과 connect로의 호출이 성공하고, 연결이 성립할 때까지 차례로 시험한다.
- 서버는 socket과 bind로의 호출이 성공하고, 식별자가 유효한 소켓 주소에 연결될 때까지 이 리스트 상의 각 소켓 주소를 시험한다.
- 메모리 누수를 피하기 위해서 응용 프로그램은 궁극적으로 freeaddrinfo를 호출해서 이 리스트를 반환해야 한다.
- getaddrinfo가 0이 아닌 에러코드를 반환하면, 응용프로그램은 gai_strerror를 호출해서 이 코드를 메시지 스트링으로 변환할 수 있다.
- getaddrinfo로의 host인자는 도메인 이름이거나 숫자 주소일 수 있다.(즉, 점-십진수 IP 주소)
- service인자는 서비스이름 (예 http)이거나 십진수 포트번호일 수 있다.
- 만일 우리가 호스트이름을 주소로 변환하고 싶지 않을 때는 host를 NULL로 설정할 수 있다. service의 경우에도 이것은 동일하다. 그렇지만 최소한 이들 중 한개는 명시되어야한다.
- hints 는 선택적으로 사용할 수 있는 인자이며, 이것은 getaddrinfo 구조체(그림 11.16)다.
그림삽입
- hints 인자로 전달될 때, ai_family, ai_socktype, ai_protocol, ai_flags 필드만이 설정될 수 있다.
- 다른 필드들은 0(또는 NULL)으로 설정되어야한다.
- 실제로 우리는 memset을 이용해서 전체 구조체를 0으로 설정하고, 일부 선택한 필드만을 설정한다.
- 기본적으로 getaddrinfo는 IPv4 IPv6소켓 주소를 리턴한다.
- ai_family를 AF_INET으로 설정하면 리스트를 IPv4주소로 제한, AF_INET6로 설정하면 리스트를 IPv6 주소로 제한한다.
11.5 웹 서버
11.5-1 웹 기초
- 웹 클라이언트와 서버는 HTTP(Hypertext Transfer Protocol) 라고 하는 텍스트 기반 응용 수준 프로토콜을 사용해서 상호연동한다. 웹 클라이언트(브라우저)는 서버로의 인터넷 연결을 오픈하고 컨텐츠를 요청한다. 서버는 요청한 컨텐츠로 응답하고, 그 후에 연결을 닫아준다. 브러우저는 컨텐츠를 읽고 이것은 스크린에 보여준다.
- 웹 컨텐츠는 HTML이라는 언어로 작성되어 FTP같은 파일 전송 시스템과 웹서비스는 구분된다.
11.5-2 웹 컨텐츠
- 웹 클라이언트와 서버에게, 컨텐츠는 연관된 MIME(Multipurpose Internet Mail Extensions) 타입을 갖는 배열이다.
- 웹 서버는 두 가지 서로 다른 방법으로 클라이언트에게 컨텐츠를 제공한다.
- 디스크 파일을 가져와서 그 내용을 클라이언트에게 보낸다. 디스크 파일은 정적 컨텐츠라고 하며, 파일을 클라이언트에게 돌려주는 작업 정적컨텐츠를 처리한다고 말한다.
- 실행 파일을 돌리고 그 출력을 클라이언트에게 보낸다. 실행 파일이 런타임에 만든 출력을 동적 컨텐츠라고 하며, 프로그램을 실행하고 그 결과를 클라이언트에게 보내주는 과정을 동적 컨텐츠를 처리한다고 말한다.
- 웹 서버가 리턴하는 모든 내용들은 서버가 관리하는 파일에 연관된다. 이 파일 각각은 URL(universal presource locator) 라고 하는 고유의 이름을 가진다.
- 어떻게 서버가 URL 접미어를 해석하는지에 대해서 이해해야 할 몇 가지 사실들
- URL이 정적 또는 동적 컨텐츠를 참조하는지를 결정하기 위한 표준 규칙은 없다. 각각의 서버는 자신이 관리하는 파일들을 위한 자신만의 규칙들을 가진다 고전적인(구식의) 접근 방법은 cgi-bin 같은 디렉토리 집합을 지정하는 것이며, 여기에 모든 실행파일들이 들어 있어야 한다.
- 접미어 앞 부분의 ‘/’ 는 리눅스의 루트 디렉토리를 나타내는 것은 아니다. 오히려 이것은 어떤 종류의 컨텐츠가 요청되든 간에 홈 디렉토리를 나타낸다. 예를 들어, 서버는 모든 정적 컨텐츠가 /usr/httpd/html 디렉토리에 저장되고, 모든 동적 컨텐츠는 /usr/httpd/cgi-bin 디렉토리에 저장되도록 구성될 수 있다.
- 최소한의 URL 은 ‘/’ 문자이며, 모든 서버는 이것을 /index.html 같은 특정 기본 홈페이지로 확장한다.
11.5-3 HTTP 트랜잭션
- HTTP가 인터넷 연결 위로 전송된 텍스트 라인들을 기초하고 있기 때문에 리눅스 TELNET 프로그램을 사용해서 인터넷 상의 모든 웹 서버와 트랜잭션을 실행할 수 있다.
- TELNET 프로그램은 연결을 통해서 텍스트 라인으로 클라이언트와 대화하는 서버를 디버그 하는데 매우 간편하다.
- 예를 들어, TELNET을 사용해서 AOL 웹서버의 홈페이지에 요청해보겠다.
1 linux> telnet www.aol.com 80 Client: open connection to server
2 Trying 205.188.146.23... Telnet prints 3 lines to the terminal
3 Connected to aol.com.
4 Escape character is ’^]’.
5 GET / HTTP/1.1 Client: request line
6 Host: www.aol.com Client: required HTTP/1.1 header
7 Client: empty line terminates headers
8 HTTP/1.0 200 OK Server: response line
9 MIME-Version: 1.0 Server: followed by five response headers
10 Date: Mon, 8 Jan 2010 4:59:42 GMT
11 Server: Apache-Coyote/1.1
12 Content-Type: text/html Server: expect HTML in the response body
13 Content-Length: 42092 Server: expect 42,092 bytes in the response body
14 Server: empty line terminates response headers
15 <html> Server: first HTML line in response body
16 ... Server: 766 lines of HTML not shown
17 </html> Server: last HTML line in response body
18 Connection closed by foreign host. Server: closes connection
19 linux> Client: closes connection and terminates
- HTTP 표준은 매 텍스트 라인이 carriage return과 line feed 쌍으로 종료될 것을 요구한다.
HTTP 요청
- HTTP는 많은 서로 다른 메소드를 지원하며 여기에는 GET, POST, OPTIONS … 등 포함되어있다. 이중 GET은 전체 HTTP 요청의 대부분에 해당한다. GET 메소드는 서버에게 URI(uniform resource identifer)에 의해 식별되는 내용을 리턴 할 것을 지시한다. URI는 파일 이름과 옵션인 인자들을 포함하는 URL의 접미어다.
- Host 헤더는 프록시 캐시에 의해 사용되며 ,이것은 때로는 브라우저와 요청된 파일을 관리하는 본래의 서버 사이의 중간자 역할을 한다. 하나의 클라이언트와 하나의 본래 서버 사이에는 소위 프록시 체인내에서 다중 프록시가 존재 할 수 있다. 원점 서버의 도메인 이름을 식별하는 Host 헤더 내의 데이터는 프록시 체인의 중간에 있는 프록시가 요청한 컨텐츠의 지역적으로 캐시된 사본을 가질 수 있는지 결정할 수 있게 한다.
HTTP 응답
- HTTP응답은 HTTP 요청과 비슷하다.
HTTP/1.0 200 OK ->version status-code status-message 버전 필드는 응답이 준수해야할 HTTP 버전을 설명한다. 상태코드는 3비트 양수로, 요청의 특성을 나타낸다. 상태메세지는 에러코드를 영어로 나타낸것이다.
| Status code | Status message | Description |
|---|
| 200 | OK | Request was handled without error. |
| 301 | Moved permanently | Content has moved to the hostname in the Location header. |
| 400 | Bad request | Request could not be understood by the server. |
| 403 | Forbidden | Server lacks permission to access the requested file. |
| 404 | Not found | Server could not find the requested file. |
| 501 | Not implemented | Server does not support the request method. |
| 505 | HTTP version not supported | Server does not support version in request. |
- 중요한 헤더는 클라이언트에게 응답 본체 내의 컨텐츠의 MIME 타입을 알려주는 Content-Type(12번 줄)과 그 크기를 나타낸 Content-Length(13번 줄)다.
- 14번 줄의 응답 헤더를 종료하는 비어있는 텍스트 라인 이후 응답 본체가 따라오며, 본체는 요청한 컨텐트를 포함한다.
11.5-4 동적 컨텐츠의 처리
11.6 종합 설계 : 소형 웹 서버
- TINY라고 부르는 작지만 동작하는 웹 서버를 개발하는 것으로 마무리한다.
- TINY Server는 프로세스 제어, Unix I/O, 소켓 인터페이스, HTTP 같은 많은 개념을 결합한 것으로 250 라인 밖에 되지 않는다.
- 실제 서버가 가지는 기능성, 견고성, 보안성은 없다. 하지만 TINY Server는 충분히 강력해서 실제 웹 브라우저에 정적 및 동적 컨텐츠를 모두 제공할 수 있다.
TINY main 루틴
- TINY는 반복 실행 서버로 명령줄에서 넘겨받은 포트로의 연결 요청을 듣는다.
- open_listenfd 함수를 호출해서 듣기 소켓을 오픈한 후에, TINY는 전형적인 무한 서버 루프를 실행하고, 반복적으로 연결 요청을 접수하고(32번 줄), 트랜잭션을 수행하고(36번 줄), 자신 쪽의 연결 끝을 닫는다(37번 줄).
doit 함수
- doit 함수는 한 개의 HTTP 트랜잭션을 처리한다.
- 먼저, 요청 라인을 읽고 분석한다(11~14번 줄) -> rio_readlineb함수를 사용해서 요청 라인을 읽어들인다는 점에 유의해야한다.
- TINY는 GET 메소드만 지원한다. 만일 클라이언트가 다른 메소드(POST 같은)를 요청하면, 에러 메세지를 보내고, main 루틴으로 돌아오고(15~19번 줄), 그 후에 연결을 닫고 다음 연결 요청을 기다린다. 그렇지 않으면 읽어들이고(살펴보겠지만), 다른 요청 헤더들을 무시한다.(20번 줄).
- 다음으로, URI를 파일 이름과 비어 있을 수도 있는 CGI 인자 스트링으로 분석하고, 요청이 정적 또는 동적컨텐츠를 위한 것인지(23번 줄) 나타내는 플래그를 설정한다.
- 만일 이 파일이 디스크 상에 있지 않으면, 에러 메시지를 즉시 클라이언트에게 보내고 리턴한다.
- 요청이 정적 컨텐츠를 위한 것이라면, 우리는 이 파일이 보통 파일이라는 것과 읽기 권한을 가지고 있는지(31번 줄)를 검증한다. 만일 그렇다면, 정적컨텐츠(36번 줄)를 클라이언트에게 제공한다.
- 요청이 동적 컨텐츠를 대한 것이라면 이 파일이 실행 가능한지(39번 줄) 검증하고, 만일 그렇다면 진행해서 동적 컨텐츠를 제공한다(44번 줄).
clienterror 함수
- TINY는 실제 서버에서 볼 수 있는 많은 에러 처리 기능들이 없다.
- 그러나, 일부 명백한 오류에 대해서는 체크하고 있으며, 클라이언트에게 보고한다.
1 void clienterror(int fd, char *cause, char *errnum,
2 char *shortmsg, char *longmsg)
3 {
4 char buf[MAXLINE], body[MAXBUF];
5
6
7 sprintf(body, "<html><title>Tiny Error</title>");
8 sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
9 sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
10 sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
11 sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);
12
13
14 sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
15 Rio_writen(fd, buf, strlen(buf));
16 sprintf(buf, "Content-type: text/html\r\n");
17 Rio_writen(fd, buf, strlen(buf));
18 sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
19 Rio_writen(fd, buf, strlen(buf));
20 Rio_writen(fd, body, strlen(body));
21 }
- clienterror함수는 HTTP 응답을 응답 라인에 적절한 상태 코드와 상태 메세지와 함께 클라이언트에게 보내며, 브라우저 사용자에게 에러를 설명하는 응답 본체에 HTML 파일도 함께 보낸다.
- HTML 응답은 본체에서 컨텐츠의 크기와 타입을 나타내야 한다.
- HTML 컨텐츠를 한 개의 스트링으로 만들어 크기를 쉽게 결정할 수 있다.
- rio_writen 함수를 모든 출력에 대해서 사용하고 있다.
Content-type: text/html\r\n은 응답 본체의 MIME 타입이 HTML 문서임을 나타내며, 웹 브라우저가 본체의 내용을 올바르게 해석하고 표시하도록 돕는다.
Content-length: %d\r\n\r\n은 응답 본체의 길이(바이트 단위)를 지정한다. 이 정보는 클라이언트가 서버로부터 받을 데이터의 양을 알 수 있게 해주며, 데이터 전송의 끝을 식별하는 데 도움이 된다.
read_requesthdrs함수
- TINY는 요청 헤더 내의 어떤 정보도 사용하지 않는다.
- read_requesthdrs함수를 호출해서 이들을 읽고 무시한다.
1 void read_requesthdrs(rio_t *rp)
2 {
3 char buf[MAXLINE];
4
5 Rio_readlineb(rp, buf, MAXLINE);
6 while(strcmp(buf, "\r\n")) {
7 Rio_readlineb(rp, buf, MAXLINE);
8 printf("%s", buf);
9 }
10 return;
11 }
- HTTP 요청 헤더를 읽고, 터미널에 출력하는 함수
- 요청 헤더를 종료하는 빈 텍스트 줄이 6번 줄에서 체크하고있는 carriage return과 line feed 쌍으로 구성되어 있다는 점에 주목
parse_uri 함수
- TINY는 정적 컨텐츠를 위한 홈 디렉토리가 자신의 현재 디렉토리이고, 실행파일의 홈 디렉토리는 /cgi-bin이라고 가정한다.
- 스트링 cgi-bin을 포함하는 모든 URI는 동적 컨텐츠를 요청하는 것을 나타낸다고 가정한다.
- 기본 파일의 이름은 ./home.html이다
- URI가 동적 인지 정적인지 구분하여 실행시킬 수 있도록 한다.
serve_static 함수
- 정적 콘텐츠를 효율적으로 서비스하기 위한 전형적인 절차를 따릅니다.
- 응답 헤더를 통해 클라이언트에게 필요한 정보를 제공하고, 메모리 매핑을 통해 파일의 내용을 직접 클라이언트에 전송하여, 파일 시스템에서 직접 읽는 것보다 더 빠른 데이터 전송을 가능하게 합니다.
serve_dynamic 함수
get_filetype 함수
- 주어진 파일 이름(filename)을 분석하여 파일의 MIME 타입을 결정하고, 이를 filetype 변수에 저장한다.
\r\n의 의미
\r\n은 "carriage return"(\r)과 "line feed"(\n)의 조합으로, HTTP 프로토콜에서 헤더 라인을 구분하는 데 사용된다.
- HTTP 헤더와 본체 사이, 그리고 헤더 라인 사이에는 항상
\r\n이 사용된다.
- 이는 HTTP 표준에 따른 것으로, 모든 헤더 필드가 끝날 때마다 사용된다. 또한, 헤더와 본문 사이를 구분하는 데에도
\r\n\r\n이 사용되어, 본문의 시작을 나타낸다.
sprintf 함수
- 데이터를 특정 포맷에 맞춰 문자열로 변환하여, 지정된 문자열 버퍼에 저장하는 표준 C 라이브러리 함수
- printf 함수와 유사하지만, 결과를 화면에 출력하는 대신 문자열 버퍼에 저장한다는 차이가 존재한다.
- 사용법 : sprintf의 첫 번째 인자는 데이터를 저장할 대상 문자열 버퍼의 주소, 두 번째 인자는 포맷 문자열로, 이후 인자들의 데이터를 어떻게 문자열로 변환할지 지정.
- 예시 : sprintf(buffer, "Value: %d", 100);는 정수 100을 문자열 "Value: 100"으로 변환하여 buffer에 저장한다.
Rio_writen 함수
- Rio_writen 함수는 "Robust I/O" (Rio) 라이브러리의 일부로, 네트워크 소켓을 통해 데이터를 안정적으로 전송하기 위한 함수
- 데이터를 전송하려는 시도에서 발생할 수 있는 여러 가지 오류를 처리하고, 지정된 바이트 수가 실제로 전송될 때까지 데이터를 전송하려고 시도
- 첫 번째 인자는 데이터를 보낼 소켓의 파일 디스크립터, 두 번째 인자는 전송할 데이터를 담고 있는 버퍼의 주소, 세 번째 인자는 전송할 바이트 수
- Rio_writen은 네트워크 프로그래밍에서 데이터 전송의 신뢰성을 높이기 위해 설계
- 기본 write 시스템 호출과 달리, 지정된 바이트 수를 완전히 전송하기 위해 필요한 경우 여러 번의 시도 전송 과정에서 발생할 수 있는 네트워크 지연이나 오류 등을 처리하는 데 도움
- 데이터를 클라이언트의 소켓 파일 디스크립터(fd)를 통해 실제로 네트워크를 통해 전송하는 역할
strstr함수 - sad
- strstr() 함수는 string1에서 string2의 첫 번째 표시를 찾는다.
- 함수는 일치 프로세스에서 string2로 끝나는 널 문자(\0)를 무시한다.
리턴값
- string1에서 string2의 첫 번째 표시 시작 위치에 대한 포인터를 리턴한다.
- string2가 string1에 나타나지 않으면 strstr() 함수는 NULL을 리턴합니다.
- string2가 길이가 0인 스트링을 가리키면 strstr() 함수는 string1을 리턴
strcat함수
- string2를 string1에 연결하고 널 문자로 결과 스트링을 종료
- strcat() 함수는 널로 끝나는 스트링에서 작동
- strcat() 함수는 연결된 스트링에 대한 포인터를 리턴
strchr함수
- 문자열 내에서 특정 문자를 찾을 때 사용
- 첫 번째 인자로 주어진 문자열 내에서 두 번째 인자로 주어진 문자를 검색하고, 그 문자가 처음으로 등장하는 위치의 포인터를 반환
- 만약 찾고자 하는 문자가 문자열에 없다면 NULL을 반환
- strchr은 문자열을 분리하지 않고 단순히 특정 문자의 위치를 찾는 용도로 사용
strtok함수
- 문자열을 특정 구분자(delimiter)를 기준으로 토큰으로 분리할 때 사용
- 첫 번째 인자로 주어진 문자열에서 두 번째 인자로 주어진 구분자 문자열에 포함된 어떤 문자라도 만나면 그 위치를 기준으로 앞부분을 토큰으로 분리
- 문자열을 여러 번에 걸쳐 분리하고 싶을 때, 첫 번째 호출에서는 대상 문자열을 인자로 주고, 이후 호출에서는 첫 번째 인자로 NULL을 주어 이전 호출에서 처리가 끝난 위치부터 계속해서 문자열을 분리
- strtok 함수는 내부적으로 정적 변수를 사용하기 때문에 thread-safe하지 않습니다. 멀티스레드 환경에서는 strtok_r 함수를 사용하는 것이 좋다.
- strtok은 문자열을 변경할 수 있으며, 내부적으로 입력 문자열에 null 문자('\0')를 삽입하여 토큰을 생성한다.
strcpy 함수
- 끝나는 널 문자를 포함하여 string2를 string1에서 지정한 위치로 복사.
- 널로 끝나는 스트링에서 작동
- strcpy() 함수는 복사된 스트링에 대한 포인터를 리턴한다.(string1)
strcmp 함수
- string1 및 string2를 비교
- 함수는 널로 끝나는 스트링에서 작동
- 함수에 대한 스트링 인수는 스트링 끝을 나타내는 널 문자(\0)를 포함해야 한다
리턴값
- 0보다 작음 : string1이 string2보다 작음
- 0 : string1이 string2와 같음
- 0보다 큼 : string1이 string2보다 큼
요약
904p이후 내용 생략
웹 서버 잘 공부하고 갑니다~ MIME 오타났슴둥