티스토리 뷰
하드웨어 레벨
해당 섹션은 키보드의 물리적인 동작과 그에 따른 운영체제의 인터럽트에 대해 다룹니다.
"g"가 눌렸을 때
가장 먼저 google의 "g"를 입력하게 되면 해당 입력 이벤트가 브라우저에 전달되고 자동 완성 기능이 실행됩니다. 사용 중인 브라우저의 알고리즘과 개인/익명 모드를 사용하는지 등에 따라 다양한 URL이 URL 창 아래의 드랍박스 형태로 나타납니다.
대부분의 알고리즘은 검색 기록, 즐겨찾기, 쿠키, 빅데이터 기반의 유명한 검색어 등을 이용하여 정렬하여 나타냅니다.
"google.com"의 단어를 하나 하나 입력해갈 때마다 알고리즘은 수정되며 추천 URL이 변경될 것이며 "google.com" 입력을 전부 끝내기도 전에 알고리즘에 의해 "google.com"이 추천될 수도 있습니다.
"엔터"가 눌렸을 때
명령의 정확한 시작 지점을 정하기 위해, 엔터 키가 끝까지 눌렸을 때를 시작하는 지점으로 정한다고 해봅시다. 엔터가 끝까지 눌린 그 시점에, 엔터에 할당된 전기 회로가 닫히게 됩니다 (직접적이든 정전식으로든).
이는 키보드의 논리 회로에 소량의 전류가 흐르도록 하는데, 해당 논리 회로는 각 키보드의 스위치의 상태를 모니터링하고 있어, 논리 회로에서는 스위치가 닫힘으로써 생겨나는 전기적 변화를 감지하고, 변화량을 키보드의 키 각각이 가지는 숫자(키코드)로 변환합니다. 엔터의 경우는 13이 될 것입니다.
키보드 컨트롤러는 해당 숫자를 컴퓨터에 전달하기 위해 인코딩하게 됩니다. 이전에는 PS/2나 ADB 연결이 사용되어 왔지만 이제는 사실상 표준이 된 USB나 블루투스를 통해 전달되게 됩니다.
USB를 사용하는 키보드의 경우:
- 키보드의 USB 회로 소자는 컴퓨터 USB 호스트 컨트롤러의 핀 1로 제공되는 5V 전원으로 동작합니다.
- 생성된 키코드는 키보드 회로 내부의 "endpoint"라고 불리는 레지스터에 저장됩니다.
- 호스트 USB 컨트롤러에서는 "endpoint"를 매 10ms(키보드에 따라 상이)마다 폴링하여 키코드를 가져옵니다.
- 키코드는 USB SIE (Serial Interface Engine)를 통해 저레벨 USB 프로토콜을 따르는 USB 패킷(1개 혹은 그 이상)으로 변환됩니다.
- USB 패킷은 D+와 D- 핀 (USB의 총 4개의 핀 중 가운데 2개. 양쪽 2개는 전원 핀)을 통해 전송됩니다. 이때 USB 2.0 표준에 의해 키보드와 같은 주변기기는 최대 1.5 Mb/s로 전송됩니다.
- 해당 직렬 신호는 컴퓨터에 도달하면 호스트 USB 컨트롤러에 의해 디코딩된 이후에 HID의 키보드 드라이버에 의해 인터프리트됩니다. 이후 키코드는 OS의 하드웨어 추상 레이어로 전달됩니다.
가상 키보드의 경우 (스마트폰의 터치 스크린과 같은):
- 정전직 터치 스크린에 터치를 하는 순간 미량의 전류가 손가락쪽으로 흐르게 됩니다. 이 전류 변화로 인해 전도층의 정전계로 회로가 이어지게 되어 터치된 정확한 그 지점의 전압을 낮추는 결과로 이어집니다. 이후 전압의 변화를 모니터링하는 "스크린 컨트롤러"에 의해 인터럽트가 발생됩니다.
- 인터럽트를 통해 모바일 OS는 현재 사용 중인 애플리케이션에 키가 눌렸음을 알립니다.(현재의 경우 애플리케이션은 가상 키보드를 말함)
- 애플리케이션(가상 키보드)에서는 '키가 입력됨' 메시지라는 소프트웨어 인터럽트를 OS에게 보내게 됩니다.
- 소프트웨어 인터럽트를 통해 '키가 입력됨' 이벤트는 현재 사용 중인 애플리케이션에 전달됩니다.
인터럽트 발생 [키보드가 USB가 아닌 경우]
키보드는 인터럽트 요청 라인 (IRQ) 를 통해 신호를 보내는데, 이 라인은 인터럽트 컨트롤러에 의해인터럽트 벡터
(integer)에 매핑되어 있습니다. CPU는 Interrupt Descriptor Table
(IDT)을 이용하여 커널에 의해 제공되는 함수들 (인터럽트 핸들러
) 에 인터럽트 벡터를 매핑합니다.
인터럽트가 도착하면, CPU는 IDT와 인터럽트 벡터를 매핑 한 이후에 적절한 핸들러를 실행합니다.
이제 커널로 진입합니다.
커널 레벨
(Windows에서) WM_KEYDOWN
메시지가 애플리케이션으로 전달
HID 트랜스포트는 키 입력 이벤트를 HID가 사용하는 형태의 스캔코드로 변환하는 KBDHID.sys
드라이버에 전달합니다. 이 경우에 스캔코드는 VK_RETURN
(0x0D
)가 될 것입니다.
KBDHID.sys
드라이버는 KBDCLASS.sys
(키보드 클래스 드라이버)의 인터페이스입니다.
해당 드라이버는 모든 키코드 입력이 안전하게 처리되게 관리하는 역할을 합니다. 그 이후 (높은 확률로 설치된 서드파티 키보드 필터를 통과한 메시지로) Win32K.sys
를 호출합니다. 이 모든 과정은 커널 레벨에서 진행되고 있습니다.
Win32K.sys
는 현재 사용 중인 window(윈도우에서는 애플리케이션을 window라 함.)를 GetForegroundWindow()
API를 통해 확인합니다. 이 API를 통해 브라우저 주소창을 제어할 수 있습니다. 윈도우 OS의 "message pump"는 SendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam)
를 호출합니다.lParam
은 키 입력의 상세 정보에 대한 비트마스크입니다: 반복 횟수(현재 예시의 경우 0),
실제 스캔 코드 (벤더별로 상이하지만 일반적으로 VK_RETURN
), 익스텐드 키(e.g. alt, shift, ctrl 등) 입력 여부 (현재 예시에서는 눌리지 않음) 이외의 몇몇 다른 상태에 대한 정보 등이 상세 정보에 담겨 있음.
윈도우 OS의 SendMessage
API는 특정한 window 핸들(hWnd
)의 큐에 메시지를 추가하는 간단한
함수입니다. 그 이후 hWnd
에 할당된 (WindowProc
이라 불리는) 메인 메시지 처리 함수가 큐의 메시지를 처리하기 위해 호출됩니다.
활성화 된 window(hWnd
)는 실제로 편집을 제어하며 현재 예시의 WindowProc
은 WM_KEYDOWN
메시지에 대한 메시지 핸들러를 갖고 있습니다. 해당 코드는 SendMessage
로 전달된 세 번째 파라미터
(wParam
)를 확인하는데 유저의 입력값은 VK_RETURN
에 담겨있기 때문입니다.
(OS X에서) KeyDown
NSEvent가 애플리케이션으로 전달
인터럽트 신호는 I/O Kit kext 키보드 드라이버에 인터럽트 이벤트를 발생시킵니다. 이 드라이버는 해당
신호를 OS X의 WindowServer
프로세스로 전달되는 키코드로 변환합니다. 그 결과,WindowServer
는 적절한 상태(e.g. active, listening 등)의 애플리케이션에 이벤트 큐에 있는 Mach의 포트로 이벤트를 전달합니다. 이제 mach_ipc_dispatch
함수를 호출할 수 있는 적절한 권한을 가진 스레드는 메시지를 읽을 수 있습니다. 해당 과정은 NSApplication
이벤트 루프에 의해 NSEventType
의 KeyDown
NSEvent
를 통해 처리됩니다.
(GNU/Linux에서) Xorg 서버가 키코드를 listen.
그래픽이 제공되는 X 서버
를 사용하는 경우, X
는 이벤트 드라이버 evdev
를 통해 listen합니다. 키코드를 스캔코드로 다시 맵핑하는 과정은 X 서버
만의 키맵과 규칙을 따릅니다. 키 입력의 스캔코드 맵핑이 완료되면, X 서버
는 해당 문자를 윈도우 관리자
(DWM, metacity, i3 등)에 전달한 이후에, 윈도우 관리자
가 해당 문자를 사용 중인 애플리케이션에 전달합니다. 마지막으로 문자를 전달받은 애플리케이션에서는 그래픽 API를 통해 해당 문자를 원하는 위치에 출력합니다.
브라우저 레벨
URL 파싱
- 브라우저에는 다음과 같은 정보를 URL(Uniform Resource Locator)에 담고 있습니다:
프로토콜
"http"
'하이퍼 텍스트 전송 프로토콜'을 따릅니다.자원
"/"
메인 (인덱스) 페이지를 가져옵니다.
스트링 분석
프로토콜(http://)이나 유효한 도메인 이름(google "." com처럼 .(dot)을 입력하는 것)이 주어지지 않는 경우, 브라우저는 해당 스트링이 검색어인지 URL인지 알기 위해 주소창에 입력된 텍스트를 브라우저의 웹 검색엔진에 전달합니다. 대부분의 경우, 텍스트가 브라우저의 주소창에 입력되었다는 것을 검색엔진에 알려주기 위한 특수한 문자가 따라붙습니다.
호스트네임에서 non-ASCII 유니코드 문자열 변환
- 브라우저는 호스트네임에서
a-z
,A-Z
,0-9
,-
, 혹은.
아닌 문자가 있는지 확인합니다. - 현재 예시의 호스트네임인
google.com
에는 non-ASCII 유니코드가 없지만, 있는 경우에 브라우저가 URL에서 호스트명 부분에퍼니코드 (Punycode)
_ 인코딩을 하기도 합니다.
HSTS 확인
- 브라우저는 "캐싱된 HSTS (HTTP Strict Transport Security)" 를 확인합니다. HSTS는 HTTPS 연결만 허용하는 주소의 목록을 나타냅니다.
- 해당 도메인이 HSTS에 있는 경우, 브라우저는 요청을 HTTP 대신 HTTPS로 보내게 됩니다. 그렇지 않은 경우, 최초의 요청은 HTTP 프로토콜로 요청이 전송됩니다. (웹사이트가 HSTS 목록에 없더라도 여전히 HSTS 정책을 사용할 수 있습니다. 유저가 보내는 최초의 HTTP 요청은 HTTPS를 통해 보낼 경우에만 응답을 보내도록 하는 것처럼 할 수 있습니다. 하지만 최초의 요청만 HTTPS로 받게 처리하는 것은 유저를
다운그레이드 공격
에 취약하게 만들 수 있으므로 대부분의 현대 웹 브라우저에서는 HSTS를 캐싱하고 있습니다.
DNS 검색
- 브라우저는 도메인이 브라우저에 캐싱되어 있는지 먼저 확인합니다. (크롬에서 DNS 캐시를 확인하고자 한다면,
chrome://net-internals/#dns <chrome://net-internals/#dns>
). - 캐싱되어 있지 않다면, 브라우저는 DNS에 요청을 보내기 위해 (OS에 따라 상이하지만)
gethostbyname
라이브러리 함수를 호출합니다. gethostbyname
은 DNS에 요청을 보내기 이전에, 호스트명이 로컬의 (OS에 따라
다른 위치에 있는) hosts 파일에 도메인이 캐싱되어 있는지 확인합니다.gethostbyname
이 브라우저의 캐시와hosts
파일 두 경우 모두에서 호스트명을 찾지 못 하는 경우, 네트워크 스택에 설정되어 있는 DNS 서버로 요청을 보냅니다. 일반적인 경우 로컬 라우터(공유기)나 ISP에 캐싱되어 있는 DNS 서버로 요청하게 됩니다.- DNS 서버가 같은 서브넷에 존재하는 경우, 네트워크 라이브러리는 DNS 서버로
ARP 프로세스
를 통해 요청합니다. - DNS 서버가 다른 서브넷에 존재하는 경우, 네트워크 라이브러리는 기본 게이트웨이 IP로
ARP 프로세스
를 통해 요청합니다.
네트워크 레벨
ARP 프로세스
ARP (주소 결정 프로토콜, Address Resolution Protocol) 브로드캐스트를 전송하기 위해서는 네트워크 스택 라이브러리가 검색할 목적지 IP의 주소를 알아야 합니다. ARP 브로드캐스트를 전송하기 위해서는 목적지의 맥 어드레스 또한 알고 있어야 합니다.
가장 먼저 ARP 캐시에서 목적지 IP에 대한 맥 어드레스를 가지고 있는지 확인합니다. 만약 캐시에 존재한다면 라이브러리 함수는 다음의 형태로 결과를 리턴합니다: 목적지 IP = MAC.
목적지 IP에 대한 맥 어드레스가 ARP 캐시에 없는 경우:
- 라우팅 테이블을 검색하여 목적지 IP 주소가 로컬 라우팅 테이블의 서브넷에 존재하는지 확인합니다. 존재하는 경우, 라이브러리가 서브넷의 인터페이스를 이용합니다. 로컬에 존재하지 않는 경우, 로컬의 디폴트 게이트웨이를 통해 인터페이스를 이용합니다.
- 선택된 네트워크 인터페이스의 MAC 주소가 검색됩니다.
- 네트워크 라이브러리는 L2 (
OSI 모델
에서 데이터 링크 레이어)를 통해 ARP 요청을 보냅니다:
ARP Request
::
Sender MAC: interface:mac:address:here
Sender IP: interface.ip.goes.here
Target MAC: FF:FF:FF:FF:FF:FF (Broadcast)
Target IP: target.ip.goes.here
컴퓨터와 라우터 사이에 어떤 종류의 하드웨어가 존재하는지에 따라 달라지는데,
직접 연결시:
- 컴퓨터가 라우터에 직접 연결되어 있는 경우 라우터는
ARP Reply
를 응답합니다. (자세한 내용은 밑에서 다룹니다.)
허브:
- 컴퓨터가 라우터와 허브로 연결되어 있는 경우, 허브에 연결되어 있는 모든 포트에 ARP 요청을 브로드캐스트합니다. 라우터가 같은 "회선"내에 연결되어 있으면, 허브는
ARP Reply
를 응답합니다. (자세한 내용은 밑에서 다룹니다.)
스위치:
- 만약 컴퓨터가 라우터와 스위치로 연결되어 있는 경우, 스위치의 로컬 CAM/MAC 테이블을 확인하여 찾고자하는 MAC 주소가 있는지 확인합니다. 스위치에 해당 맥 어드레스가 존재하지 않는 경우 ARP 요청을 다시 브로드캐스트하게 됩니다.
- 스위치가 MAC/CAM 테이블에서 해당 주소를 찾는 경우, ARP 요청을 해당 맥 어드레스의 포트에 전송합니다.
- 라우터가 같은 "회선" 내에 있다면, 스위치는
ARP Reply
를 응답합니다. (자세한 내용은 밑에서 다룹니다.)
ARP Reply
::
Sender MAC: target:mac:address:here
Sender IP: target.ip.goes.here
Target MAC: interface:mac:address:here
Target IP: interface.ip.goes.here
네트워크 라이브러리에는 이제 DNS 서버에 대한 것이든 게이트웨이에 대한 것이든 IP 주소를 가지고 있으므로 DNS 요청을 재개하게 됩니다:
- 1023보다 큰 클라이언트의 포트를 이용하여 DNS 서버의 53번 포트의 UDP 소켓에 연결을 수립합니다.
- 응답 크기가 너무 큰 경우에는 UDP 대신 TCP가 사용됩니다.
- 로컬/ISP DNS 서버에 호스트네임에 대한 주소를 가지고 있지 않다면 재귀적으로 요청하며 DNS의 SOA(Start Of Authority)에 닿을 때까지 DNS의 계층 구조를 따라 올라가게 됩니다. 주소를 찾게 되는 경우 반환합니다.
소켓 연결 수립
위 과정을 통해 목적지 서버에 대한 IP 주소를 브라우저가 받은 이후에 IP 주소와 포트(HTTP의 경우 80, HTTPS의 경우 443)를 이용하여 시스템 라이브러리 함수(AF_INET/AF_INET6
와 SOCK_STREAM
)을 이용하여 TCP 소켓 스트림을 요청합니다.
- 해당 요청은 맨 처음에 L4를 통과하며 TCP 세그먼트가 생성됩니다. 목적지 포트가 헤더에 추가되며 출발지 포트는 커널의 동적인 포트 범위(Linux에서 ip_local_port_range 등)에서 결정됩니다.
- 해당 세그먼트는 L3에서 추가적인 IP 주소 헤더로 래핑됩니다. IP 주소에는 목적지 서버에 대한 내용뿐만 아니라 클라이언트 머신에 대한 내용도 포함되어 패킷이 생성됩니다.
- 패킷은 L2에서 로컬 라우터(게이트웨이)에 대한 맥 어드레스와 현재 머신의 NIC에 대한 맥 어드레스가 포함되며 프레임이 생성됩니다. 위에서 설명했던 것처럼 커널이 게이트웨이의 맥 어드레스를 모른다면 ARP 쿼리를 브로드캐스트하여 맥 어드레스를 찾는 것이 반드시 선행되어야 합니다.
이제 패킷은 다음과 같은 방법으로 전송될 수 있습니다:
이더넷
와이파이
셀룰러 데이터
대부분의 가정이나 소규모 사업에서의 인터넷 연결은 로컬 네트워크를 통해 컴퓨터를 나와서 모뎀(MOdulator/DEModulator)을 통해 아날로그 신호를 0과 1의 디지털 신호로 변환하여 핸드폰, 케이블, 무선 연결 등으로 송신할 수 있게 합니다. 연결을 받는 반대편에서는 디지털 신호를 아날로그 신호로 변환하는 모뎀이 존재하는데 이 모뎀을 통해 아날로그로 변환되어야 어디서 왔는지 어디에 도착해야 하는지 분석됩니다.
대부분의 큰 사업장이나 새로 지어지는 주거 공간에서는 광섬유나 이더넷 연결을 통해 인터넷과 연결되는데 이 경우 아날로그로 변환될 필요없이 디지털 신호를 그대로 주고 받게 됩니다.
결과적으로 로컬 서브넷에 의해 관리되는 라우터에까지 패킷이 전달됩니다. 게이트웨이에서부터 수많은 자율적 시스템(AS)의 라우터를 거치고 거치면서 목적지 서버에까지 도달하게 됩니다. 거쳐오는 모든 라우터에서는 패킷의 IP 헤더를 분석하여 목적지 주소를 알아낸 이후 적절한 다음 hop으로 라우팅을 시켜줍니다. IP 헤더의 time to live (TTL) 필드는 라우터를 한번 거칠 때마다 하나씩 감소하게 됩니다. 패킷은 TTL이 0이 되거나 현재 라우터의 큐가 꽉 차있는 경우(e.g. 네트워크 혼잡에 의한) 소멸하게 됩니다.
이러한 주고 받는 행위는 TCP 연결 흐름에 따라 여러 번 시행됩니다:
- 클라이언트에서 초기 순서 번호 (ISN, Initial Sequence Number)를 부여하고, ISN이 부여되었음을 나타내는 비트와 함께 SYN 패킷을 전송합니다.
- 서버에서 패킷을 받을 수 있는 상태라면 SYN를 받습니다:
- 서버에서는 ISN에 따라 패킷을 선택합니다
- 서버에서는 ISN에 따라 SYN 패킷을 받고 있다는 표시를 SYN에 합니다
- 서버에서는 ACK 필드에 (클라이언트에서 보낸 ISN + 1)을 복사한 후 ACK 플래그를 추가하여 패킷을 제대로 받았음을 표시합니다
- 클라이언트에서 패킷을 보낸 이후 연결된 것을 확인하면:
- ISN을 증가시킨다.
- 송신자의 ACK 숫자를 증가시킨다.
- ACK 필드를 추가한다.
- 데이터는 다음과 같이 전달된다:
- 한 쪽에서 N 데이터 바이트를 전달하면 그만큼 SEQ를 증가시킨다.
- 반대 쪽에서 패킷이 전달되었음을 확인하면, ACK 값을 상대방에서 받은 가장 마지막의 숫자와 같게 설정하여 ACK 패킷을 전송한다.
- 연결을 종료하기 위해서는:
- 닫는 쪽에서 FIN 패킷을 전송한다.
- 반대 쪽에서 FIN 패킷에 ACK하고 자신의 FIN 패킷을 전송한다.
- 닫는 쪽에서 반대 편의 ACK 된 FIN 패킷을 받는다.
TLS handshake
- 클라이언트 컴퓨터에서 자신의 Transport Layer Security (TLS) 버전, 사용 가능한 암호 알고리즘 목록 압축 방식을
ClientHello
메시지에 담아 서버로 전송합니다. - 서버는 클라이언트에게 TLS 버전, 선택한 암호 알고리즘, 선택한 압축 방식 그리고
CA (Certificate Authority)에 의해 인증된 서버의 공개 인증서를ServerHello
메시지에 담아
답장합니다. 공개 인증서에는 대칭키가 생성되기 전까지의 클라이언트가 handshake 과정을 암호화하는데 이용될 키가 포함되어 있습니다. - 클라이언트는 서버측 디지털 인증서가 유효한지 신뢰할 수 있는 CA 목록을 통해 검증합니다. 만약 CA를 통해 신뢰성이 확보되면, 클라이언트는 의사 난수 (pseudo-random) 바이트를 생성하고 이를 서버의 공개키를 이용하여 암호화합니다. 해당 난수 바이트는 대칭키 결정에 사용됩니다.
- 서버는 난수 바이트를 서버 개인 키로 복호화하고 이를 대칭 마스터키 생성할때 이용합니다.
- 클라이언트는
Finished
메시지를 서버에 보내면서, 지금까지의 전송 내역의 해시값을 대칭키로 암호화하여 포함시킵니다. - 서버에서도 여태까지의 전송 내역의 해시값을 생성하여 클라이언트의 값과 일치하는지 검증합니다. 일치하는 경우, 대칭키로 암호화한
Finished
메시지를 클라이언트에 전송합니다. - 이제부터 TLS 세션이 대칭키로 암호화된 어플리케이션 (HTTP) 데이터를 전송합니다.
패킷이 유실되는 경우
때때로, 네트워크 혼잡이나 하드웨어 연결의 불안정성 등으로 인해 TLS 패킷은 목적지에 도착하기 전에 유실되기도 합니다. 이럴 경우를 위해 전송자는 어떻게 대처해야할지 전략을 정해둡니다. 이런 전략을 위한 알고리즘은 TCP 혼잡 컨트롤이라고 불립니다. 이는 전송자마다 달라질 수 있는데 최신 OS를 사용하는 경우 cubic을 사용하고 이전의 OS는 New Reno를 이용하는 것이 일반적입니다.
- 클라이언트는 congestion window를 maximum segment size(MSS)를 기반으로 설정합니다.
- 각 패킷의 ACK마다, window를 'slow-start threshold'에 도달할 때까지 2배씩 증가시킵니다. 몇몇 구현체의 경우, 해당 한계는 동적일 수 있습니다.
- slow-start threshold에 도달한 이후에는 패킷의 ACK마다 조금씩 늘려나갑니다. 패킷이 유실되기 시작하면 window는 다른 패킷이 ACK 될 때까지 지수적으로 감소시킵니다.
HTTP 프로토콜
웹브라우저가 구글의 크로미움을 이용하는 경우라면 페이지를 요청하기 위해 HTTP 요청을 보내는 것 대신에, HTTP를 SPDY 프로토콜로 업그레이드하기 위해 시도하는 요청을 보냅니다.
클라이언트가 HTTP 프로토콜을 사용하고 SPDY를 지원하지 않는다면 다음과 같은 요청을 보낼 것입니다.
GET / HTTP/1.1
Host: google.com
Connection: close
[other headers]
[other headers]
부분은 HTTP 명세에 따라 콜론으로 구분되며 /n에 의해 구분되는 키 밸류 쌍이 나타나는 부분을 의미합니다. (사용하는 브라우저가 HTTP 명세를 벗어나는 어떠한 버그도 없을 때를 가정합니다.
또한 웹 브라우저가 HTTP/1.1
이라 가정하는데, 1.1이 아닌 경우에는 GET
의 헤더에 HTTP/1.0
혹은 HTTP/0.9
라 명시합니다.)
HTTP/1.1은 송신자측에서 응답을 받은 직후에 연결이 끊어질 것이라는 신호를 보내기 위해 "close"라는
연결 옵션을 정의합니다. 예를 들어,
Connection: close
영구적인 접속을 허용하지 않는 HTTP/1.1 어플리케이션은 반드시 "close" 연결 옵션을 모든 메시지에 포함해야합니다.
요청과 헤더를 보낸 후, 웹 브라우저는 하나의 빈 줄을 서버에 보내 요청 내용이 모두 보내졌음을
알립니다.
서버는 요청의 상태를 나타내는 코드와 응답 내용을 다음과 같은 형태로 응답합니다:
200 OK
[response headers]
빈 줄을 하나 붙인 뒤, www.google.com
의 HTML을 페이로드에 담아 보냅니다. 서버는 이후 연결을 끊거나, 클라이언트가 보낸 헤더에 연결을 유지해달라는 요청이 있었을 시에는 추가적인 요청을 위해 재사용될 수 있도록 연결을 유지해둡니다.
웹 브라우저에서 보낸 HTTP 헤더에, 마지막으로 보냈던 파일이 브라우저에 캐시되어 있고 그 뒤로 변하지
않았다는 판단을 내릴 만큼 충분한 정보 (e.g. 웹 브라우저가 ETag
헤더를 포함)가 담겨 있었다면, 아래와 같이 응답할 수 있습니다:
304 Not Modified
[response headers]
이 경우, 페이로드 없이 응답이 오고 웹 브라우저는 브라우저에 캐싱되어있는 HTML을 렌더링합니다.
HTML을 파싱한 후에는, 브라우저(와 서버)가 같은은 과정을 HTML 페이지에서 필요로하는 모든 자원(이미지, CSS, favicon.ico, etc)에 대해 반복합니다. 요청이 GET / HTTP/1.1
대신 GET /$(www.google.com에 관련된 URL) HTTP/1.1
으로 간다는 점만 다를 것입니다.
HTML이 www.google.com
이 아닌 도메인의 리소스를 참조하려 한다면, 브라우저가 도메인을 결정하는 단계 이전으로 되돌아가 해당 도메인에 대한 연결 수립 과정을 똑같이 반복합니다. 요청에 들어있는 Host
헤더는google.com
대신 해당 도메인이 들어갈 것입니다.
서버 사이드
HTTP 서버의 요청 처리
HTTPD (HTTP 데몬) 서버가 서버 사이드의 요청/응답을 처리합니다. 일반적인 HTTPD 서버로는 리눅스에서 사용되는 Apache, nginx와 윈도우에서 사용되는 IIS가 있습니다.
- HTTPD (HTTP 데몬)를 통해 요청을 받습니다.
- 서버는 요청을 다음 파라미터를 기준으로 분리합니다:
- HTTP 요청 메소드 (
GET
,HEAD
,POST
,PUT
,DELETE
,CONNECT
,OPTIONS
,TRACE
). 주소창에 URL을 직접 입력하였다면 GET 요청일 것입니다. - 도메인, (해당 예시에서는) google.com
- 요청된 경로/페이지, (해당 예시에서는) / (특정한 경로/페이지가 요청되지 않는 경우, / 가 기본 경로로 설정됩니다).
- HTTP 요청 메소드 (
- 서버는 google.com에 해당하는 가상 호스트가 서버에 설정되어 있는지 확인합니다.
- 서버는 google.com의 GET 요청을 받을 수 있게 허용되어 있는지 확인합니다.
- 서버는 클라이언트에서 해당 메서드를 보낼 수 있는지 확인합니다 (IP 주소, 인증 etc).
- 서버에 rewrite module이 설치되어 있는 경우(Apache의 mod_rewrite, IIS의 URL Rewrite), 받은 요청을 설정해둔 규칙에 맞는 확인하는 과정을 거칩니다. 해당하는 규칙이 존재하는 경우, 서버는 해당 규칙을 이용하여 요청을 재작성합니다.
- 서버는 요청에 해당하는 컨텐츠를 가지고 오는데, 해당 예시에서는 "/"에 요청을 보냈으므로 인덱스 파일(홈페이지)를 가져올 것입니다. (오버라이드 되어 홈페이지가 아닌 다른 페이지일 가능성도 있으나 일반적인 경우라는 가정하에)
- 서버는 핸들러를 통해 파일을 파싱합니다. 만약 구글에서 PHP로 서버를 운영한다면, PHP를 이용하여 인덱스 파일을 파싱하고 출력된 값을 클라이언트에게 보낼 것입니다.
클라이언트 사이드
브라우저 동작 과정
서버가 브라우저에 리소스(HTML, CSS, JS, 이미지 etc.)를 제공하면 브라우저는 아래의 프로세스를 통해 처리합니다:
- 파싱: HTML, CSS, JS
- 렌더링: DOM 트리 생성 → 트리 렌더링 → 렌더링 된 트리 배치 → 렌더링 된 트리 CSS 적용
브라우저
브라우저는 당신이 요청한 URI를 통해 리소스를 가져오고 브라우저에 렌더링하는 기능을 수행합니다. 리소스는 일반적으로 HTML 문서이지만 PDF, 이미지 등 다른 어떤 것도 될 수 있습니다. 리소스의 위치는 유저에 의해 지정된 URI에 존재합니다.
브라우저가 HTML 파일을 분석하고 렌더링하는 것은 HTML과 CSS 명세서에 따라 달라집니다. 해당 명세서는 웹 표준 기구인 W3C (World Wide Web Consortium) 기구에 의해 관리됩니다.
브라우저의 유저 인터페이스는 브라우저간에 대부분 비슷합니다. 인터페이스 기능으로는:
- URI 작성을 위한 주소창
- 뒤로가기 앞으로가기
- 즐겨찾기
- 페이지 새로고침, 렌더링 멈춤
- 홈페이지 바로가기
브라우저의 High Level Structure
브라우저의 구성요소로는:
- 유저 인터페이스: 유저 인터페이스는 주소창, 뒤로가기/앞으로가기, 즐겨찾기 등을 포함합니다. 당신이 요청한 페이지를 보는 부분을 제외한 브라우저의 모든 부분을 일컫습니다.
- 브라우저 엔진: 브라우저 엔진은 UI와 렌더링 엔진간의 모든 과정을 통제합니다.
- 렌더링 엔진: 렌더링 엔진은 요청된 내용을 화면에 나타내는 역할을 합니다. 예를 들어 요청된 내용 HTML이라면, 렌더링 엔진은 HTML과 CSS를 분석하고, 처리된 내용을 화면에 띄워줍니다.
- 네트워킹: 네트워킹은 HTTP와 같은 네트워크 요청을 파사드 패턴을 이용하여 처리합니다.
- UI 백엔드: UI 백엔드는 콤보박스나 기본 UI 틀과 같은 기본적인 요소를 그리는 데 쓰입니다. 이 백엔드 또한 파사드 패턴으로 되어있으며 내부적으로는 OS의 유저 인터페이스 메서드를 활용합니다.
- JavaScript 엔진: JavaScript 엔진은 JavaScript 코드를 분석하고 실행하는데 사용됩니다.
- 데이터 저장소: 데이터 저장소는 영속 계층입니다. 브라우저가 쿠키같은 갖가지 종류의 데이터를 저장할 수 있어야 하기 때문입니다. 브라우저는 localStorage와 IndexedDB, WebSQL, 파일시스템과 같은 저장 메커니즘을 지원합니다.
HTML 파싱
렌더링 엔진에서 L3에서 요청한 문서를 받아오는 것으로 시작합니다. 이 문서는 주로 8kB의 크기로 분할되어 있습니다.
HTML 파서의 최초 작업은 이 문서를 HTML 마크업을 파싱하여 "파스 트리"를 생성하는 것입니다.
파스 트리는 DOM(Document Object Model) 엘레멘트와 속성을 노드로 갖는 구조입니다. HTML의 객체를 프레젠테이션하며 HTML 엘레멘트를 JS 등을 이용하여 작동합니다. 파스 트리의 최상위 객체는 Document 객체입니다. 스크립트를 통해 조작되기 전까지는 DOM은 HTML 마크업과 거의 1:1로 대응합니다.
파싱 알고리즘
HTML은 일반적인 탑-다운이나 바텀-업 방식의 파서로는 파싱될 수 없습니다.
그 이유는 다음과 같습니다:
- 문법에 관용적인 언어의 태생적인 특성.
- 브라우저가 유효하지 않은 HTML들도 지원하기 위해 전통적인 에러를 용납해옴.
- 파싱 과정에 재진입할 수 있음. 다른 언어의 경우, 파싱하는 동안에는 소스 코드가 변경되지 않으나 HTML의 경우 (document.write()과 같은 스크립트를 통해) 동적으로 코드를 생성하여 새로운 요소를 삽입할 수 있고, 그렇기에 파싱 과정자체가 인풋을 수정시킬 수 있습니다.
위와 같은 이유로 일반적인 파싱 기술을 사용할 수 없으므로 브라우저는 HTML을 파싱하는데에 커스텀 파서를 사용합니다. 파싱 알고리즘에 대한 자세한 내용은 HTML5 명세서에 설명되어 있습니다.
알고리즘은 토큰화와 트리 생성, 2단계로 구성되어 있습니다.
파싱이 끝난 후의 동작
파싱이 끝난 후 브라우저는 페이지에 링크되어있는 외부 리소스(CSS, 이미지, JavaScript etc.)를 가져오기 시작합니다.
이 단계에서 브라우저는 해당 문서가 상호작용 중임을 표시하고 "deferred" 모드에 있는 스크립트를
파싱하기 시작합니다. (deferred는 문서 파싱이 완료되고 나서 실행되어야 하는 것들을 의미합니다.) 이후 문서의 상태는 "complete"가 되고 "load" 이벤트가 발생합니다.
HTML에서는 어떤 "문법적 오류"도 없음을 알아두세요. 브라우저가 어떠한 잘못된 문법이라도 직접 고치고 마지막까지 마무리지을 것입니다.
CSS 인터프리트
<style>
태그,style
속성, CSS 파일을"CSS lexical and syntax grammar"
를 이용해 파싱합니다.- 각각의 CSS 파일은
Stylesheet object
로 파싱되는데, 각각의 객체는 selector와 CSS 문법에 맞는 객체를 CSS 규칙에 포함하고 있습니다. - CSS 파서의 경우 특정한 파서 생성기가 사용되었을 경우에는 탑다운이나 바텀업으로 진행할 수 있습니다.
페이지 렌더링
- DOM 노드를 순회하며 프레임 트리나 렌더 트리를 생성하고 각 노드에 맞는 CSS 스타일 값을 계산합니다.
- 자식 노드간의 너비와 노드의 수평 margin, border, padding 등에서 선호하는 너비를 계산해내고 이를 이용하여 프레임 트리에서 각 노드간의 너비를 계산하는데 사용합니다. (바텀업)
- 각 노드에서 자식에게 할당 할 수 있는 너비를 계산하여 각 노드간의 실제 너비를 계산합니다. (탑다운)
- 텍스트 래핑과 자식 노드간의 margin, border, padding의 높이를 계산하여 각 노드간의 높이를 계산합니다.(바텀업)
- 위의 계산을 통해 얻어낸 정보를 통해 각 노드의 좌표를 계산합니다.
- 요소가
float
이거나,absolutely
혹은relatively
으로 속성이 되어있는 경우 좀 더 복잡한 과정이 필요합니다. http://dev.w3.org/csswg/css2/ 와 http://www.w3.org/Style/CSS/current-work에서 더 자세한 정보를 확인할 수 있습니다. - 어떤 부분이 다시 픽셀화 할 필요없이 그려질 수 있는지 그룹화하여 레이어를 생성합니다. 각각의 어떠한 프레임/렌더 객체든 하나의 레이어에 속하게 됩니다.
- 페이지의 각 레이어마다 텍스처가 적용됩니다.
- 각 레이어에 있는 프레임/렌더 객체를 순회하며 레이어마다 각각의 레이어를 그려내게 됩니다. 이는 CPU에 의해 픽셀화될 수 있거나 D2D/SkiaGL 등을 이용하여 GPU로 바로 그려낼 수 있습니다.
- 위의 모든 과정은 웹페이지가 렌더링됐을 때의 계산된 마지막 값을 재사용할 수 있으므로 추가 작업에는 더 적은 작업량을 필요로 할 것입니다.
- 크롬과 같은 브라우저의 경우, iframe이나 addon 패널 등을 그려내는데 필요한 요소들이 있는 페이지 레이어는 합성 프로세스로 보내집니다.
- 최종적인 레이어의 위치는 Direct3D/OpenGL 등에 의해 계산되고 구성되게 됩니다. GPU 명령 버퍼가 GPU로 flush되어 비동기적인 렌더링을 하게 되고 그 결과인 프레임을 윈도우 서버에 전송합니다.
GPU 렌더링
- 렌더링 과정의 그래픽 연산 레이어에서는 일반적인
CPU
나 그래픽 프로세서인GPU
모두 사용 가능합니다. GPU
를 그래픽 렌더링 연산에 사용할 경우에는 그래픽 소프트웨어 레이어에서는 작업을 여러 개로 분할하여,GPU
의 병렬구조를 이용하여 렌더링 과정에서 필요로 하는 부동 소수점 연산을 빠르게 할 수 있습니다.
윈도우 서버
렌더링 후처리와 사용자에 의해 유도된 동작
렌더링이 끝나고나서, 브라우저는 JS 코드를 이용하여 (Google Doodle animation과 같은) 타이밍 메커니즘을 실행하기도 하고 유저와의 상호작용(검색창을 통해 쿼리를 요청하고 단어에 대한 제안을 받는 등)하는 결과를 결과를 내놓는다.
Flash나 자바와 같은 플러그인이 실행되기도 하지만 현재 기준으로 구글 홈페이지에서는 실행되지 않는다. 스크립트는 추가적인 네트워크 작업이나 페이지를 수정하고 레이아웃을 변경하는 등의 또 다시 요청을 처음부터 실행하게 하는 결과로 이어질 수 있다.
레퍼런스
https://github.com/alex/what-happens-when
'CS' 카테고리의 다른 글
트랜잭션 격리 수준 (0) | 2022.08.25 |
---|---|
배치나 스케줄러와 같은 cron daemon은 내부적으로 어떻게 동작할까? (0) | 2022.08.16 |
블로킹/동기 vs 논블로킹/비동기 (0) | 2022.08.06 |
왜 많은 DB의 인덱싱은 hashtable이 아닌 b-tree로 구현되어 있을까? (0) | 2022.08.04 |
프로세스와 스레드의 차이 (0) | 2022.08.02 |
- Total
- Today
- Yesterday
- RequestBody
- 배포
- RequestParam
- vim
- ModelAttribute
- lunarvim
- 도커
- 레디스
- 아키텍처
- IDE
- RequestPart
- Dap
- 루나빔
- neovim
- JavaScript
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |