IP, port, TCP và UDP
Tiếp đi. IP với port khác nhau chỗ nào, mình hay nói "gọi 10.0.1.4:8080" mà chưa hiểu rõ.
Bài trước có bản đồ. Giờ zoom vào chặng 2: developer gọi order-service trên Azure VM có private IP 10.0.1.4, app listen port 8080.
1. Bài toán
curl http://10.0.1.4:8080/orders/123
Trên cùng VM đó còn có Nginx (80/443), SSH (22). Làm sao gói tin biết phải tới đúng order-service chứ không phải SSH?
2. Vấn đề
- IP chỉ xác định máy (chính xác hơn: network interface).
- Một máy chạy nhiều process cùng nhận mạng. Cần thêm một con số để chọn process.
- Và cần quy tắc gửi: có đảm bảo tới đủ, đúng thứ tự không?
3. Giải pháp: IP + port + giao thức transport
IP = địa chỉ tòa nhà 10.0.1.4
Port = số phòng 8080 (order-service), 22 (SSH), 443 (Nginx)
TCP/UDP = cách giao hàng có ký nhận (TCP) / ném qua cửa (UDP)
- Port là số 16-bit (0–65535). 0–1023 là well-known (22 SSH, 80 HTTP, 443 HTTPS, 5432 Postgres là registered port).
- Destination port: port của server (8080). Source port: port phía client, do OS tự chọn từ dải ephemeral (Linux mặc định
32768–60999). - Một TCP connection được định danh bằng 4-tuple:
(src IP, src port, dst IP, dst port). Vì vậy một server port 8080 phục vụ được hàng nghìn client cùng lúc.
4. Từng bước
Bước 1: Xem app đang listen gì (trên VM):
sudo ss -ltnp
State Local Address:Port Process
LISTEN 0.0.0.0:22 sshd
LISTEN 0.0.0.0:80 nginx
LISTEN 127.0.0.1:8080 java <-- chỉ nhận từ chính máy này
LISTEN *:8080 java (nếu bind tất cả interface)
-l listening, -t TCP, -n không đổi số thành tên, -p process.
Bước 2: Chọn bind address trong application.yml:
server:
port: 8080
address: 0.0.0.0 # mọi interface (mặc định của Spring Boot)
# address: 127.0.0.1 # chỉ loopback: dùng khi có Nginx đứng trước
| Bind | Ai gọi được |
|---|---|
127.0.0.1 | Chỉ process trên cùng máy (Nginx cùng VM) |
10.0.1.4 | Chỉ qua interface có IP đó |
0.0.0.0 | Mọi interface (loopback, private, ...), còn tùy firewall |
Bước 3: Xem connection thật khi developer gọi:
ss -tn state established '( sport = :8080 )'
Local 10.0.1.4:8080 Peer 10.0.0.25:51734
Local 10.0.1.4:8080 Peer 10.0.0.25:51736 <-- cùng client, khác source port
Còn order-service gọi Postgres thì chiều ngược lại: order-service là client, source port ephemeral, destination 10.0.2.5:5432.
Bước 4: Test
nc -zv 10.0.1.4 8080
Connection to 10.0.1.4 8080 port [tcp/*] succeeded!
5. Cách hoạt động: TCP 3-way handshake
Laptop 10.0.0.25:51734 VM 10.0.1.4:8080
| ---- SYN (seq=x) -----------------> |
| <--- SYN-ACK (seq=y, ack=x+1) ----- | kernel trả lời, chưa tới Java
| ---- ACK (ack=y+1) ---------------> |
| ESTABLISHED | vào accept queue
| | Tomcat accept() -> xử lý
| ---- HTTP GET /orders/123 ---------> |
| <--- HTTP 200 JSON ----------------- |
| ---- FIN / ACK ... (đóng) ---------> |
- Handshake do kernel làm. Nếu không có ai listen port 8080, kernel trả RST → client thấy
Connection refused. - Nếu SYN bị firewall vứt đi, client không nhận gì → chờ tới
Connection timed out. - Bên đóng trước sẽ ở trạng thái
TIME_WAITmột lúc; client tạo connection mới liên tục mà không reuse có thể cạn ephemeral port. Đây là lý do dùng connection pool / keep-alive.
6. So sánh TCP và UDP
| TCP | UDP | |
|---|---|---|
| Kết nối | Có, handshake trước | Không, gửi luôn |
| Đảm bảo tới / đúng thứ tự | Có (ACK, retransmit) | Không |
| Flow/congestion control | Có | Không |
| Overhead / latency | Cao hơn | Thấp |
| Dùng cho | HTTP/1.1, HTTP/2, JDBC Postgres, SSH, Kafka | DNS (phần lớn), video/voice, game, QUIC/HTTP/3 |
| Java | Socket, ServerSocket | DatagramSocket |
7. Khi nào dùng
- Backend Java: gần như luôn là TCP (HTTP, DB, message broker).
- UDP khi mất vài gói vẫn chấp nhận được và cần latency thấp, hoặc protocol tự lo tin cậy (QUIC).
- Bind
127.0.0.1khi có reverse proxy cùng máy;0.0.0.0khi service phải nhận trực tiếp từ máy khác (và phải có firewall).
ping 10.0.1.4 thấy trả lời, nhưng curl :8080 lại không được. Vì sao?Vậy 1 server port 8080 có phải chỉ nhận được 65535 connection không?
Không, đây là hiểu lầm phổ biến. Giới hạn 65535 là của source port trên một client tới cùng một đích. Server phân biệt connection bằng cả 4-tuple, nên nhiều client khác IP/port đều vào chung 8080 được. Giới hạn thực tế ở server là file descriptor (ulimit -n), RAM, và số thread của Tomcat.
Trong Docker, mình EXPOSE 8080 rồi mà máy host vẫn không gọi được?
Đúng, EXPOSE chỉ là tài liệu. Port 8080 trong container và port 8080 trên host là hai thứ khác nhau. Phải map:
docker run -p 8080:8080 order-service
# host:container
Và bên trong container app phải listen 0.0.0.0. Nếu listen 127.0.0.1 thì đó là loopback của container, port mapping cũng không vào được.
Phỏng vấn hỏi về TCP/UDP và port thì trả lời sao?
Q1. TCP khác UDP thế nào?
🇻🇳 Tiếng Việt:
TCP là connection-oriented: bắt tay 3 bước, đảm bảo dữ liệu tới đủ, đúng thứ tự, có flow và congestion control. UDP không kết nối, không đảm bảo, nhưng nhẹ và latency thấp. HTTP/1.1, HTTP/2, JDBC dùng TCP; DNS, streaming, HTTP/3 (qua QUIC) dùng UDP.
🇬🇧 English:
TCP is connection-oriented: it uses a three-way handshake and guarantees reliable, ordered delivery with flow and congestion control. UDP is connectionless with no delivery guarantee, but it is lightweight and low latency. HTTP/1.1, HTTP/2 and JDBC use TCP; DNS, media streaming and HTTP/3 over QUIC use UDP.
Q2. Mô tả TCP 3-way handshake.
🇻🇳 Tiếng Việt:
Client gửi SYN, server trả SYN-ACK, client gửi ACK. Sau đó connection ESTABLISHED và nằm trong accept queue chờ ứng dụng accept. Nếu port không có ai listen, server trả RST, client thấy connection refused.
🇬🇧 English:
The client sends SYN, the server answers SYN-ACK, and the client sends ACK. The connection is then established and waits in the accept queue until the application accepts it. If nothing listens on the port, the server replies with RST and the client sees connection refused.
Q3. 0.0.0.0 và 127.0.0.1 khác gì khi bind server?
🇻🇳 Tiếng Việt:
127.0.0.1chỉ nhận connection từ chính máy đó.0.0.0.0nhận trên mọi interface. Khi có Nginx cùng máy, em bind Spring Boot vào127.0.0.1để không lộ port 8080.
🇬🇧 English:
127.0.0.1only accepts connections from the same machine, while0.0.0.0listens on all interfaces. When Nginx runs on the same host, I bind Spring Boot to127.0.0.1so port 8080 is not exposed.
Cách nhớ ngắn gọn
- IP = tòa nhà, port = số phòng.
- Connection = 4-tuple; client dùng ephemeral port.
- Refused = có RST (không ai listen); timeout = không ai trả lời (bị chặn).
- Backend Java gần như toàn TCP.
Tự kiểm tra
Ephemeral port là gì?
Port tạm OS cấp cho phía client khi mở connection ra ngoài, ví dụ 51734 khi gọi tới :8080.
4-tuple gồm những gì?
Source IP, source port, destination IP, destination port.
Lệnh nào xem process nào đang listen port 8080 trên Linux?
sudo ss -ltnp | grep 8080.
Ai trả lời SYN-ACK: kernel hay Tomcat?
Kernel. Tomcat chỉ accept() connection đã hoàn tất handshake.
Vì sao DNS thường dùng UDP?
Câu hỏi/trả lời nhỏ, cần nhanh; không cần handshake. Trả lời lớn thì chuyển sang TCP.