Skip to content

11. Network Programming {CSAPP}

Chapters Preview#

11.1. The Client Server Programming Model#

클라이언트 & 서버 모델은 리퀘스트 & 리스폰스 모델과 어떤 점에서 다른지, 다른 모델과 비교해서 어떤 특징을 가지고 있는지 알고싶다. 사실 너무 당연했지만 당연한 만큼 생각하지 않았던 주제이기도 하고.

11.2. Networks#

네트워크 지식을 함양하는 챕터인듯, OSI 7 Layers에 대한 내용이 분명 나올 것이고, 이 책은 그걸 다 다루는 건 아니고, 네트워크 책이 따로 있기도 할 정도로 분량이 방대하니까. 거의 상위 레이어 (Network Layer, Transport Layer 등등)에 대해서 다루지 않을까? 그나마도 기억이 가물가물하다. 과거 수업내용을 참고하자.

11.3. The Global IP Internet#

Internet인 만큼, 네트워크와 네트워크를 이어주는 계층인 네트워크 계층에 심도있는 내용이 나올 것 같다. 어떻게 IP로 호스트를 식별하는지, 라우팅을 어떻게 하고 그래프로 표현된 네트워크 세상 속에서 최적 경로를 찾아가는지에 대한 내용이 들어있을 것 같다.

11.4. The Sockets Interface#

소켓 통신은 어느 영역에 속하지? 네트워크 레이어보다 한 단계 올라간 전송계층인건가? 포트포워딩이 여기에서 나오나? 분명 Linux IPC Programming {inflearn archive} 공부했을 때 소켓에 대한 내용을 들었던 것 같기도 하고 0016 Systems Programming {ssu2021-1st} 🐼 시간에 파일 디스크립터 관련해서 들었던 것 같기도 하다. 빠뜨린 내용을 책 뿐만 아니라 이런 곳에서로부터 적극적으로 참고하여 메꿔보자.

11.5. Web Servers#

바로 다음장이 Tiny Web Server를 만들기 때문에 그걸 위한 준비과정이 들어갈 것 같다. 어떤 웹 서버를 만들까? 일단 소켓 API와 File Descriptor를 활용한 방식은 꼭 들어가겠지. 코드가 나와주어야 하니까.

11.6. Puttling It Together: The Tiny Web Server#

직접 C로 웹 서버를 만들어 보면서 맞딱뜨릴 다양한 함수들과 포트, IP, 프로세스 생성 등에 대한 내용이 나올 것 같다. week06 {swjungle}{proxy-lab}#2023-09-14 목 회의 결과에 따르면 여기까지 15일 금요일까지 어떻게든 도달한 뒤에 놓치고 간 지식을 백 트래킹 하는 방법으로 도입해 보려고 한다.

실습먼저, 구멍은 나중에 메꾸자#

11.4.9. Example Echo Client and Server#

Client (Figure 11.20)

서버와 연결(Open_clientfd)을 맺은 뒤 클라이언트는 루프를 돌면서 stdin으로부터 text line을 입력받고(Fgets) 서버에게 전송한다(Rio_writen). 서버로부터 응답을 수신하고(Rio_readlineb) stdout으로 에코 결과를 출력한다 (Fputs)

Server (Figure 11.21)

서버 프로세스는 에코 서버를 구현하기 위해 listening desciptor를 열고(Open_listenfd) 무한루프 안에서 클라이언트로부터 요청을 기다린다(Accept). 클라이언트의 도메인 네임과 포트를 출력한다.(GetnameInfo). echo 함수 안에서 클라이언트에게 응답을 송신한다. echo가 끝나면, 연결된 descriptor를 닫고(Close) 다시 다음 이터레이션으로 넘어간다.

echo routine (Figure 11.22)

echo 루틴은 반복적으로 읽고(Rio_readlineb) 쓴다(Rio_writen). EOF를 만나면 server 메인 루프를 다시 돌게 된다.

요구사항

  • EOF를 받으면 무한루프를 종료하고 fd를 닫을 것, 커맨드 라인에서 Ctrl + D를 누르면 EOF가 전송된다.

