Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi gateway
- 2.1 O'rnatish
- 2.2 Gateway klassi
- 2.3 Natija
- 3. Xavfsizlik: CSWSH va handshake
- 3.1 Hujumchi nigohi
- 3.2 Kanon tajribasi: haqiqiy sessiya bilan
- 4. Adapterlar va masshtablash
- 5. O'lchov: gateway, @Sse va SseHub
- 5.1 Uch yo'l, bitta yuk
- 5.2 Sarlavhalar
- 5.3 Sekin mijoz
- 5.4 Kanon qarori
- 6. /v1/oqim — kanondagi controller
- 7. Kanon topgan xatolar — umumiy ro'yxat
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): handshake tekshiruvchisi
- 3-mashq (qiyin): egasi bo'yicha tarqatish
- 10. Loyiha qadami: integratsiyalar Nest'da — yakun va commit
- 10.1 Yakuniy tekshiruv
- 10.2 Commit
- 11. Real ishda
- Xulosa
- Manbalar
WebSocket gateway'lar NestJS'da: @WebSocketGateway, SSE bilan o'lchov va yakuniy kanon
Qisqacha: Gateway — WebSocket uchun controller:
@WebSocketGateway({ path })klass,@SubscribeMessage("ping")metodlari,@MessageBody()va@ConnectedSocket()parametrlari. Ulanishning o'zihandleConnection(ws, req)da tekshiriladi — guard'lar faqat xabarlarga qo'llanadi, handshake'ga emas. WebSocket CORS'ga bo'ysunmaydi: begona sayt brauzer cookie'si bilan ulanadi (CSWSH), shuning uchunOriginqo'lda tekshiriladi. Adapter:WsAdapter(ws) yokiIoAdapter(Socket.IO). Kanon o'lchovi — 100 mijoz × 500 hodisa. Gateway 722 ms, 29-qismdagi SSE hub 173 ms, Nest@Sse1 317 ms.vazifalar-apirealtime uchun SSE'da qoldi (29-qism qarori: bir tomonlama oqim, cookie va HTTP infratuzilmasi bilan tabiiy).
Bu darsda
- Nest gateway'ini lifecycle interfeyslari, xabar handler'lari va xato bilan to'liq yozasiz.
- Handshake'da sessiya cookie'si va
Originni tekshirasiz — haqiqiy chiqish bilan. WsException, guard va adapterlarning (WsAdapter,IoAdapter, Redis adapter) o'rnini bilasiz.- Gateway, Nest
@Sseva kanondagiSseHubo'lchovini o'qiysiz va sekin mijoz muammosini tushuntirasiz. - 29-qism integratsiyalarining Nest'ga ko'chishini yakuniy tekshiruv bilan tugatib, commit qilasiz.
Oldin bilishingiz kerak: WebSocket protokoli va ws kutubxonasi, Socket.IO, Realtime'ni masshtablash, Server-Sent Events NestJS'da, Global auth guard, @Public va @Roles, Guard'lar.
O'tgan darsda tashqi xizmatlar bitta modulga yig'ildi va SSE oqimi (/v1/oqim) ham shu modul ichida ulandi. Bugun realtime'ning ikkinchi yo'li — WebSocket — Nest'da qanday bo'lishini ko'ramiz. Keyin ikkalasini o'lchab, qism bo'yicha katta qadamni yakunlaymiz.
1. Nega bu kerak?
29-qismda realtime usullarini solishtirgandik (Realtime usullari va tanlov): polling, SSE, WebSocket. vazifalar-api uchun SSE tanlangan: server → mijoz bir tomonlama hodisalar ("vazifa o'zgardi"), cookie avtomatik ketadi, HTTP proxy va keshlar uni tushunadi.
WebSocket esa ikki tomonlama: chat, onlayn o'yin, birgalikda tahrirlash. Nest loyihalarida u juda keng tarqalgan — @nestjs/websockets paketi bilan. Ishga kirsangiz, gateway'ni ko'rasiz. Shuning uchun uni to'liq o'rganamiz, xavfsizlik tuzoqlari bilan.
O'xshatish: SSE — radio. Stansiya gapiradi, hamma eshitadi, eshituvchi javob bermaydi. WebSocket — telefon: ikki tomon ham gapiradi. Radio arzon va oddiy; telefon kerak bo'lsa — telefon.
2. Birinchi gateway
2.1 O'rnatish
npm i @nestjs/websockets@12.2.0 @nestjs/platform-ws@12.2.0 ws@nestjs/websockets — dekoratorlar va yadro. Adapter alohida: @nestjs/platform-ws (ws kutubxonasi, 29-qismdagi bilan bir xil — WebSocket protokoli va ws kutubxonasi) yoki @nestjs/platform-socket.io (Socket.IO). Biz ws bilan yozdik (biz ishga tushirdik: NestJS 12.2.0, ws 8.22.0, Node 24.21.0).
const app = await NestFactory.create(M);
app.useWebSocketAdapter(new WsAdapter(app));
await app.listen(3000, "127.0.0.1");useWebSocketAdapter — qaysi WebSocket kutubxonasi ishlatilishini aytadi. Gateway HTTP server bilan bitta portda ishlaydi: ulanish HTTP Upgrade so'rovi bilan boshlanadi.
2.2 Gateway klassi
Demo gateway — to'liq (qatorlar telefon uchun bo'lingan):
const ISHONCHLI = new Set(["http://localhost:5173"]);
const egalar = new WeakMap<WebSocket, string>();
@WebSocketGateway({ path: "/v1/ws" })
class JonliGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server!: Server;
afterInit(): void {
log.push("afterInit: server tayyor");
}
handleConnection(ws: WebSocket, req: IncomingMessage): void {
const origin = req.headers.origin;
if (origin === undefined || !ISHONCHLI.has(origin)) {
ws.close(4403, "origin");
return;
}
const kim = /sessiya=(\w+)/.exec(req.headers.cookie ?? "")?.[1];
if (kim === undefined) {
ws.close(4401, "kirish-kerak");
return;
}
egalar.set(ws, kim);
log.push(`ulandi: ${kim}`);
}
handleDisconnect(ws: WebSocket): void {
log.push(`uzildi: ${egalar.get(ws) ?? "noma'lum"}`);
}
@SubscribeMessage("ping")
ping() {
return { event: "pong", data: "ok" };
}
@SubscribeMessage("qosh")
qosh(
@MessageBody() tana: { matn?: string },
@ConnectedSocket() ws: WebSocket,
) {
if (!tana.matn) throw new WsException("matn bo'sh");
return { event: "qoshildi", data: { matn: tana.matn,
kim: egalar.get(ws) } };
}
@UseGuards(AdminGuard)
@SubscribeMessage("tozala")
tozala() {
return { event: "tozalandi", data: true };
}
}Qismma-qism:
| Element | Vazifasi |
|---|---|
@WebSocketGateway({ path }) |
gateway; ws adapterida yo'l, Socket.IO'da — namespace va cors |
@WebSocketServer() |
adapterning server obyekti (ws da Server, hamma mijozlar server.clients) |
afterInit (OnGatewayInit) |
server yaratilgach bir marta |
handleConnection (OnGatewayConnection) |
har yangi ulanish — himoya joyi |
handleDisconnect (OnGatewayDisconnect) |
ulanish yopildi |
@SubscribeMessage("qosh") |
{"event":"qosh","data":…} xabari shu metodga |
@MessageBody(), @ConnectedSocket() |
xabar tanasi va mijoz ulanishi |
WsException |
xabar xatosi — mijozga exception hodisasi |
Gateway — provider: modulning providers iga yoziladi (controller emas) va DI'dan xizmatlarni oladi. Bu demoda cookie qiymatining o'zi "foydalanuvchi" — haqiqiy sessiya tekshiruvi kanon tajribasida (quyida).
ws adapterida xabar formati — JSON { event, data }. Handler { event, data } qaytarsa, u shu mijozga yuboriladi. Hammaga yuborish — this.server.clients bo'ylab send.
2.3 Natija
Uchta ulanish va to'rtta xabar. O'qishdan oldin ikki narsani belgilang: rad etilgan ulanishlar HTTP status emas, yopish kodi oladi (4401, 4403), xabar xatolari esa ulanishni uzmaydi — exception hodisasi bo'lib keladi:
cookie'siz → 4401
begona Origin → 4403
ping → {"event":"pong","data":"ok"}
qosh → {"event":"qoshildi","data":{"matn":"Non","kim":"aziz"}}
qosh bo'sh → {"event":"exception","data":{"status":"error","message":"matn bo'sh","cause":{"pattern":"qosh","data":{}}}}
tozala (aziz) → {"event":"exception","data":{"status":"error","message":"Forbidden resource","cause":{"pattern":"tozala","data":{}}}}
afterInit: server tayyor
uzildi: noma'lum
uzildi: noma'lum
ulandi: aziz
uzildi: azizQatorlarni o'qiymiz:
- 4401, 4403 — WebSocket yopish kodlari. 4000–4999 — ilovaning o'z kodlari uchun ajratilgan; kanon HTTP 401 va 403 ga o'xshatib 4401/4403 ni tanlagan.
exception—WsExceptionni Nest'ning sukut WebSocket xato filtri shu shaklda yuboradi:status,messagevacause— qaysi xabar (pattern) va qanday tana bilan kelgani. Ulanish uzilmaydi.- "Forbidden resource" — guard
falseqaytardi. HTTP'da bu 403 bo'lardi; WebSocket'da —exceptionhodisasi. Mijoz kodi buni alohida tutishi kerak. - "uzildi: noma'lum" ikki marta — rad etilgan ulanishlar uchun ham
handleDisconnectchaqirildi. Bizclose()qilganimizda ulanish allaqachon ochilgan edi. Shuning uchunhandleDisconnect"bu ulanish ro'yxatda bormi" deb tekshirishi kerak — demodaegalar.get(ws) ?? "noma'lum". - Tartib —
logoxirida chiqarildi, shuning uchun "ulandi: aziz" ikki rad etilgan uzilishdan keyin turibdi: ular avvalroq sodir bo'lgan.
HTTP so'rovining hayot sikli ni bilasiz. Gateway'da u ikkiga bo'linadi: ulanish bir marta, xabarlar esa ko'p marta keladi. Ikkalasining yo'li har xil:
| Bosqich | HTTP so'rovi | WebSocket ulanishi | WebSocket xabari |
|---|---|---|---|
| middleware | ishlaydi | yo'q (Upgrade Nest marshrutiga kirmaydi) | yo'q |
| guard (himoyachi) | ishlaydi | yo'q — handleConnection qo'lda |
@UseGuards ishlaydi |
| interceptor, pipe | ishlaydi | yo'q | ishlaydi |
| handler | controller metodi | handleConnection |
@SubscribeMessage metodi |
| xato filtri | HTTP javob (401, 422…) | — (o'zingiz ws.close(kod)) |
exception hodisasi, ulanish ochiq |
Jadvaldagi eng muhim katak — "guard, ulanish" ustuni. HTTP'dagi barcha himoya (global KirishGuard, CheklovGuard) ulanish paytida ishlamaydi. Shuning uchun handleConnection — gateway'dagi yagona eshik: u yerda nima tekshirmasangiz, o'sha ochiq qoladi.
Tekshirib ko'ring:
AdminGuardnihandleConnectionga qo'llab, "admin bo'lmaganni ulamaslik" mumkinmi?
Javob
Yo'q. Nest guard'lari, pipe va interceptor'lari gateway'da faqat @SubscribeMessage handler'lariga qo'llanadi. handleConnection — lifecycle metodi, u guard zanjiridan o'tmaydi. Ulanishni rad etish — handleConnection ichida qo'lda (ws.close(4403)), yoki adapter darajasida (verifyClient, Socket.IO'da middleware). Kanon tajribasi ham shuni tasdiqlaydi.
3. Xavfsizlik: CSWSH va handshake
3.1 Hujumchi nigohi
CSWSH (Cross-Site WebSocket Hijacking — saytlararo WebSocket o'g'irlash): foydalanuvchi vazifalar ilovasiga kirgan, brauzerda sessiya cookie'si bor. U boshqa saytni ochadi — yovuz.example. U saytdagi skript new WebSocket("wss://api.example.com/v1/ws") qiladi. Cookie SameSite=None bo'lsa yoki hujumchi sahifasi bir saytdagi boshqa subdomenda bo'lsa (same-site), brauzer cookie'ni o'zi qo'shadi. WebSocket uchun esa CORS yo'q — server javobini brauzer to'smaydi. Server Origin'ni tekshirmasa — begona sayt foydalanuvchi nomidan ulanib, uning vazifalari oqimini o'qiydi.
Himoya — handshake'da Origin sarlavhasini ruxsat ro'yxati (allowlist) bilan solishtirish. Brauzer Origin ni soxtalashtirishga yo'l qo'ymaydi. Demo natijadagi "begona Origin → 4403" — aynan shu himoya. Faqat o'z lokal loyihangizda sinang.
3.2 Kanon tajribasi: haqiqiy sessiya bilan
Kanon xuddi shunday gateway'ni 28-qismdagi haqiqiy sessiya ombori bilan yozdi (@nestjs/websockets 12.2.0, @nestjs/platform-ws 12.2.0). Handshake (qisqartirilgan):
// Guard (@UseGuards) bu yerda ISHLAMAYDI — u faqat
// @SubscribeMessage handler'lariga; ulanish shu yerda tekshiriladi
async handleConnection(ws: WebSocket, req: IncomingMessage) {
const origin = req.headers.origin;
if (origin === undefined || !ISHONCHLI.has(origin)) {
ws.close(4403, "origin");
return;
}
const t = cookieOl(req.headers.cookie, cookieNomi(SUKUT));
const s = t === undefined ? undefined : await sessiyalar.tekshir(t);
if (s === undefined) {
ws.close(4401, "kirish-kerak");
return;
}
egalar.set(ws, s.foydalanuvchi.id);
}sessiyalar.tekshir(token) — 28-qismdagi SessiyaOmbori: token hash'i bazada, muddati o'tmagan. Natija:
### 1-2. Handshake: cookie'siz, begona Origin (CSWSH), to'g'ri
cookie'siz → yopildi, kod 4401
begona Origin + cookie (CSWSH) → yopildi, kod 4403 (Origin tekshiruvisiz — ulanardi: brauzer cookie'ni yuboradi, CORS yo'q)
to'g'ri → ochiq; ping → {"event":"pong","data":"ok"}Ikkinchi qatordagi izohga qarang: Origin tekshiruvisiz begona sayt haqiqiy cookie bilan ulanardi. Global KirishGuard bu yerda yordam bermaydi — u HTTP controller'lar uchun.
4. Adapterlar va masshtablash
| Adapter | Paket | Xususiyat |
|---|---|---|
WsAdapter |
@nestjs/platform-ws |
xom WebSocket, eng yengil; xona va qayta ulanish — o'zingiz |
IoAdapter |
@nestjs/platform-socket.io |
Socket.IO: xonalar, namespace, ack, qayta ulanish, polling'ga tushish |
Socket.IO'da @WebSocketGateway({ namespace: "/jonli", cors: { origin: [...] } }) va client.join("xona") bor (Socket.IO). Lekin Socket.IO mijozi ham Socket.IO bo'lishi shart — oddiy WebSocket u bilan gaplasha olmaydi.
Bir nechta server nusxasi. Mijoz A birinchi nusxaga, mijoz B ikkinchisiga ulangan. Birinchi nusxadagi hodisa B ga qanday yetadi? 29-qismda buni o'rgandik (Realtime'ni masshtablash): Redis Pub/Sub. Socket.IO uchun — @socket.io/redis-adapter: Nest'da IoAdapter dan meros olgan o'z adapter klassi yoziladi va createIOServer ichida Redis adapteri ulanadi. ws uchun — o'zingiz: har nusxa Redis kanalini tinglab, o'z mijozlariga yozadi. Kanondagi SseHub aynan shunday ishlaydi (SSE uchun). Redis adapter'ni biz sinamadik.
5. O'lchov: gateway, @Sse va SseHub
5.1 Uch yo'l, bitta yuk
Kanon bitta mashinada, bitta jarayonda uch realtime yo'lni bir xil yuk bilan o'lchadi: 100 mijoz, har biriga 500 hodisa — hammasi yetib kelguncha vaqt.
- WebSocket gateway (
ws) — shu darsdagi kabi. - Nest
@Sse— Server-Sent Events NestJS'da darsidagi/v1/vazifalar/oqim: handlerObservable<MessageEvent>qaytaradi, Nest javobni o'zi yozadi. SseHub— 29-qismdagi SSE hub (@Res()bilan): Redis Stream tarixi, Last-Event-ID, sekin mijozni uzish.
### 1. 100 mijoz × 500 hodisa — hammasi yetib kelguncha (bir jarayon, lokal)
@Sse + Observable (A) 1317 ms, olindi: min 500 / 500
SseHub @Res (29 kanon) 173 ms, olindi: min 500 / 500### 3. 100 mijoz × 500 hodisa (tC-sse bilan bir xil yuk)
WebSocket gateway (ws) 722 ms, hammasi 500 / 500- SseHub (@Res, 29-qism)173 ms
- WebSocket gateway (ws)722 ms
- Nest @Sse + Observable1 317 ms
Manba: Kanon o'lchovi (IlmHamroh, 2026-11-01): NestJS 12.2.0, @nestjs/platform-ws 12.2.0, ws 8.22.0, rxjs 7.8.2, bitta mashina, bitta jarayon
SseHub @Sse dan ~7.6× tez chiqdi. Sabab kanon tomonidan tekshirilmagan — taxmin: @Sse har hodisani RxJS zanjiri va Nest'ning SSE yozuvchisi orqali o'tkazadi, SseHub esa tayyor satrni to'g'ridan res.write qiladi. Gateway o'rtada. Hamma yo'lda 500/500 — hodisa yo'qolmadi. Bu o'lchovlar ikki alohida tajribada, lekin bir xil yukda va bir xil mashinada; nisbatga qarang.
5.2 Sarlavhalar
### 2. Javob sarlavhalari
/a: cache-control=private, no-cache, no-store, must-revalidate, max-age=0, no-transform; connection=keep-alive; content-type=text/event-stream; x-accel-buffering=no
/b: cache-control=no-cache, no-transform; connection=keep-alive; content-type=text/event-stream; charset=utf-8; x-accel-buffering=no/a — @Sse, /b — SseHub. Ikkalasi ham x-accel-buffering: no qo'yadi — nginx oqimni buferlamasin (Reverse proxy orqasida Node). @Sse keshni qattiqroq o'chiradi.
5.3 Sekin mijoz
Eng muhim farq — o'qimaydigan mijoz. Telefon tunnelga kirdi: TCP ulanish ochiq, lekin ma'lumot o'qilmayapti. Server unga yozishda davom etsa, baytlar server xotirasida, ulanish buferida yig'iladi.
### 3. Uzilganda tozalash va sekin mijoz (o'qimaydi), 2 000 × 2 KB hodisa
@Sse obunalari (hamma mijoz uzilgandan keyin): 0; SseHub ulanishlari: 0
@Sse: obuna tirik (buferlaydi); SseHub: ulanishlar 0 (bufer 256 KB dan oshgach uzildi); heap farqi 13 MBBirinchi qator: mijozlar uzilganda ikkalasi ham obunani to'g'ri tozaladi. Ikkinchi qator — farq: SseHub bitta sekin mijoz uchun bufer 256 KB dan oshishi bilan ulanishni uzdi (res.writableLength ni tekshiradi). @Sse esa yozishda davom etdi. Mijoz qayta ulanganda Last-Event-ID bilan o'tkazib yuborgan hodisalarni oladi — uzish xavfsiz.
Kichik model (brauzerda ishlaydi):
// Sekin mijoz modeli: hub har mijozga hodisa yozadi; o'qimaydigan
// mijozning buferi o'sadi. SseHub — chegaradan oshsa uzadi, @Sse esa
// buferlashda davom etadi (xotira o'sadi)
const KB = 1024;
function hubYarat({ maksBufer }) {
const mijozlar = [];
return {
ulan(nom, oqiydi) {
mijozlar.push({ nom, oqiydi, bufer: 0, ochiq: true });
},
yubor(bayt) {
for (const m of mijozlar.filter((x) => x.ochiq)) {
m.bufer = m.oqiydi ? 0 : m.bufer + bayt;
if (maksBufer !== undefined && m.bufer > maksBufer) {
m.ochiq = false; // uzildi
}
}
},
holat: () => mijozlar.map((m) =>
`${m.nom}: ${m.ochiq ? "ochiq" : "uzildi"}, ` +
`bufer ${Math.round(m.bufer / KB)} KB`),
};
}
for (const [nom, sozlama] of [
["SseHub (256 KB)", { maksBufer: 256 * KB }],
["@Sse (chegarasiz)", {}],
]) {
const hub = hubYarat(sozlama);
hub.ulan("tez", true);
hub.ulan("sekin", false);
for (let i = 0; i < 2000; i++) hub.yubor(2 * KB);
console.log(nom, "→", hub.holat().join(" | "));
}Konsolda:
SseHub (256 KB) → tez: ochiq, bufer 0 KB | sekin: uzildi, bufer 258 KB
@Sse (chegarasiz) → tez: ochiq, bufer 0 KB | sekin: ochiq, bufer 4000 KBBitta sekin mijoz — 4 MB. Ming sekin mijoz — 4 GB. Model soddalashtirilgan: haqiqiy buferni Node oqimi va operatsion tizim boshqaradi, kanon tajribasida heap 13 MB o'sdi.
5.4 Kanon qarori
| Mezon | Gateway (ws) | @Sse |
SseHub (kanon) |
|---|---|---|---|
| Yo'nalish | ikki tomonlama | server → mijoz | server → mijoz |
| Auth | qo'lda: cookie + Origin | HTTP guard | HTTP guard |
| Qayta ulanish va tarix | o'zingiz | yo'q | Last-Event-ID + Redis Stream |
| Sekin mijoz | o'zingiz | buferlaydi | 256 KB da uzadi |
| O'lchov (100 × 500) | 722 ms | 1 317 ms | 173 ms |
vazifalar-api ga ikki tomonlama kanal kerak emas — mijoz o'zgarishni REST orqali yuboradi, server esa hodisa bilan xabar beradi. SSE'da auth global KirishGuard dan o'tadi (HTTP so'rov), WebSocket'da esa himoyani qo'lda yozish kerak. @Sse esa sekin mijoz va tarix masalasida SseHub dan zaif. Shuning uchun:
/v1/oqim—SseHub(@Res()bilan),OqimController;/v1/vazifalar/oqim—@Sse(oldingi qadamdan, bitta jarayon,EventEmitter) — qoldi, solishtirish uchun;- WebSocket gateway — kanonga kirmaydi.
Chat yoki birgalikda tahrirlash kerak bo'lsa — gateway to'g'ri tanlov, shu darsdagi Origin va handshake qoidalari bilan.
6. /v1/oqim — kanondagi controller
@Controller("oqim")
@Cheklov(API_CHEKLOVI) // uzoq ulanish — bitta so'rov sifatida
export class OqimController {
constructor(@Inject(JONLI) private readonly hub: SseHub) {}
@Get()
async ulan(
@Egasi() egasi: number,
@Req() req: Request,
@Res() res: Response,
): Promise<void> {
await this.hub.ulan(req, res, egasi);
}
}Uch qator ish: egasi (@Egasi(), guard o'tkazgan sessiyadan), so'rov va javob — hub'ga. Hammasi 29-qismdagi SseHub.ulan ichida: sarlavhalar, retry: 2000, Last-Event-ID dan tarix, faqat shu egasining hodisalari, heartbeat, sekin mijozni uzish. Kanon izohi nega @Sse() emasligini ham aytadi: "tarix → navbat → jonli" tartibi, takrorsiz yuborish, backpressure va Redis Pub/Sub tarqatuvchisi Response obyektini talab qiladi.
Jonli tekshiruvdan (Redis, ishchi relay bilan; 3 s tinglash, parallel POST):
$ curl -N /v1/oqim (3 s; parallel POST /v1/vazifalar, X-Jonli-Manba: curl-oyna)
retry: 2000
id: <stream-id>
event: ozgardi
data: {"manba":"curl-oyna","tur":"vazifa.yaratildi","vazifaId":51}Hodisa yo'li: POST → bazaga vazifa + outbox hodisasi (bitta tranzaksiya) → ishchi relay → Redis Stream va Pub/Sub → API nusxasidagi SseHub → mijoz. id — Redis Stream id, har yugurishda boshqa (yashirilgan).
7. Kanon topgan xatolar — umumiy ro'yxat
29-qism integratsiyalari Nest'ga ko'chganda kanon topgan va tuzatgan xatolar (har biri o'z darsida batafsil):
| Xato | Qayerda | Holati |
|---|---|---|
Nest POST sukuti 201, @Res() bilan ham |
fayl tasdig'i (Fayl yuklash NestJS'da) | @HttpCode(200) — tuzatildi |
| Parallel testlar bitta Redis Pub/Sub kanalida | jonli/outbox testlari (Navbatlar) |
Vitest projects — tuzatildi |
Vite (oxc) test ichidagi klass maydonlari — SyntaxError |
telegram-bot testi (Tashqi xizmatlar) | klass modul darajasiga — tuzatildi |
| Obuna sahifasi formasi brauzerda yuborilmaydi (CSP) | 29-qismdan (Email yetkazib berish) | sahifaga alohida CSP — tuzatildi, brauzerda sinalmagan |
curl -o /dev/null Windows'da exit 23 |
kanon tekshiruv skripti | kut.mjs va -o NUL — tuzatildi |
404 title inglizcha ("Not Found") |
fayl yo'llari, 29-qismdan | tuzatilmagan (biz topdik) |
Oxirgisidan oldingi qator — kodda emas, tekshiruv skriptida: Windows'dagi curl.exe MSYS yo'li /dev/null ga yoza olmaydi va 23 bilan chiqadi. "Server tayyormi" sikli shu sabab hech qachon to'xtamasdi. Kanon hisobotiga ko'ra, auth ko'chishi paytidagi "curl osilib qoldi" holatining asl sababi ham shu bo'lishi mumkin — bu tekshirilmagan.
Bundan tashqari kanon yangi test yozdi — test/nest-integratsiya.test.ts — 29-qism testlari ko'rmaydigan Nest'ga xos xavflar uchun: 22 ta integratsiya yo'lining hammasi anonimga 401, versiyasiz integratsiya yo'llari 404, Bull Board 403/200, obuna sahifasi CSP'si.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Gateway'ga hech kim ulanmaydi | useWebSocketAdapter yo'q yoki mijoz Socket.IO/ws nomuvofiq |
adapter va mijoz bir xil kutubxona |
@UseGuards ulanishni to'xtatmaydi |
guard faqat xabarlarga | handleConnection da tekshiruv |
| Begona sayt ulanadi | Origin tekshirilmaydi (CSWSH) |
ruxsat ro'yxati, close(4403) |
Xato javobi exception hodisasi |
WsException / guard rad etdi |
mijozda exception ni tuting |
| Server xotirasi asta-sekin o'sadi | sekin mijozlar buferlanadi | bufer chegarasi va uzish |
| Ko'p nusxada hodisa yetmaydi | har nusxa faqat o'z mijozlarini biladi | Redis Pub/Sub yoki Redis adapter |
9. Mashqlar
Mashqlar kurs/mashqlar/30/65-gateway/ da.
1-mashq (oson): to'ldiring
- Xabar handler'i dekoratori:
@("ping") - Ulanish tekshiriladigan lifecycle metodi:
wsadapterini ulash:app.(new WsAdapter(app))- Begona saytdan ulanishni to'sish uchun tekshiriladigan sarlavha:
Yechim
- SubscribeMessage —
{ event: "ping" }xabari shu metodga. - handleConnection —
OnGatewayConnectioninterfeysi; guard'lar bu yerda ishlamaydi. - useWebSocketAdapter — qaysi WebSocket kutubxonasi.
- Origin — brauzer uni soxtalashtirishga yo'l qo'ymaydi; CSWSH'ga qarshi.
2-mashq (o'rta): handshake tekshiruvchisi
handshake(sarlavhalar, sessiyalar, ishonchli) — natija { ochiq: true, kim } yoki { ochiq: false, kod }. Avval Origin (yo'q yoki ro'yxatda emas — 4403), keyin sessiya=<token> cookie'si (yo'q yoki sessiyalar Map'ida yo'q — 4401). To'rt holatni sinang. Ishora: RegExp.exec (RegExp asoslari).
Yechim
function handshake(sarlavhalar, sessiyalar, ishonchli) {
const origin = sarlavhalar.origin;
if (origin === undefined || !ishonchli.has(origin)) {
return { ochiq: false, kod: 4403 };
}
const token = /sessiya=(\w+)/.exec(sarlavhalar.cookie ?? "")?.[1];
const kim = token === undefined ? undefined : sessiyalar.get(token);
if (kim === undefined) return { ochiq: false, kod: 4401 };
return { ochiq: true, kim };
}
const sessiyalar = new Map([["t0k3n", "aziz"]]);
const ishonchli = new Set(["http://localhost:5173"]);
const OK = "http://localhost:5173";
const holatlar = {
"to'g'ri": { origin: OK, cookie: "sessiya=t0k3n" },
"cookie'siz": { origin: OK },
"eski token": { origin: OK, cookie: "sessiya=eski" },
"CSWSH": {
origin: "https://yovuz.example", cookie: "sessiya=t0k3n",
},
};
for (const [nom, s] of Object.entries(holatlar)) {
console.log(nom, "→", JSON.stringify(handshake(s, sessiyalar,
ishonchli)));
}Konsolda:
to'g'ri → {"ochiq":true,"kim":"aziz"}
cookie'siz → {"ochiq":false,"kod":4401}
eski token → {"ochiq":false,"kod":4401}
CSWSH → {"ochiq":false,"kod":4403}Tartib muhim: Origin birinchi. CSWSH holatida cookie to'g'ri — tekshiruv cookie'dan boshlansa, begona sayt o'tib ketardi. Haqiqiy loyihada sessiyalar.get o'rniga 28-qismdagi sessiya ombori (bazadagi hash, muddat).
3-mashq (qiyin): egasi bo'yicha tarqatish
Paketsiz model. tarqat(mijozlar, egasi, hodisa) — faqat shu egasining ochiq mijozlariga hodisani yuborsin (m.qabul.push(hodisa)), yuborilganlar sonini qaytarsin. Uch mijoz: Aziz (2 ta ulanish, biri yopiq), Malika (1). Aziz uchun hodisa yuboring va har mijozning qabul qilganini chiqaring. Ishora: filter (filter).
Yechim
function tarqat(mijozlar, egasi, hodisa) {
const kerak = mijozlar.filter((m) => m.egasi === egasi && m.ochiq);
for (const m of kerak) m.qabul.push(hodisa);
return kerak.length;
}
const mijozlar = [
{ nom: "Aziz telefon", egasi: 1, ochiq: true, qabul: [] },
{ nom: "Aziz noutbuk", egasi: 1, ochiq: false, qabul: [] },
{ nom: "Malika", egasi: 2, ochiq: true, qabul: [] },
];
const hodisa = { tur: "vazifa.yaratildi", vazifaId: 51 };
const n = tarqat(mijozlar, 1, hodisa);
console.log("yuborildi:", n);
for (const m of mijozlar) console.log(m.nom, "→", m.qabul.length);Konsolda:
yuborildi: 1
Aziz telefon → 1
Aziz noutbuk → 0
Malika → 0Malika Aziz'ning hodisasini olmadi — bu vazifalar-api ning asosiy qoidasi: har foydalanuvchi faqat o'z vazifalarini ko'radi. Kanon tajribasidagi gateway'da xuddi shu: egalar.get(k) === egasi && k.readyState === WebSocket.OPEN. Auth Nest'ga ko'chganda kanon xuddi shunday xavfni topgan: SSE hodisalari xotirada, RLS ularni himoya qilmaydi — shuning uchun har hodisaga egasi qo'shildi.
10. Loyiha qadami: integratsiyalar Nest'da — yakun va commit
Bu — 29-qism integratsiyalari ko'chishining oxirgi qismi. Avvalgi darslarda: fayllar, navbat va ishchi, IntegratsiyaModule va xizmat controller'lari. Bu darsda:
src/realtime/oqim.controller.ts— «/v1/oqim» bo'limidagiOqimController(IntegratsiyaModuleunijonliyoqilganda qo'shadi).- Yangi test —
test/nest-integratsiya.test.ts(4 test): integratsiya yo'llari anonimga 401, versiyasiz integratsiya yo'llari 404, Bull Board 403/200, obuna sahifasi CSP'si. package.json: versiya2.1.0, skriptlarishchivabot:
"ishchi": "node --env-file-if-exists=.env dist/ishchi.js",
"bot": "node --env-file-if-exists=.env dist/bot.js"API.md— auth va integratsiyalar bo'limlari (yangi yo'llar, statuslar).
WebSocket gateway qo'shilmaydi — «Kanon qarori» bo'limiga qarang. @nestjs/websockets o'rnatilmaydi.
10.1 Yakuniy tekshiruv
npm run check
TESTCONTAINERS=1 npm test
npm run e2eKanon yakuniy tekshiruvi (Docker'dagi Postgres 18.6, Redis, MinIO, Mailpit; haqiqiy vazifalar_api, vazifalar_auth, vazifalar_ishchi rollari):
--- npm run tip
exit 0 (5940 ms)
--- npm run lint
exit 0 (12914 ms)
--- npm run test
↓ |asosiy| test/konteyner.test.ts (1 test | 1 skipped)
Test Files 55 passed | 1 skipped (56)
Tests 298 passed | 1 skipped (299)
exit 0 (82927 ms)
--- npm run e2e
Test Files 3 passed (3)
Tests 10 passed (10)
exit 0 (32280 ms)
--- npm run build
exit 0 (7254 ms)
--- Testcontainers (TESTCONTAINERS=1): [Testcontainers vaqtlari] konteyner 2318 ms, migratsiya 3355 ms, snapshot 460 ms, restore (oxirgi) 192 ms Tests 10 passed (10) ; kanon30-tc qoldiqlari: 0| Qadam | Natija |
|---|---|
| tip / lint / build | 0 / 0 / 0 |
| Vitest (56 fayl, 2 loyiha) | 298 o'tdi, 1 SKIP (konteyner — alohida 10/10) |
| e2e (Playwright, haqiqiy brauzer: CORS, CSRF, passkey) | 10/10 |
| Migratsiyalar | 20 ta, schema.prisma bilan farq yo'q, sql/ bilan farq 0 qator |
| Jonli (uch rejim, API + ishchi) | 200, 201, 202, 204, 206, 401, 403, 404, 409, 412, 422 — kutilgan; SSE hodisasi keldi; outbox — hammasi tarqatilgan |
|asosiy| — Vitest loyihasi nomi (Navbatlar darsidagi projects). Testcontainers bu safar 10/10 — migratsiya 3.4 s.
Jonli tekshiruvda ilova ishga tushishi — 2.2–2.3 s: S3 bucket tayyorlash, SMTP verify() va Redis subscriber ulanishi bilan.
10.2 Commit
git add -A
M="feat(api): 29-qism integratsiyalari NestJS'da — fayllar (MinIO),"
M+=" email, bildirishnomalar, Telegram, webhook obunalari, SSE,"
M+=" BullMQ va outbox (30-qism)"
git commit -m "$M"git status ga qarang: .env, dist/, coverage/ kirmasin.
Bu commit nest tarmog'ida. Uni Nest CLI va loyiha tuzilmasi darsida ochgandingiz. Endi bu tarmoq main dagi Express API qila oladigan hamma narsani qiladi. Unda 28-qism auth'i ham, 29-qism integratsiyalari ham bor — xuddi o'sha testlar ostida. git log --oneline main..nest buyrug'i 30-qismda nest ga qo'shilgan hamma commit'ni ko'rsatadi: skeletdan shu commit'gacha butun ko'chish tarixi.
Shu bilan 30-qismning loyiha qismi tugadi. vazifalar-api 25–29-qismlardagi hamma narsa bilan — REST API, PostgreSQL va Prisma, Redis kesh, auth va xavfsizlik, fayllar, email, bildirishnomalar, Telegram, webhook'lar, SSE, navbat va outbox — NestJS 12 da ishlaydi. HTTP shartnomasi o'zgarmadi: 25–29-qismlarning testlari Nest'da o'tdi.
11. Real ishda
- Gateway — Nest'dagi chat, bildirishnoma va "kim onlayn" funksiyalarining standart yo'li, ko'pincha Socket.IO bilan. Intervyuda "WebSocket'ni qanday himoya qilasiz?" savoliga to'g'ri javob — handshake'da auth va Origin, guard emas.
- SSE vs WebSocket — ko'p loyihalar WebSocket'ni "zamonaviy" deb tanlaydi, lekin bir tomonlama hodisalar uchun SSE oddiyroq: HTTP auth, proxy, qayta ulanish brauzerda tayyor.
- Sekin mijoz — prodakshndagi xotira oqishlarining tez-tez uchraydigan sababi. Har realtime server buferni cheklashi kerak.
- Intervyu: "Nest gateway'da guard qayerda ishlaydi?", "CSWSH nima?", "Ko'p server nusxasida WebSocket qanday masshtablanadi?", "Qachon SSE, qachon WebSocket?".
Xulosa
- Gateway — WebSocket uchun provider:
@WebSocketGateway,@SubscribeMessage,@MessageBody,@ConnectedSocket, lifecycle interfeyslari; adapter —WsAdapteryokiIoAdapter. - Guard'lar faqat xabarlarga; ulanish
handleConnectionda cookie vaOrigin(CSWSH) bilan qo'lda tekshiriladi. WsExceptionva rad etgan guard mijozgaexceptionhodisasi bo'lib boradi;handleDisconnectrad etilgan ulanish uchun ham chaqiriladi.- O'lchov (100 × 500):
SseHub173 ms, gateway 722 ms,@Sse1 317 ms;@Ssesekin mijozni buferlaydi,SseHub256 KB da uzadi. - Kanon: realtime — SSE (
/v1/oqim,SseHub), gateway kanonga kirmaydi; integratsiyalar ko'chishi yakunlandi: Vitest 298/299, e2e 10/10, Testcontainers 10/10.
Keyingi dars: SQLite — 31-qism: prodakshn backend. Endi vazifalar-api tayyor; navbatda ma'lumotlar bazalarining boshqa turlari, replikatsiya va yuqori mavjudlik.
Manbalar
- NestJS hujjati — Gateways, Exception filters, Guards, Adapters: https://docs.nestjs.com/websockets/gateways
- OWASP — Cross-Site WebSocket Hijacking (WebSocket Security Cheat Sheet): https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html
- RFC 6455 — WebSocket yopish kodlari (4000–4999 — ilovalar uchun): https://www.rfc-editor.org/rfc/rfc6455#section-7.4.2
- Kanon (IlmHamroh, 2026-11-01): gateway va
@Sse/SseHubtajribalari,OqimController,test/nest-integratsiya.test.ts, yakuniy tekshiruv. Demo gateway (handshake,WsException, guard, lifecycle) — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, @nestjs/platform-ws 12.2.0, ws 8.22.0, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!