kleindan.dev

HTTP czyli podstawowy budulec aplikacji internetowych

Protokoły sieciowe są standaryzowane przez organizację Interner Engineering Task Force (IETF) w dokumentach o nazwie Request For Comments (RFC) Obecnie obowiązujący standard dla komunikacji HTTP w wersji 1.1 to RFC 9112 w połączeniu z RFC 9110 oraz RFC 9111

Z jednej strony protokół HTTP na pierwszy rzut oka wydaje się przerażający, jeżeli zaczniemy od czytania powyższych trzech dokumentów. Jednak pierwsze specyfikacje nie były tak rozległe, a nawet w nich część rozwiązań była bardzo rzadko używana. Stąd u niektórych przekonanie, że HTTP jest prostym protokołem, ale jak twórca narzędzia curl pisze w swoim artykule pozory mylą.

Ćwiczenie 1:

  1. Kiedy opublikowana została najnowsza wersja protokołu HTTP/1.1?
  2. Kiedy opublikowana została poprzednia wersja tego protokołu?

Co w URI piszczy, czyli czym jest adres wpisany do przeglądarki?

URI czyli Unified Resource Identifier to kolejny termin zdefiniowany przez jeszcze inny dokument RFC, jednak idźmy już dalej tą ścieżką. Z punktu widzenia aplikacji internetowych URI to po prostu link, który pozwala nam wejść na interesującą nas stronę.

Próba przeczytania w całości dowolnego dokumentu RFC szybko kończy się kilkoma otwartymi zakładkami w przeglądarce i uczuciem, że zaraz zacznie boleć nas głowa. Warto takie ćwiczenie wykonać na jakimś w miarę niedużym dokumencie. Po przeczytaniu kilku z nich można nabrać pewnej wprawy i zacząć rozróżniać informacje znaczące od pomijalnych.

Przykładowy nietrywialny adres internetowy może wyglądać na przykład tak:

http://www.example.com/path/to/resource?min=10&max=20#my-favorite-paragraph

Dzieląc go na poszczególne elementy, mamy kolejno:

Ściśle rzecz ujmując fragment nie wchodzi w skład protokołu HTTP. Po wpisaniu w przeglądarkę adresu z fragmentem przeglądarka nie przesyła go do serwera jako część żądania HTTP. Za chwilę to sprawdzimy.

Metody, czasowniki czyli czego chcemy od serwera?

O ile URI odpowiada na pytanie co? o tyle drugą niezbędną częścią żądania HTTP jest odpowiedź na pytanie jak?. Korzystając z internetu nie tylko pobieramy informacje, ale też wysyłamy nowe treści, edytujemy te istniejące albo kasujemy nieaktualne. Wszystkie te działania mają odzwierciedlenie w HTTP. Protokół definiuje tzw. metody albo potocznie po angielsku verbs. Najczęściej używane z nich to:

Prócz tych istnieją jeszcze PUT, PATCH, HEAD, OPTIONS oraz wiele innych, o których wielu programistów nawet nie słyszało. To nie jest przesadzone stwierdzenie.

Statusy HTTP

Serwer HTTP po otrzymaniu żądania o dany zasób powinien się do tego żądania jakoś ustosunkować. Jeżeli serwer jest w stanie spełnić żądanie, odeśle klientowi treść danego zasobu, czyli np. stronę HTML, zdjęcie, plik CSS, etc., ale poprzedzi tę informację tzw. status code, albo po prostu statusem.

Statusy to 3-cyfrowe kody, które dzielą się na 5 głównych grup, pogrupowanych po pierwszej cyfrze:

Na pewno w swoim korzystaniu spotkaliście się z kodem 404 oznaczającym nieznalezioną stronę, to typowy przykład błędu z rodziny 4xx. W przypadku poprawnie sformułowanego żądania obsłużonego z powodzeniem serwer powinien odpowiedzieć kodem 200.

Pełną listę statusów możecie znaleźć na stronie IANA

Nagłówki HTTP

Zarówno klient kontaktujący się z serwerem jak i serwer odpowiadający na żądanie mogą chcieć dodać do swoich wiadomości dodatkowe informacje. Najczęściej są one dołączane do wiadomości jako tzw. nagłówki HTTP, które są po prostu parami w formie Nazwa: Wartość.

Lista zdefiniowanych przez standardy nagłówków jest długa. W tej lekcji na pewno spotkamy się z nagłówkiem Content-Type, który jest używany przez serwer do informowania klienta (zazwyczaj przeglądarki) o tym jak należy interpretować treść odpowiedzi, tj. czy jest to dokument HTML, zwykły tekst, czy np. dane w formacie JSON.

Pierwszy program Node.js

Korzystając ze swojego środowiska programistycznego, otwórz terminal i uruchom komendy:

mkdir first-server
cd first-server
npm init --yes
touch index.js

Objaśnienia:

W ten sposób zainicjalizujemy strukturę naszego pierwszego projektu Node.js. W tym samym oknie terminala możesz wpisać następującą komendę, aby otworzyć nowo utworzony folder w Code

code -r .

Najpierw zaczniemy od wprowadzenia drobnej modyfikacji do naszego pliku konfiguracyjnego w package.json.

> package.json
 1{
 2  "name": "first-server",
 3  "version": "1.0.0",
 4  "description": "",
 5  "main": "index.js",
 6  "type": "module",
 7  "scripts": {
 8    "test": "echo \"Error: no test specified\" && exit 1"
 9  },
10  "author": "",
11  "license": "ISC"
12}

Dodanie klucza "type": "module" pozwoli nam na importowanie modułów w plikach w sposób zgodny ze współczesnym JavaScriptem.

Tradycyjnie pierwszy program w nowym języku programowania to Hello World, więc zacznijmy od tego. Dopisz następujący kod do index.js

> index.js
1console.log("Hello world!");

A następnie w terminalu uruchom komendę:

node index.js

Gratulacje, oto Twój pierwszy program w Node.js! Ale nie zatrzymujmy się tutaj, pójdźmy o krok dalej.

Pierwszy serwer HTTP w Node.js

Node.js jest środowiskiem uruchomieniowym dla języka JavaScript, ale oprócz tego, zapewnia także bibliotekę narzędzi pozwalających np. czytać i zapisywać informacje do plików na dysku, uruchamiać proste serwery sieciowe, czy mierzyć wydajność naszych aplikacji.

W przypadku postawienia serwera WWW zaczniemy tego kawałka kodu. Przepisz go do swojego edytora.

> index.js
 1import { createServer } from 'node:http';
 2
 3// Create a HTTP server
 4const server = createServer((req, res) => {
 5  res.writeHead(200, { 'Content-Type': 'text/plain' });
 6  res.end('hello world!');
 7});
 8
 9const port = 8000;
10const host = "localhost";
11
12// Start the server
13server.listen(port, host, () => {
14    console.log(`Server listening on http://${host}:${port}`);
15});

Dlaczego napisałem o przepisywaniu kodu, a nie o wklejeniu go do swojego edytora? O ile doceniam ułatwianie sobie życia, o tyle na etapie uczenia się nowych rzeczy jak języków programowania czy frameworków, dobrze jest dać sobie trochę czasu na poznanie podstaw nowych technologii. W przeciwnym wypadku kiedy przyjdzie nam napisać cokolwiek bez dostępu do internetu lub innych zewnętrznych pomocy, może się okazać, że nie potrafimy się posługiwać własnymi narzędziami.

Powyższy kawałek kodu ma zaledwie kilkanaście linii, a jednak dużo się w nim wydarzyło. Przejdźmy przez niego powoli.

Importowanie zewnętrznych modułów

import { createServer } from 'node:http';

Aby móc skorzystać z jakiejkolwiek funkcjonalności z poza naszego pliku z kodem, musimy zaimportować coś z innego modułu, tj. pliku. Node.js zawiera różne moduły, w tym wypadku chcemy zaimportować funkcję createServer z modułu http z kolekcji modułów z Node.js. Funkcja, jak nietrudno zgadnąć, służy do stworzenia serwera HTTP.