helper functions csapp.c

  • open_listenfd: Opens and returns a listening descriptor.
    • 연결리스트 struct addrinfo listp 를 순회하면서 우리가 ==bind==할 수 있는 descriptor를 찾는다.
    • [?] struct addrinfo는 연결리스트를 가리키고 있는데, getaddrinfo라는 녀석을 호출하면서 리스트의 원소를 얻어오나보다.
    • Setsockopt, Getaddrinfo, Close, Freeaddrinfo 와 같이 대문자로 시작하는 함수들은 책에서 정의한 헬퍼함수들이다.
  • rio_readinitb(rio_t *rp, int fd): Associate a descriptor with a read buffer and reset buffer. rio 버퍼와 fd를 초기화 시키는 함수.
  • rio_writen(int fd, void *usrbuf, size_t n): Robustly write n bytes (unbuffered)
  • rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)

실습결과

스크린샷 2023-09-15 오후 3.38.34.png

11.5.3. Http Transactions#

telnet 실습 결과

쪽은 403 Redirected가 떠서 구글로 바꿨다.

스크린샷 2023-09-15 오후 3.57.30.png

11.5.4. Serving Dynamic Content#

tiny/cgi-bin/adder.c

  • [?] man strchr(3) - returns a pointer to the first occurence of the character c in the string s
  • [?] 아, sprintf를 여러번 호출해도 타겟 인자의 출발위치를 굳이 바꾸지 않아도 알아서 뒤에 append 되는갑다보다. 아니, 매번 문자열 앞에다 %s 하고 뒤에 자기 자신을 인자로 넣고 있었다

11.6 Putting It Together: The TINY Web Server#

tiny/tiny.c:main

  • [?] 도대체 struct socketaddr_storage는 뭐하는 녀석이냐? 쟤한테 sizeof는 왜 다는거지?
  • [?] stat.h

tiny/tiny.c:doit
tiny/tiny.c:clienterror
tiny/tiny.c:parse_uri
tiny/tiny.c:serve_static
tiny/tiny.c:serve_dynamic

11.1. The Client-Server Programming Model#

  • cardinality: Client : server = N : 1, client_process : server_process = 1 : 1, 즉, 서버 프로세스는 요청이 들어올 때마다 하나씩 생성된다.
  • Request: 클라이언트는 서버에게 서비스를 요청한다. 서비스는 구체적으로 서버에게 요청을 보내는 트랜잭션으로 시작될 수 있다. (≠ DB의 ACID 규칙에 따른 트랜잭션)
  • Response: 서버는 (서버 프로세스) request에 따라 동작을 수행한다. file을 읽을 수도 있고, 프로그램을 실행시킬 수도 있다. 하지만 반드시 클라이언트에게 응답을 보내야 하며, 파일이나 바이트 스트림을 보내주게 된다.

11.2. Networks#

NIC(Network Interface Card), 쉽게말해 랜카드를 메인보드에 꽂으면 인터넷을 사용할 수 있게 된다. 그런데 랜카드를 "꽂는다"는 것이 정확히 무엇을 하는 일일까?

IO 버스에 달려있는 어댑터는 하드웨어 인터페이스를 제공한다. DMA 기술로 CPU 간섭 없이 어댑터에서 받은 신호를 IO버스를 통해 메모리로 복사할 수 있으며, 마찬가지로 데이터를 메모리에서 어댑터로 복사할 수도 있다.

네트워크는 계층구조를 가지고 있다. 제일먼저 가장 작은 단위의 네트워크인 LAN이 있다. 허브나 스위치를 통해 서로 물리적으로 연결되어있다. 모든 이더넷 어댑터(랜카드)는 데이터를 frame 단위로 보게 되는데, frame 안에는 출발주소, 목적지 주소가 담긴 헤더, 페이로드, 프레임 길이정보가 담겨있다. 호스트의 모든 하위 레이어가 그렇겠지만, 이더넷 어댑터를 예로 들면 어댑터는 오로지 페이로드를 제외한 헤더, frame length에만 관심을 갖는다. 진짜 페이로드는 오로지 호스트만 볼 수 있다.

