## 책 정보
[밑바닥부터 시작하는 웹 브라우저](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 표시하기
## 암호화된 연결
## 요약
## 연습 문제