Taka forma importowania to tzw. nazwany import (ang. named import). Alternatywnie moglibyśmy zaimportować obiekt domyślnie eksportowany przez moduł node:http używając tzw. domyślnego importu (ang default import), wtedy wywołanie funkcji createServer wyglądałoby następująco:

import http from 'node:http';

const server = http.createServer();
// ...

Więcej informacji o importowaniu z innych modułów znajdziecie pod tym linkiem do dokumentacji języka JavaScript.

Dokumentacja, szukanie informacji i podpowiadanie składni

Powyżej znajduje się link do dokumentacji języka JavaScript. Dokumentacja języka bądź narzędzia z którego korzystamy zazwyczaj dostarczy nam najpełniejszych odpowiedzi na temat tego, jak działają nasze narzędzia. Oto krótkie podsumowanie, gdzie szukać wartościowych informacji:

Domyślnie edytor VS Code stara się nam podpowiadać jak może, jednak czasem musimy mu lekko pomóc. Jeżeli Code nie podpowiada nic na temat funkcji createServer, wykonaj następującą komendę w swoim terminalu:

npm install --save-dev @types/node

Definiowanie funkcji w JavaScript

Prosty przykład definicji własnej funkcji i wywołania jej może wyglądać w JavaScript w ten sposób:

// definicja funkcji myFunction
function myFunction() {
  console.log("hello!");
}

// wywołanie funkcji myFunction
myFunction();

Nieco inaczej możemy zdefiniować funkcję np. w ten sposób:

// definicja anonimowej funkcji i przypisanie jej do stałej myFunction
const myFunction = function () {
  console.log("hello!");
}

// wywołanie funkcji wskazywanej przez myFunction
myFunction();

Są subtelne różnice w tych sposobach, ale ostatecznie wynik działania programu jest ten sam.

Obecnie dużo częściej wykorzystywaną składnią są tzw. arrow functions. Składnia ta wygląda następująco:

// definicja anonimowej funkcji i przypisanie jej do stałej myFunction
const myFunction = () => {
  console.log("hello!");
}

// wywołanie funkcji wskazywanej przez myFunction
myFunction();

Funkcje mogą przyjmować też argumenty, które zmieniają działanie funkcji. Aby tak się stało, musimy zdefiniować funkcję razem z parametrami, np. tak jak w poniższym przykładzie:

// definicja funkcji myFunction
function myFunction(name, age) {
  console.log("hello! My name is", name, "and I am", age, "years old");
}

// wywołanie funkcji myFunction
myFunction("Douglas", 42);

Używając składni arrow function, ta definicja może wyglądać w ten sposób:

// definicja funkcji myFunction
const myFunction = (name, age) => {
  console.log("hello! My name is", name, "and I am", age, "years old");
}

Funkcje mogą być po prostu zbiorem instrukcji, ale mogą też zwracać przetworzoną wartość. Dodawanie to może niezbyt praktyczny przykład, ale użyjemy go ze względu na prostotę:

// definicja funkcji add
function add(a, b) {
  return a+b;
}

// wywołanie funkcji myFunction
console.log("2 + 3 = ", add(2, 3));

Jak widać wywołanie funkcji może być też użyte jako argument innej funkcji.

Szablony ciągów znaków