bridges

브릿지는 이더넷 세그먼트들 사이를 연결하는 역할을 가지고 있다. 방 한 칸을 LAN이 담당했다면 건물 하나 또는 캠퍼스 하나를 묶어주기 위해 브릿지가 들어간다. 브릿지-브릿지 연결도 있고 브릿지-허브 연결도 존재한다. 브릿지는 한 세그먼트에서 다른 세그먼트로 넘어가야 하는 신호를 선별적으로 골라 필요한 놈만 복제하여 신호를 전파한다.

예를 들어 하나의 허브에 묶인 두 호스트 A, B는 같은 이더넷 세그먼트에 있다. A가 B에게 프레임을 보내면 허브는 항상 모든 링크로 같은 신호를 복제한다고 했지. (허브가 영향을 미치는 구간을 Collision Domain이라고도 한다.) 그래서 B또한 프레임을 받아 원하는 목적을 달성할 수 있다. 문제는 허브는 브릿지에게도 신호를 쏴버렸다는 것이다. 이때 브릿지는 똑똑하게 신호를 무시하는데, 같은 이더넷 세그먼트에 있는 신호를 굳이 밖으로 전파해봤자 무의미하기 때문이다.

[!question]- 그래서, 이더넷 세그먼트가 뭔데?
Network Segment {wiki}에 따라, 공유 매질에 따라 전기적으로 연결된 네트워크 기기들을 네트워크 세그먼트라고 부른다. 그러니까 랜선으로 직접 연결이 되던, 스위치나 허브에 의해 간접적으로 연결이 되던 결국 같은 매질에 의해 전기적으로 연결이 되어있다는 것을 의미하는듯.
네트워크 세그먼트는 OSI 레이어와 같이 가는데, 이더넷 세그먼트는 물리영역인 OSI 1에 해당하는 네트워크 세그먼트이다.

Routers

브릿지가 LAN과 LAN 사이를 이어주는 건 알겠어. 이더넷 세그먼트라고 불리우는 호환되는 영역 안에서 조금 큰 단위로 묶어주는 놈인거지. 그러면 라우터는 할 일이 없겠네?

라우터는 서로 호환되지 않는 LAN과 WAN을 이어주어 Interconnected Network를 구성한다.

지구는 넓잖아. 회사도 많고. 다들 최고의 기술력을 자랑하다보면 어떤 곳은 광랜을 쓰는 곳도 있고, 동축 케이블 내지는 twisted copper wire를 쓰는 곳도 있을 거고. 브릿지는 이런 규격의 차이를 이해하지 못한다. 세상이 하나로 연결되기 위해서 프로토콜이라는 공통규약을 만들어놓고 네트워크들이 이것만큼은 반드시 구현하도록 만들었다. 크게 두 가지 규약이 있다.

  1. Naming Scheme: IP주소로 불리우는 주소 식별 규칙이다.
  2. Delivery Mechanism: 비트를 패킷으로 감싸는 방법에 대한 규칙이다.

다음은 두 호환되지 않는 LAN 사이에 페이로드를 전송하는 일련의 과정에 대해서 설명한다.

  1. LAN1 영역의 Host A는 옆동네 LAN2에 사는 Host B에게 메시지를 보내기 위해 시스템 콜을 사용하여 커널 버퍼에 메시지를 복사한다.
  2. 프로토콜 소프트웨어는 (몇 단계가 숨어있기는 하지만) 각각 패킷 헤더와 프레임 헤더를 붙여 여러개의 작은 프레임으로 나누어 Host A 어댑터에게 프레임을 전달한다.
  3. Host A 어댑터는 네트워크망(호스트 A 밖)으로 프레임을 전달한다. (구체적으로 라우터에게 보내는 행위가 아닌가보네)
  4. 프레임을 받은 라우터의 어댑터(LAN1 어댑터)는 라우터 안에 상주한 프로토콜 소프트웨어에게 프레임을 전달한다.
  5. 프로토콜 소프트웨어는 프레임 > 패킷 순으로 열어재껴 패킷 헤더에 담긴 목적지 주소를 라우팅 테이블에 넣고 돌린다. LAN2가 나왔다고 치면 프로토콜 소프트웨어는 기존 LAN1 프레임 헤더를 LAN2 프레임 헤더로 바꿔치기 한 후 같은 라우터 안에 있는 LAN2 어댑터로 프레임을 전달한다.
  6. 라우터의 LAN2 어댑터는 LAN2 네트워크에 프레임을 전파한다.
  7. Host B가 프레임을 수신하게 되면, HostB 어댑터가 먼저 프레임을 읽고 프로토콜 소프트웨어에게 프레임을 전달한다.
  8. Host B의 프로토콜 소프트웨어는 프레임 헤더와 패킷 헤더를 벗겨내 목적지 프로세스의 가상메모리 공간에 페이로드 내용을 복사한뒤 시스템 콜을 호출하여 그 내용을 읽을 수 있게된다.

