Как стига страница до теб
Проблем
Между Enter и страницата
Пишеш в браузъра shop.example.bg, натискаш Enter и след половин секунда виждаш каталога с кафе и чай. Страницата е на компютър в друг град, а може би в друга държава. Как браузърът го намира? Как разбира, че става дума за каталога, а не за количката? Какво точно си казват двете програми?
В следващия урок ще напишеш сървър, който отговаря на браузъра. За да го направиш, трябва да знаеш езика, на който си говорят.
Идеята
Пакети, рутери и IP адреси
Интернет е мрежа от мрежи: домашният ти рутер, мрежата на доставчика, мрежите на държави и центрове за данни, свързани помежду си.
- Данните не пътуват на едно парче. Делят се на пакети, малки части от по около 1 500 байта. Всеки пакет носи адреса, за който е.
- Рутерът е устройство, което получава пакет и го праща към следващата мрежа по пътя. Пакетът минава през 10–20 рутера, всеки го доближава до целта.
- IP адресът е номерът на устройство в мрежата, например
93.184.215.14. Твоят компютър винаги може да се наречеlocalhost(127.0.0.1). - Портът е номер от 0 до 65 535 за програма на този компютър. IP адресът е сградата, портът е апартаментът. Уеб сървърите обикновено слушат на 80 (HTTP) и 443 (HTTPS).
Правилата, по които се говори, се казват протоколи. IP доставя отделните пакети, но не обещава, че ще стигнат. TCP стъпва върху IP: прави връзка между две програми, номерира пакетите, подрежда ги и праща наново загубените. Затова двете заедно се казват TCP/IP.
По-точно, за любопитните
- Адресите от вида
93.184.215.14са IPv4: четири числа от 0 до 255, около 4 милиарда адреса. Те не стигат, затова има и IPv6:2606:2800:21f:cb07:6820:80da:af6b:8b2c. - Освен TCP има и UDP: без връзка и без препращане на загубеното, но по-бърз. Ползва се за глас и видео на живо и за игри. Уебът е върху TCP.
- Двата края на една TCP връзка се казват сокети: адрес и порт от всяка страна. Портът на браузъра се избира сам, от свободните.
Идеята
Име вместо номер: DNS
Хората помнят имена, компютрите търсят по IP адрес. Домейнът е човешкото име на сайта: shop.example.bg. DNS (Domain Name System) превръща името в адрес:
- Браузърът пита DNS сървъра на доставчика: „кой е адресът на
shop.example.bg?“ - Ако той не го знае, пита по веригата: сървъра за
.bg, после сървъра наexample.bg. - Отговорът е IP адрес. Браузърът го запомня за известно време и следващия път не пита.
Чак тогава браузърът се свързва по TCP с този адрес, на порт 443.
Целият път на една страница, с частта от адреса, която ползва всяка стъпка:
Uri address = new Uri("https://shop.example.bg/products?category=coffee");Console.WriteLine("1. DNS: кой е адресът на " + address.Host + "?");Console.WriteLine("2. TCP: връзка до порт " + address.Port);Console.WriteLine("3. HTTP: GET " + address.PathAndQuery);Стъпка по стъпка, с думи
- Адресът казва всичко нужно: по кой протокол, до кой компютър и кой ресурс.
- Първо името: браузърът пита DNS кой е IP адресът на shop.example.bg.
- DNS отговаря с IP адрес, например 93.184.215.14. Браузърът го запомня.
- TCP връзка с този адрес. Портът не е в адреса, затова е 443, този на https.
- По връзката отива HTTP заявка: метод GET и пътят /products?category=coffee.
- Сървърът отговаря: статус 200 OK и HTML. Браузърът рисува страницата.
- Три стъпки: име в адрес (DNS), връзка (TCP), заявка и отговор (HTTP).
Хостингът е компютърът, на който живее сайтът: сървър в център за данни, винаги включен и свързан. Домейнът сочи хостинга: в DNS пише „shop.example.bg е на този IP адрес“.
По-точно, за любопитните
- Домейн се регистрира срещу годишна такса през регистратор: фирма, акредитирана за съответния край (
.bg,.com,.eu). Регистрацията е за определен срок, обикновено година, и се подновява. Ако не я подновиш, името се освобождава и всеки може да го вземе. Добрият домейн е кратък, лесен за запомняне и казва за какво е сайтът. - Хостингът има няколко вида. Споделен: много сайтове на един сървър, евтин, но съседите ти влияят. VPS (виртуален частен сървър): твоя част от сървър с гарантирани ресурси. Облак: плащаш колкото ползваш и растеш при нужда. Собствен (dedicated) сървър: цяла машина само за теб. Изборът зависи от посещенията, от нужните ресурси и от бюджета.
Виж го
Адресът и частите му
Адресът на страница се казва URL (Uniform Resource Locator). Той казва всичко нужно, за да се стигне до нея. В документацията ще срещнеш и по-общото име URI (Uniform Resource Identifier), откъдето идва и името на класа. В C# частите му дава класът Uri:
Uri address = new Uri("https://shop.example.bg:8443/products?category=coffee&page=2");Console.WriteLine("Схема: " + address.Scheme);Console.WriteLine("Хост: " + address.Host);Console.WriteLine("Порт: " + address.Port);Console.WriteLine("Път: " + address.AbsolutePath);Console.WriteLine("Заявка: " + address.Query);
Uri home = new Uri("https://shop.example.bg/");Console.WriteLine("Порт по подразбиране: " + home.Port);Схема: httpsХост: shop.example.bgПорт: 8443Път: /productsЗаявка: ?category=coffee&page=2Порт по подразбиране: 443- Схема (
https): по кой протокол. - Хост (
shop.example.bg): кой компютър. Името минава през DNS. - Порт (
8443): коя програма на него. Ако го няма в адреса, е този по подразбиране за схемата: 443 заhttps, 80 заhttp. - Път (
/products): кой ресурс на сървъра. - Заявка (query,
?category=coffee&page=2): допълнителни параметри като двойкииме=стойност, разделени с&.
Какво ще отпечата програмата?
Uri cart = new Uri("http://shop.example.bg/cart?step=2");Console.WriteLine(cart.Port + " " + cart.AbsolutePath);Схемата е http, а портът не е написан, затова е 80. AbsolutePath е само пътят, без заявката след ?.
По-точно, за любопитните
- В адреса може да има само латински букви, цифри и няколко знака. Всичко друго, кирилица и интервали, се записва като байтове в UTF-8 с
%отпред. Браузърът ти показва „зелен чай“, но по мрежата отива кодираното:
Uri search = new Uri("https://shop.example.bg/search?q=зелен чай");Console.WriteLine(search.AbsoluteUri);Console.WriteLine(Uri.UnescapeDataString(search.Query));https://shop.example.bg/search?q=%D0%B7%D0%B5%D0%BB%D0%B5%D0%BD%20%D1%87%D0%B0%D0%B9?q=зелен чай- След пътя може да има и фрагмент:
#reviews. Той не стига до сървъра: браузърът само превърта страницата до тази част.
Идеята
HTTP заявка
Браузърът се е свързал. Сега праща HTTP заявка (HyperText Transfer Protocol). Тя е текст:
GET /products?category=coffee HTTP/1.1Host: shop.example.bgAccept: text/htmlUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/131.0- Първият ред е метод, път и версия. Методът казва какво искаш:
GETе „дай ми“,POSTе „ето ти данни“ (поръчка, форма). Има иPUT(замени),DELETE(изтрий) и други. - После идват заглавките (headers), по една на ред:
име: стойност.Hostказва кой сайт искаш, защото на един сървър може да има много сайтове.Acceptказва какво разбира браузърът,User-Agentкой е той. - Празен ред завършва заглавките. След него може да има тяло: данните на заявката.
GETняма тяло,POSTобикновено има:
POST /cart HTTP/1.1Host: shop.example.bgContent-Type: application/x-www-form-urlencodedContent-Length: 19
product=C-101&qty=2Content-Length казва колко байта е тялото, за да знае сървърът кога е получил всичко.
Идеята
HTTP отговор
Сървърът отговаря със същия вид текст. Първият ред носи статус: число и кратко обяснение. Първата цифра казва вида:
| Статус | Вид | Пример |
|---|---|---|
| 2xx | успех | 200 OK: ето каквото поиска |
| 3xx | пренасочване | 301 Moved Permanently: вече е на друг адрес |
| 4xx | грешка в заявката | 404 Not Found: няма такова нещо |
| 5xx | грешка на сървъра | 500 Internal Server Error: програмата на сървъра се провали |
После са заглавките на отговора и тялото: HTML на страницата, JSON с данни, картинка.
HTTP/1.1 404 Not FoundContent-Type: text/html; charset=utf-8Content-Length: 43
<h1>Няма такъв продукт</h1>Content-Type казва какво е тялото и в каква кодировка. При пренасочване заглавката Location казва новия адрес, а браузърът сам праща нова заявка натам:
HTTP/1.1 301 Moved PermanentlyLocation: https://shop.example.bg/productsПо-точно, за любопитните
- HTTP няма памет (stateless): всяка заявка е отделна и сървърът не помни предишните. Затова, за да те „помни“ в количката, сайтът ти дава бисквитка (cookie): заглавка, която браузърът праща обратно с всяка следваща заявка.
- Същата липса на памет се използва при DDoS атака през HTTP: хиляди заразени компютри пращат огромен брой заявки към един сървър. Всяка изглежда като обикновена и сървърът трябва да я обработи, докато не спре да смогва и за истинските клиенти. Защитата е пред сървъра: услуги, които филтрират съмнителния трафик и ограничават броя заявки от един адрес.
Виж го
Отговорът, разглобен с код
Отговорът е просто текст, затова можеш да го разглобиш с методите на текста. Краят на всеки ред в HTTP е \r\n, а празният ред между заглавките и тялото е \r\n\r\n:
string response = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html; charset=utf-8\r\n" + "Content-Length: 27\r\n" + "\r\n" + "<h1>Кафе и чай</h1>";
string[] parts = response.Split("\r\n\r\n");string[] lines = parts[0].Split("\r\n");string[] status = lines[0].Split(' ', 3);Console.WriteLine("Версия: " + status[0]);Console.WriteLine("Статус: " + status[1] + " (" + status[2] + ")");
Dictionary<string, string> headers = new Dictionary<string, string>();for (int i = 1; i < lines.Length; i++){ string[] header = lines[i].Split(": ", 2); headers[header[0]] = header[1];}Console.WriteLine("Тип: " + headers["Content-Type"]);Console.WriteLine("Дължина: " + headers["Content-Length"] + " байта");Console.WriteLine("Тяло: " + parts[1]);Версия: HTTP/1.1Статус: 200 (OK)Тип: text/html; charset=utf-8Дължина: 27 байтаТяло: <h1>Кафе и чай</h1>Split("\r\n\r\n")дели на две: заглавна част и тяло.- Заглавната част се дели на редове. Първият е статусът.
Split(' ', 3)дава най-много 3 части: така при404 Not Foundобяснението остава цяло. - Заглавките влизат в речник: име → стойност.
Split(": ", 2)дели само на първото:.
Така работи всяка програма, която говори HTTP. В следващия урок това ще прави HttpClient вместо теб.
По-точно, за любопитните
Content-Lengthе 27 байта, а текстът е 19 знака. По мрежата отиват байтове в UTF-8, а там всяка кирилска буква е 2 байта:
string body = "<h1>Кафе и чай</h1>";Console.WriteLine(body.Length + " знака");Console.WriteLine(System.Text.Encoding.UTF8.GetByteCount(body) + " байта");19 знака27 байта- Имената на заглавките не зависят от главни и малки букви:
content-typeиContent-Typeса едно и също. Речник, който сравнява така, се прави сnew Dictionary<string, string>(StringComparer.OrdinalIgnoreCase). - HTTP/1.1 е текст. По-новите HTTP/2 и HTTP/3 пращат същите методи, заглавки и статуси, но в двоичен вид и с много заявки по една връзка. За програмата ти смисълът е същият.
Пробвай
Заглавка с двоеточие
Програмата трябва да отпечата името и стойността на заглавката, но вместо адреса печата само https. На кой ред е грешката?
Избери реда с грешката.
Split(':') реже на всяко двоеточие, а в адреса има още едно, след https. Частите стават три: Location, https и //shop.example.bg/products. Затова се дели само на първото: line.Split(": ", 2).
Идеята
HTTPS: същото, но заключено
HTTP е обикновен текст. Всеки рутер по пътя може да прочете паролата ти или да подмени цената. HTTPS е HTTP през TLS (по-старото име е SSL):
- Ръкостискане (handshake). Браузърът и сървърът се договарят как да криптират и си разменят ключове.
- Сертификат. Сървърът показва сертификат, подписан от организация, на която браузърът вярва. Така знаеш, че говориш с истинския
shop.example.bg, а не с някой по средата. - Криптиране. Оттам нататък заявките и отговорите пътуват криптирани. По пътя се вижда до кой сървър отиваш, но не и какво си казвате.
Методите, заглавките и статусите са същите като при HTTP. Катинарчето в адресната лента значи, че връзката е по HTTPS.
Идеята
Какво прави браузърът
Първият отговор е HTML: текстът и структурата на страницата. Браузърът го чете и в него намира още ресурси: стилове (CSS), програми (JavaScript), картинки, шрифтове. За всеки праща нова заявка. Обикновена страница прави десетки заявки, преди да е готова. Затова сайт с много тежки картинки се зарежда бавно, дори HTML-ът да е малък.
После браузърът подрежда всичко и го рисува на екрана. JavaScript може да промени страницата и след това, без да я зарежда наново.
По-точно, за любопитните
- Браузърът има няколко части. Потребителският интерфейс: адресната лента, бутоните, отметките, всичко освен самата страница. Двигателят на браузъра свързва интерфейса с двигателя за изобразяване, който разбира HTML и CSS и рисува. Мрежовата част прави HTTP заявките. Слой за рисуване на основни контроли (падащи списъци, прозорци), общ за всички операционни системи. Интерпретаторът на JavaScript изпълнява скриптовете. Хранилището пази бисквитки и данни на сайтовете (
localStorage, IndexedDB). - Как се рисува: от HTML се строи дърво на елементите (DOM), от CSS дърво на стиловете (CSSOM). Двете заедно дават дървото за изобразяване (render tree). После се смята къде е всеки елемент и колко е голям (layout), и накрая се рисува (paint). JavaScript променя DOM и браузърът изобразява наново само засегнатото.
- За повече: Как работи интернет, Какво е HTTP, Как работят браузърите и DDoS атака с HTTP заявки (на английски).
Упражнение
Адреси, статуси и заявки
Реши задачите в редактора си.
1. Части на адрес. За https://shop.example.bg/search?q=espresso&max=30&page=2 отпечатай хоста, порта и пътя. После раздели заявката на двойки в речник (TrimStart('?') маха въпросителния знак, после Split('&') и Split('=')), отпечатай ги и вземи номера на страницата като число:
Хост: shop.example.bg, порт 443Път: /searchq = espressomax = 30page = 2Страница: 22. Какво значи статусът. Метод Meaning(int code) със switch връща смисъла на известните кодове 200, 201, 301, 404 и 500. За другите казва вида по първата цифра (code / 100), а за непознат вид връща „непознат код“:
200: успех201: създадено301: преместено завинаги404: няма такъв ресурс418: грешка в заявката500: грешка на сървъра503: друга грешка на сървъра3. Сглоби заявка. Метод BuildGet(Uri address) връща текста на GET заявка с Host и Accept: text/html, с \r\n в края на всеки ред и празен ред накрая. address.PathAndQuery дава пътя заедно със заявката. Отпечатай я, като замениш всяко \r\n с видимия текст \r\n и нов ред:
GET /products?category=tea HTTP/1.1\r\nHost: shop.example.bg\r\nAccept: text/html\r\n\r\nТест