Port & socket — localhost:8080 thực sự là gì
Port định danh ứng dụng, socket là điểm cuối kết nối, 5-tuple định danh một kết nối: vì sao một cổng 443 phục vụ được nghìn client, 0.0.0.0 vs 127.0.0.1, EADDRINUSE.
TL;DR: Tới đúng máy (IP) chưa đủ: một máy chạy nhiều dịch vụ, nên cần port (số 0–65535) để vào đúng ứng dụng. Một socket là điểm cuối giao tiếp gắn với (IP, port). Điều khiến một web server lắng nghe một cổng 443 mà phục vụ được hàng nghìn client cùng lúc là 5-tuple: (protocol, src IP, src port, dst IP, dst port) định danh duy nhất mỗi kết nối, và client khác nhau ở IP/port nguồn nên không lẫn. Bind 127.0.0.1 là chỉ nội bộ, 0.0.0.0 là mọi interface. Hai tiến trình không thể bind cùng IP:port, tiến trình thứ hai nhận EADDRINUSE. Soi bằng ss/lsof.
npm run dev báo "listening on localhost:3000", ssh user@host đi tới cổng 22, EADDRINUSE: address already in use :::8080 — bạn gặp port mỗi ngày. Nhưng localhost:8080 thực sự nghĩa là gì? Và bí ẩn lớn hơn: một web server chỉ lắng nghe cổng 443, làm sao nó phục vụ hàng nghìn người dùng đồng thời mà không lẫn lộn ai với ai?
Bài này giải thích port, socket, và khái niệm 5-tuple — chìa khoá trả lời cả hai câu hỏi.
1. Analogy — Số điện thoại công ty + máy lẻ
Một công ty có một số điện thoại (như IP của máy), nhưng hàng trăm máy lẻ (extension) bên trong. Bạn gọi số công ty rồi bấm "108" để vào đúng phòng. Tổng đài phân biệt hàng trăm cuộc gọi đồng thời nhờ biết ai gọi từ đâu tới máy lẻ nào.
| Công ty | Mạng |
|---|---|
| Số điện thoại công ty | Địa chỉ IP (đúng máy) |
| Máy lẻ "108" | Port (đúng ứng dụng) |
| Tổng đài phân biệt từng cuộc | 5-tuple phân biệt từng kết nối |
IP đưa bạn tới đúng toà nhà; port đưa tới đúng phòng. localhost:8080 = "máy này, ứng dụng đang nghe ở cổng 8080".
2. Port — số định danh ứng dụng
Port là số nguyên 0–65535 ở tầng Transport, cho biết gói dữ liệu thuộc về ứng dụng nào trên máy. Chia ba nhóm:
| Nhóm | Dải | Ví dụ |
|---|---|---|
| Well-known | 0–1023 | 80 (HTTP), 443 (HTTPS), 22 (SSH), 53 (DNS) |
| Registered | 1024–49151 | 5432 (PostgreSQL), 3306 (MySQL), 6379 (Redis) |
| Ephemeral (tạm) | 49152–65535 | port nguồn OS tự cấp cho client |
Khi bạn mở một web, đích là IP:443 (cổng cố định của server), còn nguồn là IP_của_bạn:<port tạm> — OS tự chọn một ephemeral port cho kết nối đó. Port chỉ là con số; thứ OS thực sự cấp cho ứng dụng để dùng con số đó là socket.
3. Socket — điểm cuối kết nối
Socket là giao diện hệ điều hành cấp cho ứng dụng để gửi/nhận qua mạng — gắn với một cặp (IP, port). Có hai vai trò. Listening socket là cái server tạo một lần: gọi bind(IP, port) rồi listen(), nghĩa là "tôi nhận kết nối ở cổng này", ví dụ web server bind 0.0.0.0:443. Connection socket sinh ra cho từng client: mỗi client kết nối tới, accept() ở server (và connect() ở client) tạo một socket riêng cho kết nối đó. Vậy một server có một listening socket nhưng nhiều connection socket, mỗi client một cái. Nhìn thấy cả hai loại trên một web server thật bằng ss:
ss -tan | head -4
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
# ESTAB 0 0 10.0.0.5:443 203.0.113.7:51001
# ESTAB 0 0 10.0.0.5:443 198.51.100.9:60200
Dòng LISTEN là listening socket: nó chỉ có địa chỉ cục bộ 0.0.0.0:443, cột Peer để * vì chưa gắn với ai. Hai dòng ESTAB là hai connection socket, mỗi dòng một client đang nối. Nhìn kỹ hai dòng đó: phía server trùng nhau hoàn toàn (10.0.0.5:443), chúng khác nhau chỉ ở IP và port nguồn của client. Bốn giá trị đó cộng với giao thức TCP là bộ định danh mà mục tiếp theo gọi tên.
4. 5-tuple — vì sao một cổng phục vụ nghìn client?
Điểm cốt lõi: 5 thành phần (5-tuple) định danh duy nhất một kết nối TCP:
Hai client cùng kết nối tới server:443. Chúng trùng dst IP và dst port — nhưng khác src IP hoặc src port. Vậy 5-tuple của hai kết nối khác nhau, OS coi chúng là hai kết nối tách biệt, không lẫn. Nhờ thế một cổng 443 phục vụ được vô số client:
TCP 203.0.113.7:51001 -> 10.0.0.5:443 (client A)
TCP 203.0.113.7:51002 -> 10.0.0.5:443 (client A, ket noi 2)
TCP 198.51.100.9:60200 -> 10.0.0.5:443 (client B)
Cả ba cùng 10.0.0.5:443 nhưng khác nguồn, nên là ba kết nối độc lập.