11.3. The Global IP Internet#

Internet Protocol의 역할은 서로 다른 호환되지 않는 두 네트워크 간 통신을 가능하게 만들어준다고 했다. IP 통신은 시스템 콜인 소켓 api를 통해 가능하다. IP는 데이터그램이 누락됐는지, 중복됐는지 여부를 조사하지 않기 때문에 상위 레이어인 TCP에서 커넥션을 기반으로 한 신뢰도 있는 통신을 보장한다.

IP Addresses

전 세계 모든 호스트는 32비트 unsigned 정수형으로 된 주소를 가지고 있다. 모든 네트워크는 byte ordering이 빅 엔디언이다. arpa/inet.h 헤더에는 바이트 오더를 네트워크에서 호스트로, 호스트에서 네트워크로 바꿔주는 함수 ntoh[l/s], hton[l/s]가 있다.

단순 숫자이긴 하지만 외우기 불편하기 때문에 8비트씩 쪼개어 4개의 1바이트 크기의 정수형으로 관리한다. inet/in.h 헤더에는 사람이 보는 아이피주소를 32비트 unsigned 정수형으로 변환해주는 inet_ntop와 네트워크 주소를 사람이 읽기 편한 inet_pton으로 제공되고 있다.

11.3.2. Internet Domain Names#

그런데, 사람들은 게을러터져서 8비트 숫자 네개를 기억하고 싶어하지도 않았다. 따라서 도메인과 아이피를 매핑한 DNS(Domain Name System)를 통해 유저들의 편의성을 도모했다. 도메인 이름은 트리구조로 이루어져 있는데, 흔히 볼 수 있는 choiwheatley.github.io를 예시로 들면 오른쪽에서부터 unnamed_root > io > github > choiwheatley 순으로 트리가 깊어진다. 첫번째 레벨 도메인 이름은 ICANN에서 정의하고 두번째 도메인 이름은 선착순으로(...)이름을 정의했다고. 세번째 도메인부터는 서브도메인으로, 자유롭게 추가할 수 있다.

11.4. The Sockets Interface#

Socket Programming C API

서버 & 클라이언트 공통

  • struct sockaddr_in: IP주소와 포트번호가 빅 엔디언으로 들어있는 구조체
  • struct sockaddr: 일반화된 소켓주소 타입. connect, bind, accept 할 때 캐스팅 해야함.
  • int socket(int domain, int type, int protocol): 소켓 fd를 리턴한다. socket(2)
    • domain: AF_INET | AF_INET6
    • type: SOCK_STREAM | SOCK_DGRAM
    • protocol: 0

클라이언트

  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen): connect(2)
    • 소켓 디스크립터로 읽기 쓰기 가능한 상태를 만들기 위한 주소 정보를 추가한다.

