Програми в мрежата
Проблем
Касата и складът
Касата в магазина трябва да пита склада колко кафе е останало, а складовата програма работи на друг компютър. Файл не помага: двете програми не виждат дисковете си. Трябва едната да попита, а другата да отговори, веднага, по мрежата.
Така работи почти всичко около теб: браузърът пита сървъра за страница, приложението на банката пита банката за баланса, играта праща ходовете ти на другите играчи.
Идеята
Адрес, порт и два начина за пращане
За да стигнат данните до програма на друг компютър, трябват две неща:
- IP адрес: номерът на компютъра в мрежата, например
192.168.1.20или142.250.185.78. Собственият ти компютър винаги е127.0.0.1, с имеlocalhost. - Порт: номер от 0 до 65 535 за програмата на този компютър. Адресът е сградата, портът е апартаментът. HTTPS ползва 443, а ние ще ползваме 5101 и нагоре.
Програмата, която чака да я потърсят, е сървър. Тази, която търси, е клиент. Как пътуват данните, решава протоколът:
| TCP | UDP | |
|---|---|---|
| Връзка | първо се свързват, после си говорят | без връзка, всеки пакет е отделно писмо |
| Доставка | всичко стига, в същия ред, или разбираш, че не е | пакетът може да се загуби или да дойде в друг ред |
| Скорост | по-бавен | по-бърз |
| За какво | уеб, имейл, плащания, файлове | глас и видео на живо, игри, сензори |
node:net.По-точно, за любопитните
- Мрежата се описва на слоеве. Моделът OSI има седем: физически (битове по кабела или въздуха), канален (връзка до съседното устройство), мрежов (IP адреси и маршрути), транспортен (TCP, UDP), сесиен, представителен и приложен (HTTP, имейл). Интернет на практика ползва по-простия модел TCP/IP с четири слоя. Програмата ти работи на най-горния: Node и операционната система поемат всичко отдолу.
- IP доставя отделни пакети до адрес, но не обещава, че ще стигнат. TCP стъпва върху IP и номерира пакетите, за да подреди и поиска наново загубените.
- Протоколи, които ще срещнеш: HTTP и HTTPS (уеб; HTTPS е HTTP, криптиран с TLS, по-старото име е SSL), WebSocket (двупосочна връзка за чат и известия в браузъра), FTP (файлове), SSH (сигурен достъп до отдалечен компютър).
Виж го
Сървърът
Складовият сървър получава име на продукт и отговаря с цената:
// await readLine() чака следващия ред от сокета (undefined, когато връзката е затворена).function lineReader(socket) { const lines = readline.createInterface({ input: socket })[Symbol.asyncIterator](); return async () => (await lines.next()).value;}const PRICES = new Map([["кафе", 7.50], ["чай", 3.20]]);
async function serve(client) { const readLine = lineReader(client); const product = await readLine(); console.log(`Сървърът получи: ${product}`); const price = PRICES.get(product); client.end(price === undefined ? "няма такъв продукт\n" : `${product}: ${price} €\n`);}- Нишка и
accept()няма. Node викаserveза всеки клиент, който се свърже, и му дава сокет за връзката (client). - Сокетът е поток: байтовете идват на парчета, когато пристигнат.
lineReader(горе) ги събира в редове сnode:readline, аawait readLine()чака следващия ред. client.end(текст)праща последния текст и затваря връзката. Текстът минава по мрежата в UTF-8."\n"в края казва на другата страна, че редът е свършил. Ще видим защо е важно.
Виж го
Клиентът
net.connect(порт, адрес) и ползва същите lineReader и write. Тук сървърът и клиентът са в една програма: Node обслужва двете страни в една нишка, като ги редува, докато всяка чака. Така можеш да ги пробваш без втори компютър:const server = net.createServer(serve);server.listen(5101, "localhost");await once(server, "listening");
const socket = net.connect(5101, "localhost");const readLine = lineReader(socket);socket.write("кафе\n");console.log(`Клиентът получи: ${await readLine()}`);socket.end();server.close();Сървърът получи: кафеКлиентът получи: кафе: 7.5 €server.listen(5101, "localhost") отваря порта, а await once(server, "listening") чака, докато е отворен. Така, когато клиентът се свърже, сървърът вече слуша.Двете страни се редуват: клиентът пише ред и чака отговор с await readLine(), а сървърът чете реда и отговаря. Ако сменят местата си и двете чакат да четат, ще чакат завинаги.
const server = net.createServer(serve);server.listen(5101, "localhost");await once(server, "listening");
const socket = net.connect(5101, "localhost");const readLine = lineReader(socket);socket.write("кафе\n");console.log(`Клиентът получи: ${await readLine()}`);socket.end();server.close();Стъпка по стъпка, с думи
- Сървърът отваря порт 5101 и започва да слуша.
- Нишка няма: Node ще извика serve, когато дойде клиент. Дотогава програмата продължава.
- Клиентът се свързва. Node вика serve със сокет за клиента и двете страни могат да пишат и да четат.
- Клиентът пише ред: „кафе“. Той минава по връзката до сървъра.
- Клиентът чака с await readLine(). Сървърът прочита реда и го печата.
- Сървърът намира цената и пише обратно: „кафе: 7.5 €“.
- await readLine() на клиента връща отговора и той го печата.
- socket.end() затваря връзката, а server.close() затваря порта.
По-точно, за любопитните
- Цялата програма, с
import-ите:
import net from "node:net";import readline from "node:readline";import { once } from "node:events";
// await readLine() чака следващия ред от сокета (undefined, когато връзката е затворена).function lineReader(socket) { const lines = readline.createInterface({ input: socket })[Symbol.asyncIterator](); return async () => (await lines.next()).value;}
const PRICES = new Map([["кафе", 7.50], ["чай", 3.20]]);
async function serve(client) { const readLine = lineReader(client); const product = await readLine(); console.log(`Сървърът получи: ${product}`); const price = PRICES.get(product); client.end(price === undefined ? "няма такъв продукт\n" : `${product}: ${price} €\n`);}
const server = net.createServer(serve);server.listen(5101, "localhost");await once(server, "listening");
const socket = net.connect(5101, "localhost");const readLine = lineReader(socket);socket.write("кафе\n");console.log(`Клиентът получи: ${await readLine()}`);socket.end();server.close();- В два отделни файла и два терминала е същото: пускаш първо сървъра, после клиента. Ако на порта няма сървър, сокетът дава грешка
ECONNREFUSED. Ако портът вече е зает от друга програма, сървърът дава грешкаEADDRINUSE. Грешките идват като събитие"error": безsocket.on("error", …)програмата спира с тях. - Текстът минава по мрежата като байтове.
writeиreadlineползват UTF-8 по подразбиране, затова кирилицата пристига цяла.
Пробвай
Отвори порта
Кой метод отваря порта, за да чака сървърът клиенти?
const server = net.createServer(serve);
server.(5101, "localhost");listen(порт, адрес) отваря порта. След това Node вика serve за всеки клиент, който се свърже: accept() в JavaScript няма.Идеята
Много клиенти наведнъж
handle за всеки нов клиент. Докато един клиент чака, Node обслужва другите, всичко в една нишка:let number = 0;
async function handle(client) { number++; const n = number; for await (const line of readline.createInterface({ input: client })) { console.log(`Клиент ${n}: ${line}`); client.write(`Ехо: ${line}\n`); }}handle отговаря на всеки ред, докато клиентът не затвори връзката: тогава for await свършва. Два клиента един след друг:const server = net.createServer(handle);server.listen(5102, "localhost");await once(server, "listening");
for (const order of ["маса 4: две кафета", "маса 7: сметката"]) { const socket = net.connect(5102, "localhost"); const readLine = lineReader(socket); socket.write(`${order}\n`); console.log(await readLine()); socket.end();}server.close();Клиент 1: маса 4: две кафетаЕхо: маса 4: две кафетаКлиент 2: маса 7: сметкатаЕхо: маса 7: сметкатаПо-точно, за любопитните
numberе общ за всички клиенти, но състезание няма: в даден момент работи само една функция, а междуnumber++иconst n = numberнямаawait.server.close()спира новите връзки. Node свършва програмата, когато се затворят и старите.- Тази една нишка стига за хиляди клиенти, защото през повечето време те чакат мрежата. Ако сървърът трябва да смята много, тежката работа отива в Worker от предишния урок.
Пробвай
Двамата чакат
Клиентът праща „кафе“ и чака отговор. Сървърът е същият, но програмата увисва и не печата нищо. На кой ред е грешката?
Избери реда с грешката.
"\n". Сървърът чете редове, а ред има чак когато дойде "\n". Сървърът чака края на реда, а клиентът чака отговор. Поправката е socket.write("кафе\n").Идеята
UDP: писма без връзка
node:dgram (datagram, отделен пакет):const receiver = dgram.createSocket("udp4");receiver.bind(5103, "localhost");await once(receiver, "listening");
const sensor = dgram.createSocket("udp4");sensor.connect(5103, "localhost");await once(sensor, "connect");for (const reading of ["кухня 21.5", "склад 4.0", "хладилник 3.2"]) { sensor.send(reading);}
let count = 0;receiver.on("message", (data) => { console.log(`Температура: ${data} °C`); count++; if (count === 3) { receiver.close(); sensor.close(); }});Температура: кухня 21.5 °CТемпература: склад 4.0 °CТемпература: хладилник 3.2 °CcreateSocket("udp4")прави UDP сокет. Получателят слуша на порт 5103 сbind(5103, "localhost").- Изпращачът вика
connect(5103, "localhost"): при UDP това не прави връзка, само запомня накъде отиват пакетите. Послеsend(текст)праща един пакет. - Всеки пакет идва като събитие
"message". След третия програмата затваря двата сокета, иначе Node чака още пакети завинаги. - Никой не потвърждава, че пакетът е стигнал. На
localhostне се губят, но по истинска мрежа може.
По-точно, за любопитните
Ако получателят трябва да отговори, взима адреса и порта на изпращача от втория параметър на "message": (data, rinfo) => receiver.send(отговор, rinfo.port, rinfo.address).
Пакетът е ограничен по размер (под 64 KB, а по интернет е разумно до около 1 400 байта), затова за файлове UDP не става.
Идеята
HTTP: езикът на уеба
Браузърът, когато отваряш страница, прави TCP връзка до сървъра и праща текстова HTTP заявка: „дай ми /“. Сървърът връща отговор: статус, заглавки и тяло (HTML, JSON, картинка). В Node (и в браузъра) това прави fetch, без import:
const response = await fetch("https://example.com");console.log(`Статус: ${response.status}`);console.log(`Тип: ${response.headers.get("content-type")}`);const body = await response.text();console.log(`Заглавие: ${body.slice(body.indexOf("<title>") + 7, body.indexOf("</title>"))}`);Статус: 200Тип: text/html; charset=utf-8Заглавие: Example Domainfetch(адрес)праща заявкаGETи дава обещание за отговора.awaitго чака.response.text()чете тялото като текст. И то е обещание, затова пакawait.- Статусът казва как е минало:
200е успех,404няма такава страница,500е грешка на сървъра.fetchне хвърля при 404: провериresponse.statusилиresponse.ok.
Проверката на кода на сайта е без интернет, затова тази програма е показана с изхода от пускане с мрежа. Без мрежа fetch хвърля TypeError: fetch failed.
По-точно, за любопитните
node:http). Така можеш да пробваш fetch и без интернет, на localhost:const server = http.createServer((request, response) => { if (request.url !== "/stock") { response.writeHead(404).end("няма такъв адрес"); return; } response.writeHead(200, { "content-type": "text/plain; charset=utf-8" }).end("кафе: 12 бр.");});server.listen(5104, "localhost");await once(server, "listening");
for (const path of ["/stock", "/prices"]) { const response = await fetch(`http://localhost:5104${path}`); console.log(`${path} -> ${response.status} ${await response.text()}`);}server.close();/stock -> 200 кафе: 12 бр./prices -> 404 няма такъв адрес/prices тя връща 404, а fetch го дава като обикновен отговор. В истински проекти за уеб сървър се ползват библиотеки като Express. Те са построени върху същия node:http.Упражнение
Сървъри на localhost
1. Ехо сървър. Сървърът връща всеки ред с „Ехо: “ отпред, докато не получи „край“. Тогава отговаря „Довиждане“ и затваря връзката. Клиентът праща „здравей“, „как си“ и „край“ и печата отговорите:
Ехо: здравейЕхо: как сиДовиждане2. Сървър с команди. ГЛАВНИ текст връща текста с главни букви, ЧАС връща текущия час като 14:05 (new Date().toTimeString().slice(0, 5)), а всичко друго връща „Непозната команда: “ и командата. Часът се мени, затова клиентът проверява само вида му с регулярен израз:
ПОРЪЧКА 42 Е ГОТОВАЧасът има вид ЧЧ:ММ: trueНепозната команда: ВРЕМЕasync функция, двете са пуснати заедно с Promise.all, и всеки праща готовите си реплики, като след всяка чака отговор (await once(server, "connection") чака първия клиент):Борис получи: Здрасти, Борис! Свободен ли си в 18:00?Ана получи: Да, ще дойда.Борис получи: Супер, до скоро.Ана получи: Чао!readline с input: process.stdin) и ги праща, а редовете от сокета печата, щом дойдат. Втора нишка не трябва: и двете идват като събития в цикъла на събитията. Така можеш да пишеш, докато чакаш отговор.Тест