Sporą częścią naszych programów będzie przetwarzanie tekstu. JavaScript pozwala na tworzenie szablonów tekstowych, ang. template literals, które pozwalają na umieszczanie wartości zmiennych z kodu w wynikowym łańcuchu znaków. Same szablony umieszczamy pomiędzy dwoma znakami backtick, czyli `, natomiast w środku możemy umieszczać znaczniki ${}, gdzie pomiędzy klamrami możemy wstawiać nazwy zmiennych bądź wywołania funkcji, których wartości pojawią się jako fragment tekstu.

Tak wygląda przykładowe zastosowanie:

const item = "sword";
const weight = "3 lb";

console.log(`You found a ${item}, it weighs ${weight}. Take it?`);

function pokedex_entry(name, elements) {
  return `Pokemon name: ${name} \nElements: ${elements}`;
}

console.log(pokedex_entry("Charmander", "Fire"));

Kombinacja znaków \n wstawia w ciąg znaków znak nowej linii.

Przekazywanie funkcji jako argumenty do innych funkcji

W języku JavaScript funkcje są tzw. obywatelami pierwszej kategorii. To znaczy, że są traktowane dokładnie tak samo jak wszystkie inne elementy w języku, np. zmienne i stałe. Wszędzie tam, gdzie możemy stworzyć i użyć zmiennej, tam powinniśmy móc stworzyć i użyć funkcji. To oznacza, że możemy też przekazać funkcję jako argument do innej funkcji.

Istotne rozróżnienie: przekazanie funkcji jako argumentu to nie to samo, co wywołanie funkcji i przekazanie jej zwrotu jako argumentu. Przejdźmy przez kilka przykładów:

function make_introduction_en(name, age) {
  return `My name is ${name} and I am ${age} years old`;
}

function make_introduction_pl(name, age) {
  return `Mam na imię ${name} i mam ${age} lata`;
}

function greet(greeting, make_introduction) {
    console.log(greeting, make_introduction("Douglas", 42));
}

greet("Hello!", make_introduction_en);
greet("Cześć!", make_introduction_pl);

Ok, czyli możemy w jednej funkcji wywoływać inną funkcję przekazaną jako argument. Spójrzmy na kolejny przykład, tym razem dodając mały zwrot akcji. Jeżeli chcemy przekazać funkcję jako argument, to nie musimy jej wcześniej definiować w innym miejscu. Możemy ją zdefiniować w miejscu wywołania funkcji, do której chcemy przekazać tę nową.

function transform_number_and_print(number, transfromer) {
  console.log(transfromer(number));
}

function add_2(x) {
  return 2 + x;
}

const multiply_by_5 = (x) => {
  return x * 5;
};

transform_number_and_print(5, add_2);
transform_number_and_print(5, multiply_by_5);
transform_number_and_print(5, (x) => {
  return x + 3;
});
transform_number_and_print(5, function (x) {
  return add_2(multiply_by_5(x));
});

Tworzenie serwera HTTP Node.js

Po tym wszystkim możemy przejść do wyjaśnienia, co się dokładnie dzieje w naszym pliku index.js

> index.js
 1import { createServer } from 'node:http';
 2
 3// Create a HTTP server
 4const server = createServer((req, res) => {
 5  res.writeHead(200, { 'Content-Type': 'text/plain' });
 6  res.end('hello world!');
 7});
 8
 9const port = 8000;
10const host = "localhost";
11
12// Start the server
13server.listen(port, host, () => {
14    console.log(`Server listening on http://${host}:${port}`);
15});

Omówienie:

Sprawdzenie serwera HTTP

Po uruchomieniu serwera komendą:

node index.js

VS Code powinno pozwolić nam otworzyć przeglądarkę pod adresem “http://localhost:8000/”. Jeżeli przeglądarka wyświetliła tekst “hello world!”, to gratulacje! Właśnie uruchomiłeś swój własnoręcznie napisany serwer HTTP w Node.js.

Sprawdź w narzędziach deweloperskich przeglądarki, jak wygląda żądanie i odpowiedź odesłana przez serwer. Czy są tam jakieś niespodziewane rzeczy? Albo dziwne żądania?

Jeżeli masz ochotę, możesz też spróbować wysłać żądanie do serwera używając konsolowego programu curl na swojej maszynie wirtualnej:

curl --verbose http://localhost:8000

Albo, jeżeli czujesz przypływ odwagi, możesz wykorzystać program telnet do własnoręcznego napisania swojego własnego żądania HTTP.

> telnet localhost 8000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.0
Host: localhost:8000
Accept: */*

HTTP/1.1 200 OK
Content-Type: text/plain
Date: Mon, 08 Sep 2025 12:33:55 GMT
Connection: close

hello world!Connection closed by foreign host.

Podsumowanie

Postawiliśmy dziś swój pierwszy serwer HTTP oraz poznaliśmy podstawy tego protokołu.

W następnych lekcjach będziemy budować na tej wiedzy i tworzyć bardziej zaawansowaną aplikację internetową.