서버

  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen): bind(2)
    • assign a name to a socket
    • use getaddrinfoto supply the arguments to bind
  • int listen(int sockfd, int backlog): listen(2)
    • sockfd를 액티브 디스크립터에서 listening 디스크립터로 바꿔 클라이언트 요청을 수신하는 descriptor로 사용되도록 만든다.
    • backlog: 큐의 최대 길이를 지정.
    • return: 클라이언트 listening descriptor를 리턴한다.
  • int accept(int listenfd, struct sockaddr *addr, int *addrlen):
    • client desciptor로부터 데이터가 들어오기까지 기다린다.
    • return: connected deciptor를 리턴한다.
    • listening descriptor와 connected descriptor 와의 차이
      • 서버가 살아있는 동안 함께 살아있는 디스크립터를 listening descriptor라고 부른다.
      • 오직 하나의 커넥션이 살아있는 동안에만 존재하는 디스크립터를 connected descriptor라고 부른다.

Pasted image 20230917194318.png

  1. 서버는 accept를 호출하여 block 상태가 된다.
  2. 클라이언트는 connect를 호출하며 서버에게 커넥션을 요청한 뒤 block 상태가 된다.
  3. 서버는 block 상태에서 벗어나 accept를 호출하여 connected descriptor를 획득한다. accept 응답이 클라이언트에게 날아가 클라이언트로 block 상태에서 벗어나 client desciptor를 획득한다. 이제 서버는 connected descriptor를 이용하여, 클라이언트는 client desciptor를 이용하여 소통할 수 있게 된다.

11.4.7. Host and Service Conversion#

getaddrinfo(3)

getnameinfo(3)

11.4.8. Helper Functions for the Sockets Interface#

open_clientfd {socket} {connect}

open_listenfd {socket}{bind}{listen}

11.5. Web Servers#

  • HTML, Hyper Text Markup Language, 그래픽 화면에 띄울 내용을 인간도, 컴퓨터도 읽을 수 있게 만든 규격. 특징으로는 하이퍼링크를 걸어 다른 곳으로도 이동할 수 있다.
  • MIME Type, Multipurpose Internet Mail Extensions로, 연속된 바이트로 전송된 문자열이 어떤 방식으로 해석될 수 있는지에 대한 일종의 해독지이다.
  • Static Content: 서버는 클라이언트의 요청에 디스크에 저장된 파일을 가져와 보내준다. 이때 그 파일을 정적 컨텐츠라고 볼 수 있음.
  • Dynamic Content: 서버는 클라이언트의 요청에 실행파일을 실행한 결과를 보내준다. 이때 실행 결과물을 동적 컨텐츠라고 볼 수 있음.
  • URL: Uniform Resource Locator의 약자로, 서버 컨텐츠의 리소스를 식별할 수 있기 때문에 URI(Uniform Resource Identifier)라고도 불리운다. <프로토콜>://<호스트>[:포트][/][리소스 패스]로 이루어져 있다.
    • 동적 컨텐츠의 경우, 클라이언트는 URL에 실행파일의 이름과 그 인자를 넣을 수 있는데, 파일이름과 인자들을 구분하는 ? 와 인자들 사이를 구분하는 &가 있다. 예를 들어
    • http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?16000&213
    • 다음 URL은 bluefish.ics.cs.cmu.edu 호스트의 8000번 포트에 상주하고 있는 프로세스에게 adder라는 이름의 실행파일을 실행시킨다. 이때 두 인자 16000213을 넣었다.
  • HTTP Requests
    • request headers
      • <method> <uri> <version>
      • <header-name>: <header-data>
    • [?] Persistent connection?
    • [?] proxy cache?
  • HTTP Responses
    • response headers
      • <version> <status-code> <status-message>
    • response body

CGI (Common Gateway Interface)

Dynamic Content를 생성하기 위해서 클라이언트는 URL 리소스 뒤에 인자를 추가한다고 했다. 그러면 서버가 이를 분리하고 별도의 변수(또는 리스트)에 담았다고 치자. 서버에서는 무슨 일이 일어날까?

  1. CGI 환경변수 중 QUERY_STRING에 인자목록을 걍 넣는다.
  2. 사전에 등록된 CGI program을 서버가 fork하고 execve하여 자식 프로세스를 생성한다.
  3. CGI 프로세스는 해당 환경변수를 읽음으로써 자연스럽게 인자를 읽을 수 있다. 파싱까지 해주나?
  4. [?] dup2 fd 리디렉션 덕분에 CGI 프로세스는 별 생각없이 stdout으로 리턴하기만 해도 알아서 클라이언트에게 응답이 간다고!