Thử ngẫmmáy bạn cạn ephemeral port, chỉ còn đúng một cổng nguồn rảnh. Client đó mở được mấy kết nối cùng lúc tới server:443, và 5-tuple giải thích thế nào?
5. localhost, 127.0.0.1 và 0.0.0.0
Khi bind, địa chỉ quyết định ai kết nối được (nối lại bài IP). Bind 127.0.0.1 (localhost) thì chỉ nhận kết nối từ chính máy này, an toàn cho dịch vụ nội bộ như database dev. Bind 0.0.0.0 thì lắng nghe trên mọi interface, kể cả từ máy khác trong LAN hay Internet nếu có đường vào. Bind nhầm 0.0.0.0 cho thứ đáng lẽ chỉ nội bộ là một lỗi bảo mật phổ biến — vô tình mở dịch vụ ra ngoài. Địa chỉ quyết định ai vào được. Còn khi hai tiến trình cùng đòi một địa chỉ, OS không cho cả hai cùng giữ; mục sau là thông báo lỗi của tình huống đó.
6. EADDRINUSE — vì sao "address already in use"
Hai tiến trình không thể cùng bind một cặp IP:port. Chạy hai server trên :8080, cái thứ hai báo EADDRINUSE. Tìm thủ phạm:
ss -tlnp # liet ke socket dang LISTEN + tien trinh
# LISTEN 0 511 0.0.0.0:8080 ... users:(("node",pid=4321,...))
lsof -i :8080 # cach khac, theo cong
Đôi khi cổng vẫn "bận" một lúc sau khi tắt server do trạng thái TIME_WAIT của TCP — chi tiết ở course TCP. Tạm thời: chờ vài giây hoặc dùng option tái sử dụng địa chỉ.
Thử ngẫmbạn restart server ngay sau khi tắt, EADDRINUSE xuất hiện dù ss -tlnp không thấy tiến trình nào khác giữ :8080. Nghi ngờ tiếp theo là gì?
7. Pitfall — hiểu nhầm thường gặp
❌ Nhầm 1: "Một cổng chỉ phục vụ được một client một lúc." ✅ Một listening socket trên cổng 443 đẻ ra nhiều connection socket, mỗi client một cái, phân biệt bằng 5-tuple. Một cổng phục vụ vô số kết nối đồng thời.
❌ Nhầm 2: "localhost:3000 thì ai cũng vào được."
✅ Bind 127.0.0.1:3000 chỉ chấp nhận kết nối từ chính máy đó. Muốn máy khác vào phải bind 0.0.0.0 (và có đường mạng/port forwarding).
❌ Nhầm 3: "EADDRINUSE là bug của code tôi."
✅ Thường là một tiến trình khác (hoặc lần chạy trước chưa thoát hẳn) đang giữ cổng, hoặc TIME_WAIT. Dùng ss -tlnp/lsof -i tìm tiến trình giữ cổng trước khi nghi code.
8. 📚 Deep Dive — tài liệu gốc
Đọc khi muốn tới gốc port/socket:
- RFC 9293 — TCP: khái niệm port và cách định danh một kết nối.
- IANA Service Name and Port Registry — danh sách well-known port chính thức.
- Berkeley sockets API —
socket(),bind(),listen(),accept(),connect().
Ghi chú: "Socket" vừa là khái niệm (điểm cuối (IP, port)) vừa là API hệ điều hành. Chi tiết vòng đời socket TCP sẽ học ở course TCP.
9. Tóm tắt
- Port (0–65535) định danh ứng dụng trên một máy; well-known (0–1023), registered, ephemeral (OS cấp cho client).
- Socket là điểm cuối
(IP, port): một listening socket ở server, nhiều connection socket cho từng client. - 5-tuple
(protocol, src IP, src port, dst IP, dst port)định danh duy nhất mỗi kết nối — nên một cổng 443 phục vụ được vô số client (khác src). - Bind
127.0.0.1= chỉ nội bộ;0.0.0.0= mọi interface (cẩn thận lộ dịch vụ). - Hai tiến trình không bind cùng
IP:port, cái sau nhậnEADDRINUSE; soi bằngss -tlnp/lsof -i.
10. Tự kiểm tra
- Q1Tới đúng máy bằng IP rồi, vì sao vẫn cần port?
- Q2Một web server chỉ lắng nghe cổng 443 nhưng phục vụ hàng nghìn client cùng lúc mà không lẫn. Cơ chế nào cho phép?
- Q3Bind server vào 127.0.0.1 khác 0.0.0.0 thế nào, và vì sao nhầm lẫn có thể thành lỗ hổng?
- Q4Bạn chạy server thì gặp
EADDRINUSE. Nên kiểm tra gì trước khi nghi code? - Q5Bạn thấy
ss -tanliệt kê một dòng LISTEN và ba dòng ESTAB cùng cổng 443. Dòng nào là listening socket, dòng nào là connection socket, và vì sao chúng cùng tồn tại? - Q6Khi bạn mở một kết nối tới
server:443, vì sao client không cần tự chọn một cổng cố định, và cổng nguồn đó từ đâu ra?
Bài tiếp theo: DNS — phân giải tên miền hoạt động thế nào
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Module 3 — Port, Socket & DNS: tổng quan
Bài tiếpDNS — phân giải tên miền hoạt động thế nào
Thảo luận & góp ý
Hỏi đáp về bài này