Network Programming · Part 4 — DNS & Addressing: From Names to IPs
How DNS turns hostnames into IP addresses, the resolution chain from your app to authoritative servers, Node.js dns.lookup vs dns.resolve, record types, TTL caching, and dual-stack IPv4/IPv6 — bilingual, with runnable TypeScript examples.
Đây là Phần 4 của series 10 bài về lập trình mạng với Node.js + TypeScript. Phần 1–3 đã cho bạn mô hình phân tầng, TCP và UDP. Giờ ta xử lý khoảng cách giữa tên dễ nhớ cho con người và thứ socket thực sự cần: địa chỉ IP.
Mỗi lần bạn connect('api.example.com', 443) hay fetch('https://example.com'), phải có thứ gì đó dịch hostname thành địa chỉ có thể định tuyến trước khi một packet rời máy bạn. Đó là DNS — Hệ thống Tên Miền.
Vì sao cần DNS
Con người nhớ tên; Internet định tuyến theo số. 93.184.216.34 khó gõ và không thể “di chuyển” khi server đổi máy. example.com ổn định — bạn chỉ cập nhật bản ghi DNS phía sau tên khi hạ tầng thay đổi.
DNS là cơ sở dữ liệu phân tán ánh xạ tên sang địa chỉ (và metadata khác). Ứng dụng gần như không nói chuyện trực tiếp ngay — bạn truyền hostname cho net.connect, http.request, hay fetch, và runtime thực hiện bước phân giải tên trước khi mở socket.
Ý chính: socket kết nối tới IP; DNS là cầu nối từ hostname → IP.
Chuỗi phân giải
Khi process Node hỏi “IP của example.com là gì?”, truy vấn thường đi qua nhiều bước — mỗi tầng có thể cache câu trả lời.
- Ứng dụng: gọi
dns.lookup,dns.resolve4, hoặc đểnet/httptự phân giải. - Resolver đệ quy: thường là server OS cấu hình — nó làm phần việc nặng và cache kết quả.
- Server gốc + TLD:
.gốc.chỉ tới nameserver.com, rồi chỉ tới authority của domain. - Nameserver chủ quyền: nguồn sự thật cho
example.com— trả về bản ghi A / AAAA thực tế.
Câu trả lời kèm TTL — cache được giữ bản ghi bao lâu trước khi hỏi lại.
Các loại bản ghi DNS phổ biến
| Record | Mục đích | Ví dụ |
|---|---|---|
| A | địa chỉ IPv4 | example.com → 93.184.216.34 |
| AAAA | địa chỉ IPv6 | example.com → 2606:2800:220:1:248:1893:25c8:1946 |
| CNAME | bí danh trỏ tới tên khác | www.example.com → example.com |
| MX | server mail + độ ưu tiên | example.com → 10 mail.example.com |
| TXT | văn bản tùy ý | example.com → "v=spf1 include:..." |
| NS | nameserver của vùng | example.com → ns1.example.com |
| SRV | vị trí dịch vụ | _xmpp._tcp → 5269 xmpp.example.com |
Với lập trình socket, bạn quan tâm nhất A và AAAA — chúng cho IP để connect(). CNAME nghĩa là “phân giải tên khác trước”; MX / TXT / SRV dùng cho mail, xác thực, discovery — không phải client TCP thường.
So sánh lookup và resolve
Module node:dns có hai họ API mà người mới hay nhầm.
| API | Làm gì | Bên trong |
|---|---|---|
dns.lookup | Hostname → IP để kết nối | resolver OS qua getaddrinfo — cùng đường net / http dùng |
dns.resolve / dns.promises.resolve4 | Truy vấn DNS cho loại bản ghi cụ thể | Nói trực tiếp với server DNS |
Khác biệt quan trọng:
dns.lookuptuân cấu hình OS (/etc/hosts, mDNS, VPN) và có thể trả IPv4 hoặc IPv6 tùy chính sách hệ thống.dns.resolve4luôn hỏi mạng lấy bản ghi A — bạn nhận đúng DNS nói, không phải OS “ưu tiên” khi kết nối.dns.lookupdùng libuv thread pool chogetaddrinfoblocking — nhiều lookup đồng thời có thể đói việc khác trong pool (file I/O, crypto).dns.promises.resolve*async hoàn toàn từ góc nhìn Node — tốt hơn khi tra DNS hàng loạt.
Dùng lookup khi muốn “địa chỉ tôi sẽ kết nối”. Dùng resolve4 / resolve6 / resolveMx khi cần dữ liệu DNS thô hoặc nhiều câu trả lời.
Phân giải tên bằng TypeScript
Lưu thành resolve-demo.ts và chạy bằng npx tsx resolve-demo.ts.
Đường resolver OS
import { lookup } from 'node:dns/promises';
// What net.connect() effectively uses: one "best" address for connecting.
const { address, family } = await lookup('example.com');
console.log(`lookup → ${address} (IPv${family})`);
// e.g. lookup → 93.184.216.34 (IPv4)
family là 4 hoặc 6 — cho biết OS chọn stack nào. Truyền \{ all: true \} để lấy mọi địa chỉ OS sẽ thử (hữu ích debug dual-stack):
import { lookup } from 'node:dns/promises';
const results = await lookup('example.com', { all: true });
for (const r of results) {
console.log(` ${r.address} IPv${r.family}`);
}
Truy vấn bản ghi A trực tiếp
import { resolve4 } from 'node:dns/promises';
// Talks to DNS servers; returns ALL A records (may be multiple!).
const ipv4s = await resolve4('example.com');
console.log('A records:', ipv4s);
// A records: [ '93.184.216.34' ]
Site lớn thường trả nhiều bản ghi A để cân bằng tải (round-robin DNS). OS / client có thể thử theo thứ tự hoặc chọn một — đừng giả định một tên = một IP.
Định tuyến mail
import { resolveMx } from 'node:dns/promises';
const mx = await resolveMx('gmail.com');
// Sorted by priority (lower = preferred).
for (const record of mx) {
console.log(`priority ${record.priority} → ${record.exchange}`);
}
Tra ngược IP → hostname
import { reverse } from 'node:dns/promises';
const hostnames = await reverse('93.184.216.34');
console.log('PTR:', hostnames);
// PTR: [ 'example.com', 'www.example.com', ... ] (depends on PTR record)
Reverse DNS dùng bản ghi PTR; thiếu PTR là bình thường, không ảnh hưởng kết nối thuận.
IPv4, IPv6 và dual-stack
Internet vẫn chủ yếu IPv4 (bốn byte). IPv6 giải quyết cạn kiệt địa chỉ và đơn giản hóa định tuyến.
Máy dual-stack có cả hai:
- A record → IPv4
- AAAA record → IPv6
Khi lookup('example.com') không gợi ý, Node/OS áp dụng chính sách kiểu Happy Eyeballs: thử địa chỉ có độ trễ thấp, thường ưu tiên IPv6 nếu có.
import { lookup, resolve4, resolve6 } from 'node:dns/promises';
const v4only = await lookup('example.com', { family: 4 });
const v6only = await lookup('example.com', { family: 6 }).catch(() => null);
const allA = await resolve4('example.com');
const allAAAA = await resolve6('example.com').catch(() => [] as string[]);
console.log({ v4only, v6only, allA, allAAAA });
resolve6 có thể ném ENODATA nếu không có AAAA — luôn xử lý trong code production.
TTL, cache và câu trả lời cũ
Mỗi phản hồi DNS có TTL tính bằng giây. Resolver và OS cache ánh xạ đến khi TTL hết hạn.
| Layer | Cache gì |
|---|---|
| OS resolver | Kết quả lookup gần đây |
| Recursive resolver (8.8.8.8, etc.) | Câu trả lời theo TTL từ upstream |
| Your app | Chỉ khi bạn cache — Node không cache resolve4 giữa các lần gọi |
Hàm ý cho lập trình viên mạng:
- Sau deploy hay failover, một số client vẫn tới IP cũ đến khi TTL hết.
- TTL thấp (vd 60s) lan nhanh nhưng tăng tải truy vấn DNS.
- Gọi
resolve4mỗi request HTTP lãng phí —lookup+ tái sử dụng kết nối (Phần 5+) là đường thường gặp.
Xem TTL bằng tùy chọn resolve4 (Node 18+) hoặc dig:
dig example.com A +noall +answer
# example.com. 86400 IN A 93.184.216.34
# ^^^^^ TTL in seconds
Lookup ngầm khi connect
Bạn không bắt buộc gọi dns trước. Truyền hostname cho net.connect sẽ tự phân giải:
import { connect } from 'node:net';
// Implicit dns.lookup inside — blocks threadpool briefly, then connects.
const socket = connect({ host: 'example.com', port: 80 }, () => {
console.log('connected to', socket.remoteAddress, 'port', socket.remotePort);
socket.end();
});
socket.on('error', (err) => console.error('connect failed:', err.message));
Trình tự: phân giải hostname → chọn địa chỉ → bắt tay TCP (Phần 2). DNS lỗi → ENOTFOUND trước khi packet tới server. Đã có IP thì truyền thẳng, bỏ qua DNS.
Lỗi người mới hay mắc
- Dùng
resolve4khi ý làlookup— nguồn dữ liệu và ngữ nghĩa khác nhau. - Bỏ qua TTL và cache — thắc mắc vì sao client vẫn tới IP chết sau khi đổi DNS.
- Giả định một hostname = một IP — round-robin trả nhiều A; kết nối có thể tới máy khác nhau.
- Gọi hàng nghìn
dns.lookupsong song — cạn libuv thread pool và làm chậm việc async khác.
Bài tập
Thử từng bài trước khi mở lời giải.
- Chạy cả hai và so sánh kết quả, giải thích khác biệt.
- Gọi
resolve4('google.com')hai lần — đếm số IP; giải thích vì saoconnectvẫn hoạt động. - Viết hàm trả về cả IPv4 và IPv6, mảng rỗng khi thiếu.
Lời giải
import { lookup, resolve4, resolve6 } from 'node:dns/promises';
// 1. localhost — OS resolver vs direct DNS
const os = await lookup('localhost');
const direct = await resolve4('localhost');
console.log('lookup:', os); // often ::1 (IPv6) on modern macOS/Linux
console.log('resolve4:', direct); // often ['127.0.0.1'] from DNS/hosts
// lookup follows getaddrinfo policy; resolve4 asks DNS for A records only.
// 2. google.com — many A records, connect picks one
const ips = await resolve4('google.com');
console.log(`${ips.length} A records:`, ips.slice(0, 3), '...');
// connect() uses lookup + Happy Eyeballs; one successful TCP handshake is enough.
// 3. resolveAll
async function resolveAll(host: string): Promise<{ ipv4: string[]; ipv6: string[] }> {
const [ipv4, ipv6] = await Promise.all([
resolve4(host).catch(() => [] as string[]),
resolve6(host).catch(() => [] as string[]),
]);
return { ipv4, ipv6 };
}
console.log(await resolveAll('example.com'));lookup có thể trả ::1 còn resolve4 trả 127.0.0.1 vì OS ưu tiên IPv6 loopback nhưng bản ghi A vẫn là IPv4. Google có nhiều A để cân tải; connect chỉ cần một địa chỉ hoạt động.
Điều cốt lõi
DNS nối tên con người với địa chỉ máy qua chuỗi phân giải phân cấp có cache. Trong Node, dns.lookup đi theo đường OS mà net và http dùng; dns.resolve* hỏi DNS trực tiếp theo loại bản ghi. Tôn trọng TTL, chấp nhận nhiều IP, tránh làm ngập lookup trên thread pool. Đã phân giải tên, Phần 5 xây HTTP thật trên socket TCP bạn đã biết.