Homework Problems#

week06 {swjungle}{proxy-lab}에서 제안한 과제만 아래에 적어두었다.

11.6c#

Inspect the output from Tiny to determine the version of HTTP your browser uses
Tiny 출력결과를 통해 당신이 사용하는 브라우저 버전을 알아보세요.

Accepted connection from (localhost, 54870)
Request headers:
GET / HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
DNT: 1
Connection: keep-alive
Cookie: csrftoken=y6OUxjxvrhDvbEQwpxvkkKlXpMGcZfh7
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0

11.7#

Extend Tiny so that it serves MPG video files. Check you work using a real browser
TinyMPG 영상파일을 제공하도록 기능을 확장해 보세요. 실제 브라우저를 활용해 시도해 보세요.

❗️ MPG 포맷이 아닌, MP4 포맷을 사용하도록 변경.

스크린샷 2023-09-18 오전 11.12.07.png

11.9#

Modify Tiny so that when it serves static content, it copies the requested file to the connected descriptor using malloc, rio_readn, and rio_writen, instead of mmap and rio_written.
Tiny를 수정해서 정적 컨텐츠를 제공할 때 malloc, rio_readn, rio_writen을 사용해서 디스크립터가 컨텐츠를 복사하도록 만들어보세요.

어려운 용어들 나온다. descriptor는 file desciptor를 의미하나? rio 접두어는 무엇을 하는 녀석일까?

10. System-Level IO {CSAPP}에 제안된 Robust IO 패키지였다...! 11장의 디펜던시가 10장에 있었다니... 이부분도 읽어야 하나?

2023-09-18T11:12:45

쉽게 말해 mmap 쓰지말고 너가 malloc 써서 직접 메모리를 매핑하라 이 말인듯.

2023-09-18T14:26:55

11.10#

Write an HTML form for the CGI adder function in Figure 11.27. Your form should include two text boxes that users fill in with the two numbers to be added together. Your form should request content using GET method.
adder 함수를 위한 폼 HTML을 작성하세요. 폼은 두개의 텍스트 박스로 이루어져 있으며 유저가 각각의 숫자를 채워 넣어 두 숫자를 더한 값을 확인하도록 해보세요. 폼은 GET 메서드를 이용하여 요청되어야 합니다.

❓ CGI? Computer Graphics Interface? 아니, Common Gateway Interface이다

11.11#

Extend Tiny to support the HTTP HEAD method. Check your work using TELNET as a web client.
TinyHEAD 메서드를 지원하도록 확장해보세요. 텔넷을 웹 클라이언트로 결과를 확인해 보세요.

아니 요즘 누가 텔넷을 쓰냐? 맛좋은 포스트맨이 있는데!

일단 request header의 구성을 조사해 볼 필요가 있다.

A request header is an HTTP header that can be used in an HTTP request to provide information about the request context, so that the server can tailor the response.

GET 메서드에 대한 request header 예시. 첫 줄에는 무조건 메서드, 엔드포인트, HTTP 버전이 들어가는구나.

GET /home.html HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/testpage.html
Connection: keep-alive
Upgrade-Insecure-Requests: 1
If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT
If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"
Cache-Control: max-age=0

생각해 보니 serve_static에서 body를 제외한 부분만 보내면 되는 아주 간단한 문제였다.

/// @brief response header against HEAD request
void serve_static_head(int fd, char *filename, int filesize) {
  int srcfd;
  char *srcp, filetype[MAXLINE], buf[MAXBUF];

  // Send response headers to client
  get_filetype(filename, filetype);
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
  sprintf(buf, "%sConnection: close\r\n", buf);
  sprintf(buf, "%sContent-Length: %d\r\n", buf, filesize);
  sprintf(buf, "%sContent-Type: %s\r\n\r\n", buf, filetype);

  // send to client response header
  Rio_writen(fd, buf, strlen(buf));

  // just for me
  printf("Response headers: \n");
  printf("%s", buf);

  // there are no body in this response! end function
}