## 책 정보 [밑바닥부터 시작하는 웹 브라우저](https://www.hanbit.co.kr/store/books/look.php?p_code=B6818199506) ## 서버에 연결하기 URL : Uniform Resource Locator, 자원을 식별하는 통일된 접근 방법 기술 규약 ![[2119ef67-9294-4f58-8b63-42544f22d927.png]] - 스킴 : 어떻게 정보를 얻을 수 있는지를 설명 - 호스트 : 어디에서 정보를 얻을 수 있는지를 설명 - 경로 : 무슨 정보를 얻을 것인지를 설명 이 외에도 선택 값인 포트, 쿼리, 프래그먼트 같은 부분도 있다. URL이 주어지면 => 브라우저는 웹페이지를 다운로드한다. 1. 브라우저 -> 운영체제 : 호스트 이름에 해당하는 서버와 연결해달라 2. 운영체제 : DNS와 통신해 호스트 이름을 93.184.216.34 같은 IP주소로 변환함 3. 운영체제 : 라우팅 테이블을 이용해 해당 IP 주소와 통신하기에 가장 적합한 장치를 결정. 장치 드라이버로 신호를 보냄 4. 라우터 : 신호를 감지해서 메시지를 전달할 최적의 라우터에게 전송 5. ... 라우터간 전달 반복 ... 후 목적지에 도착 6. 메시지가 서버에 도착해서 해당 서버와의 연결이 이루어진다. telnet으로 서버와의 연결을 확인할 수 있다 (mac에서는 `nc -v` 를 사용해도 되고, telnet을 설치해도 된다.) ```shell nc -v example.org 80 ``` ![[ad8e6e7d-7d2a-4df7-9e8a-98589a1c476f.png]] ```shell brew install telnet telnet example.org 80 ``` ![[674e786e-aff6-446d-80ed-c3c10007850a.png]] 서버와 연결했으므로 이제 example.org와 메시지를 주고받을 수 있다! > [!note] > URL 문법은 [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) 에 정의되어 있고 첫번째 저자는 팀 버너스 리이다 (또...!) ## 정보 요청하기 서버와 연결했으므로 /index.html처럼 호스트 이름 뒤에 오는 URL의 경로에 해당하는 정보를 요청할 수 있다. ![[170893b5-1f84-43e4-8999-52b038305ef4.png]] 실습 환경에서는 HTTP 1.0 버전만을 사용한다. [Evolution of HTTP — HTTP/0.9, HTTP/1.0, HTTP/1.1, Keep-Alive, Upgrade, and HTTPS \| by Thilina Ashen Gamage \| Platform Engineer \| Medium](https://medium.com/platform-engineer/evolution-of-http-69cfe6531ba0) 요청 첫 번째 줄 이후에는 줄마다 이름과 값으로 이루어진 헤더가 자리한다. 헤더 종류는 더 많지만 우선은 Host만 사용한다. 마지막 헤더가 끝나면 빈 줄을 하나 넣어주는데, 이는 호스트에게 헤더가 끝났음을 알린다. telnet에서 요청할 때에도 위의 두 줄을 입력한 뒤에 enter를 두번 입력해야 example.org에서 응답을 받을 수 있다. ![[d2f13e8f-df94-4b76-81dc-837e460ac311.png]] ## 서버의 응답 서버의 응답 첫 줄은 이렇게 생겼다. ![[ee5d096c-e22a-4709-8abc-dcc4870cd554.png]] 호스트가 HTTP/1.0으로 응답하고 요청에 대해 OK로 응답했음을 알려준다. 다른 응답 코드로는 아래와 같은 것들이 있다. - 100번대: 정보 메시지 - 200번대: 성공 - 300번대: 후속 작업(일반적으로 리다이렉트) 필요 - 400번대: 잘못된 요청 - 500번대: 서버가 요청을 제대로 처리하지 못함 첫번째 줄 뒤로는 서버게 헤더를 보낸다. 요청에 따라 다르지만 이런 헤더들을 보낸다. ``` Content-Type: text/html ETag: "bc2473a18e003bdb249eba5ce893033f:1760028122.592274" Last-Modified: Thu, 09 Oct 2025 16:42:02 GMT Cache-Control: max-age=86000 Date: Mon, 24 Nov 2025 14:24:39 GMT Content-Length: 513 Connection: close X-N: S ``` 많은 정보들이 있지만 일단은 넘어갑니다. 헤더 뒤에는 빈 줄이 있고, 이어서 HTML 코드가 전달된다. 이를 서버 응답의 바디라고 하고, 브라우저는 Content-Type 헤더를 통해서 이어지는 Body가 html임을 짐작한다. 여기까지 본 브라우저와 서버의 요청 응답 과정을 그림으로 정리하면 다음과 같다. ![[53504968-6c69-4990-9505-58dc6636cea2.png]] 이젠 telnet 대신 파이썬으로 이 과정을 직접 작성해봅시다. ## 파이썬을 통한 텔넷 ```python import socket class URL: def __init__(self, url: str): # 스킴 분리 - 스킴은 '://' 앞부분이다 self.scheme, url = url.split("://", 1) assert self.scheme == "http" # 실습 브라우저는 http만 지원하기 때문에 확인 # 호스트와 경로 분리 - 호스트는 다음에 오는 '/' 앞부분이다 if "/" not in url: url = url + "/" self.host, self.path = url.split("/", 1) #url으로 요청을 보내기 위한 메서드 def request(self): # 호스트에 연결하기 (소켓을 이용합니다.) # 다른 컴퓨터와 연결할 때 필요한 요소 # 1. 주소 패밀리 (AF로 시작) : 다른 컴퓨터를 찾는 방법을 알려준다. # 우리는 AF_INET을 쓴다. # 2. 소켓 타입 (SOCK으로 시작) : 어떤 방식으로 데이터를 주고 받을지 알려준다. # 우리는 SOCK_STREAM을 쓴다.(임의의 양의 데이터를 전송할 수 있는 타입) # 3. 프로토콜 (IPPROTO로 시작) : 어떤 프로토콜을 사용할지 알려준다. # 우리는 IPPROTO_TCP를 쓴다.(TCP 프로토콜) # (최신 버전의 HTTP는 QUIC 프로토콜을 사용함) sock = socket.socket( family=socket.AF_INET, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP ) # 다른 컴퓨터에 연결하라고 지시 sock.connect((self.host, 80)) # 👏 연결 끝. ``` ## 요청과 응답 ```python # 요청(request) 만들기 request = "GET {} HTTP/1.0\r\n".format(self.path) request += "Host: {}\r\n".format(self.host) request += "\r\n" # 헤더와 바디 구분을 위한 빈 줄 # 요청(request) 보내기 sock.send(request.encode("utf8")) ``` 중요한 점...! - 줄바꿈에는 `\r\n`을 사용한다. - 마지막에 한번 더 `\r\n`을 보내야 한다. 이걸 빼먹으면 서버는 계속 요청의 끝을 기다리고 우리도 응답이 오기를 기다리게 된다. encode는 텍스트를 바이트로 변환한다. ![[ee7a6102-081f-46e0-b927-69d62760ddbe.png]] 서버의 응답을 읽을 때에는 소켓의 read 함수를 사용하는데, 데이터가 도착할 때까지 수집하는 루프가 필요하다. 계속해서 읽는 makefile이라는 헬퍼 함수를 사용한다. (파이썬이 아니라면 루프에서 직접 socket.read를 호출해줘야 한다.) ```python # 응답(response) 받기 response = sock.makefile("r", encoding="utf8", newline="\r\n") # 응답 파싱 # 상태줄 파싱 statusline = response.readline() version, status, explanation = statusline.split(" ", 2) # 서버 응답 HTTP 버전 확인은 생략하지만, 사실은 확인하는 것이 좋다 # 헤더 줄 파싱 response_headers = {} while True: line = response.readline() if line == "\r\n": break header, value = line.split(":", 1) # 헤더는 대소문자 구분하지 않으므로 소문자로 통일 response_headers[header.casefold()] = value.strip() # 중요한 헤더가 있는지를 확인한다. assert "transfer-encoding" not in response_headers assert "content-encoding" not in response_headers # 바디 읽기 body = response.read() # 연결 닫기 sock.close() return body ``` ## HTML 표시하기 ## 암호화된 연결 ## 요약 ## 연습 문제