Truyền dữ liệu giữa các máy: socket, UDP và gửi file an toàn
Câu hỏi xuyên suốt: Lan cần gửi file danh sách thành viên từ laptop sang máy của thủ quỹ trong cùng mạng Wi-Fi. Có những cách nào, dữ liệu đi qua đâu, ai đọc được, và làm sao chắc file đến đủ, đúng, từ đúng người?
Thời lượng: khoảng 100 phút (giảng và thực hành xen kẽ, 6 bài tập, bài cuối làm ở nhà)
Mạch bài: một file đi từ máy này sang máy kia
5 phútBài 8 cho bạn biết ai ở trong mạng. Bài này trả lời câu hỏi kế: khi đã biết máy kia ở 192.168.1.23, làm sao đưa dữ liệu sang đó?
thanh-vien.csv trên laptop, cần chuyển cho thủ quỹ trên một máy khác cùng Wi-Fi. Gửi qua email hoặc ứng dụng chat thì file đi qua máy chủ bên thứ ba. Trong mạng nội bộ, hai máy có thể nói chuyện trực tiếp. Nhưng ai khác trong mạng cũng ở cùng đường truyền: họ đọc được file không, gửi file giả vào được không, file có đến đủ không?Bài đi qua năm cách, từ đơn giản tới an toàn:
| Cách | Dùng khi | Mã hóa, xác thực | Phần |
|---|---|---|---|
Máy chủ web tạm (http.server) | Chia sẻ nhanh một thư mục | Không, ai cùng mạng cũng tải được | B |
| Socket TCP tự viết | Hiểu cách các máy nói chuyện; chương trình riêng | Tự làm | C, E |
| Socket UDP | Thông báo nhanh, tìm thiết bị | Không | D |
| TCP + TLS + token | Gửi file có bảo vệ trong chương trình tự viết | Có | F |
SSH/scp, ứng dụng chuyên dụng | Việc thật, dùng thường xuyên | Có, đã được kiểm chứng | F |
Nhận diện hai vai: ai có file, ai cần file?
Hai terminal có thể đóng hai vai trên cùng một laptop. Khi sang hai máy thật, vai trò vẫn giữ nguyên.
A: server / receiver → B: client / sender127.0.0.1 luôn chỉ chính máy đang dùng địa chỉ đó. Trong lab socket, A chờ nhận; B chủ động kết nối. Hai cửa sổ phải vào đúng thư mục transfer-lab.
Điều cần hiểu: Làm thành công trên một máy trước khi đổi IP.
sender.py thanh-vien.csv --host IP_MAY_AỞ phần E, máy nhận chạy receiver.py trước. Máy gửi chạy sender.py sau và dùng IP của máy nhận. Router/AP nối các máy trong LAN; dữ liệu cùng mạng không cần đi ra Internet.
Điều cần hiểu: File mới nằm trong received/ ở máy A.
B: curl -O http://IP_MAY_A:8000/thanh-vien.txtỞ phần B, A có file và chạy web server; B yêu cầu tải. Server/client mô tả vai trò kết nối, không đồng nghĩa server luôn là bên nhận file.
Điều cần hiểu: Luôn hỏi: ai có file gốc, ai cần bản sao?
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Bấm gửi một câu giữa A và B, hoặc gửi hẳn một file CSV. Bạn có thể sửa dữ liệu, bấm chạy rồi xem cả hai màn hình nhận/gửi ngay trên trang.
- A–B: hiểu địa chỉ và tải một file bằng web.
- C–D: gửi một câu bằng TCP, rồi so với UDP.
- E: gửi file, nhận
OK, kiểm tra bản nhận. - F–G: bật TLS, đối chiếu rồi tắt dịch vụ.
Lần đầu hãy chọn một máy + hai terminal. Chỉ chuyển sang hai máy sau khi bản một máy chạy được.
Cần chuẩn bị
- Python 3.8 trở lên, terminal, trình duyệt,
curl,openssl(đã dùng ở bài 6). Wireshark (cũng bài 6) để quan sát. Code chỉ dùng thư viện có sẵn. - Hai máy cùng một mạng Wi-Fi sẽ thấy rõ nhất (laptop và một máy khác, hoặc laptop và điện thoại ở phần B). Nếu chỉ có một máy, vẫn làm được toàn bộ bằng hai cửa sổ terminal và địa chỉ
127.0.0.1. Bài đánh dấu chỗ nào cần đổi khi có hai máy.
A. Điều kiện để hai máy nói chuyện được với nhau
12 phútChương trình gửi chạy mà "không được" thường do một trong ba điều kiện thiếu. Đây là cách các lớp ở bài 6 (phần D) áp vào việc truyền dữ liệu.
1. Địa chỉ đúng: IP của máy nhận và port
Máy gửi cần hai số: IP của máy nhận (xem bằng lệnh bài 6, bước 1, hoặc bảng kiểm kê bài 8) và port mà chương trình nhận đang dùng. Trong lab, nối hai máy vào cùng mạng nội bộ không cách ly. Cùng mạng không có nghĩa luôn là /24: cần xem cả IP và mặt nạ mạng (bài 8). Những mạng con khác nhau vẫn có thể liên lạc nếu có định tuyến và được cho phép. IP riêng không được định tuyến trực tiếp trên Internet.
2. Có chương trình đang nghe, và nghe ở đâu
Chương trình nhận phải vào một địa chỉ và port. Đây là điểm bài 5 đã nói về :
| Bind vào | Ai kết nối được | Dùng khi |
|---|---|---|
127.0.0.1 | Chỉ chính máy đó | Thử nghiệm, một máy, hoặc dịch vụ chỉ dùng cục bộ |
192.168.1.23 (IP riêng của máy) | Mọi máy truy cập được tới IP đó, tức cùng mạng | Chia sẻ trong mạng nhà |
0.0.0.0 | Mọi card mạng của máy (Wi-Fi, Ethernet, VPN…) | Tiện nhưng rộng nhất. Hạn chế dùng |
Quên đổi 127.0.0.1 thành IP mạng là lỗi số một khi chuyển từ "chạy trên một máy" sang "hai máy".
3. Không bị firewall của máy chặn
Hệ điều hành mặc định chặn kết nối vào các chương trình lạ. macOS hỏi "Cho phép Python nhận kết nối đến?" (chọn Allow cho lần thử, nhớ gỡ sau). Windows hiện hộp thoại Windows Defender Firewall: chọn Private networks (mạng riêng), không chọn Public. Linux với ufw cần sudo ufw allow 9000/tcp (chỉ trong lúc thử, sau đó sudo ufw delete allow 9000/tcp).
Kiểm tra sau khi đã bật chương trình nhận
Chưa chạy receiver thì việc cổng chưa mở là bình thường. Có thể quay lại bảng này sau phần E; không cần sửa mạng chỉ vì port 9000 hiện chưa có ai nghe.
Trước khi viết code, kiểm tra đường tới máy nhận và port. Thay 192.168.1.23 và 9000 bằng giá trị thật:
ping -c 3 192.168.1.23
nc -vz 192.168.1.23 9000ping 192.168.1.23
Test-NetConnection 192.168.1.23 -Port 9000ping -c 3 192.168.1.23
nc -vz 192.168.1.23 9000| Phần | Ý nghĩa |
|---|---|
ping | Hỏi máy kia có trả lời không (bài 6). Không trả lời chưa chắc là lỗi: máy có thể chặn ping. |
nc -vz IP PORT | nc (netcat) thử mở kết nối TCP tới IP và port. -v in kết quả, -z chỉ thử kết nối rồi đóng, không gửi dữ liệu. |
Test-NetConnection IP -Port PORT | Lệnh PowerShell tương đương nc: báo TcpTestSucceeded : True nếu mở được. |
Đọc kết quả nhờ phần D của bài 6:
| Kết quả | Nghĩa | Việc kiểm tra tiếp |
|---|---|---|
succeeded / True | Có đường đi và có chương trình đang nghe | Chuyển sang viết hoặc chạy chương trình gửi |
Connection refused (bị từ chối ngay) | Thường do không có chương trình nghe ở địa chỉ/port đó; firewall cũng có thể chủ động từ chối | Chương trình nhận chưa chạy, sai port, hoặc đang bind ở 127.0.0.1 thay vì IP mạng |
Treo lâu rồi timed out | Gói không tới nơi, hoặc bị chặn im lặng | Firewall của máy nhận, hai máy khác mạng/mạng khách, sai IP |
No route to host | Không tìm được đường tới IP | Sai IP, khác mạng, máy kia đã tắt |
BÀI TẬP 1 · 8 PHÚT
Đoán lỗi từ kết quả kiểm tra
Máy nhận 192.168.1.23 chạy chương trình nghe ở port 9000. Mô tả ba lần thử từ máy gửi cùng mạng, nêu nguyên nhân hợp lý nhất và cách sửa:
nc -vz 192.168.1.23 9000báo "Connection refused". Trên máy nhận, chương trình in "Dang nghe 127.0.0.1:9000".- Kết quả treo rồi "timed out". Chương trình nhận đã bind
0.0.0.0, hai máy cùng Wi-Fi, máy nhận là Windows vừa cài Python. - Chạy được một lần từ máy gửi, nhưng khi đưa điện thoại vào cùng Wi-Fi để thử thì không kết nối được, dù cùng Wi-Fi và IP đúng.
Xem đáp án
- Chương trình nghe ở
127.0.0.1nên chỉ chính máy nhận kết nối được; từ máy khác, gói tới nơi nhưng không ai nghe ở IP mạng nên bị từ chối. Sửa: chạy lại với--host 0.0.0.0(hoặc IP mạng của máy). - Firewall của Windows chặn kết nối vào chương trình mới cho mạng hiện tại. Sửa: cho phép Python trong Windows Defender Firewall cho mạng Private (hoặc tạo quy tắc cho port 9000, và gỡ sau khi thử).
- Mạng đang bật cách ly thiết bị (AP isolation) hoặc điện thoại nối vào mạng khách; hoặc điện thoại đang dùng MAC riêng và router lọc theo MAC. Sửa: nối cùng mạng chính, kiểm tra cài đặt cách ly (bài 8).
B. Cách nhanh nhất: máy chủ web tạm và rủi ro của nó
10 phútMáy có file chạy một web server nhỏ; máy cần file mở trình duyệt để lấy bản sao. Ở phần này, server là bên có file, không phải máy nhận file.
Chia sẻ web: mở quầy, lấy file, đóng quầy
Theo thứ tự bên dưới: A mở một thư mục nhỏ, B tải đúng file, A tắt server.
python3 -m http.server 8000 --bind 127.0.0.1Server phục vụ thư mục hiện tại. Trong lần thử một máy, chỉ nghe ở 127.0.0.1. Để Terminal A chạy, không gõ lệnh tải file trong chính cửa sổ này.
Điều cần hiểu: Thấy dòng Serving HTTP …; A đứng chờ là đúng.
curl -O http://127.0.0.1:8000/thanh-vien.txtB gửi yêu cầu GET; A trả nội dung. Với một máy, hãy tải trong thư mục khác share để giữ file gốc và file nhận tách biệt.
Điều cần hiểu: A có dòng GET … 200; B có file tải về.
Ctrl+C ở Terminal ABản tải về vẫn tồn tại sau khi tắt server. Cách này không có đăng nhập và không mã hóa; chỉ dùng file giả cho lab, hoặc dữ liệu không nhạy cảm trên mạng tin cậy.
Điều cần hiểu: Tắt chia sẻ chỉ ngăn các lượt tải tiếp theo.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Bước 1 · Terminal A: tạo thư mục và file giả
mkdir -p ~/share
cd ~/share
printf "Danh sach thanh vien (du lieu gia)\n" > thanh-vien.txtNew-Item -ItemType Directory -Force $HOME\share
cd $HOME\share
Set-Content thanh-vien.txt "Danh sach thanh vien (du lieu gia)" -Encoding asciimkdir -p ~/share
cd ~/share
printf "Danh sach thanh vien (du lieu gia)\n" > thanh-vien.txtshare chỉ chứa file định chia sẻ. Server có thể phục vụ cả thư mục con; đừng chạy ở thư mục chứa tài liệu riêng.
Bước 2 · Terminal A: mở web server, giữ cửa sổ chạy
python3 -m http.server 8000 --bind 127.0.0.1py -m http.server 8000 --bind 127.0.0.1python3 -m http.server 8000 --bind 127.0.0.1Serving HTTP on 127.0.0.1 port 8000 ...Thấy thông báo đang phục vụ ở port 8000. Terminal A đứng chờ; hãy mở Terminal B, không dừng A lúc này.
Bước 3 · Terminal B: tải vào thư mục khác
mkdir -p ~/download-lab
cd ~/download-lab
curl -O http://127.0.0.1:8000/thanh-vien.txt
cat thanh-vien.txtNew-Item -ItemType Directory -Force $HOME\download-lab
cd $HOME\download-lab
curl.exe -O http://127.0.0.1:8000/thanh-vien.txt
Get-Content thanh-vien.txtmkdir -p ~/download-lab
cd ~/download-lab
curl -O http://127.0.0.1:8000/thanh-vien.txt
cat thanh-vien.txtDanh sach thanh vien (du lieu gia)-O lưu file với tên gốc; cat/Get-Content mở nội dung văn bản. Bạn cũng có thể mở http://127.0.0.1:8000/ trong trình duyệt và bấm tên file.
B có bản sao trong download-lab; A vẫn giữ bản gốc trong share và thường in dòng GET /thanh-vien.txt … 200.
Bước 4 · Dừng chia sẻ
Nhấn Ctrl+C ở A. Thử tải lại ở B: kết nối mới sẽ thất bại nếu không có chương trình khác giữ port 8000. Bản đã tải vẫn còn ở B.
Bước 5 · Khi đã có hai máy
Ở máy A có file, chạy lại server với --bind IP_MAY_A, thay bằng IP thật của A; ví dụ:
python3 -m http.server 8000 --bind 192.168.1.23py -m http.server 8000 --bind 192.168.1.23python3 -m http.server 8000 --bind 192.168.1.23Máy B cùng mạng truy cập http://192.168.1.23:8000/thanh-vien.txt; điện thoại cũng dùng trình duyệt được. Không dùng 127.0.0.1 trên B để gọi A. Nếu bị chặn, xem phần A.
Cách này có bảo vệ dữ liệu không?
- Không có đăng nhập: máy nào truy cập được tới server đều có thể yêu cầu tải.
- Không mã hóa: có thể đọc nội dung nếu quan sát được luồng HTTP; không phải cứ ở cùng Wi-Fi là tự động thấy mọi gói của nhau.
- Phải tắt khi xong: giữ Ctrl+C ở A như bước kết thúc lab.
Chỉ dùng file giả trong bài. Phần F giải thích cách che nội dung bằng TLS. 0.0.0.0 là cách nghe trên mọi card IPv4, không phải địa chỉ để nhập vào trình duyệt.
Nếu vừa thử hai máy ở bước 5, hãy dừng server bằng Ctrl+C ở A rồi mới chuyển sang phần C.
BÀI TẬP 2 · 5 PHÚT
Tự chứng minh server tạm lộ ra mạng
Với server phần B đang nghe ở IP mạng của A, ví dụ 192.168.1.23:8000, trả lời:
- Thiết bị thứ ba trong mạng (không phải máy gửi, không phải máy nhận) có tải được file không? Cần biết gì?
- Dùng công cụ nào ở bài 8 để thấy server này trong bảng kiểm kê?
- Bắt gói trên Wireshark khi tải file: nội dung file thấy được không?
Xem đáp án
- Được. Chỉ cần biết IP và port (đoán được hoặc thấy bằng quét), không cần mật khẩu.
lan_inventory.pysẽ in port 8000 làhttp-altkèm bannerServer: SimpleHTTP/0.6 Python/…ở thiết bị chạy server.- Bắt trên chính máy tham gia lab thì đọc được nội dung và đường dẫn trong luồng HTTP, giống Lab 1 của bài 6. Việc quan sát lưu lượng máy khác còn tùy vị trí bắt và cấu hình mạng.
C. Socket TCP từ đầu: một kết nối, một dòng chữ
15 phútThử ngay: A chờ, B gửi một câu, A trả lời
Bấm 1 ở A trước, rồi bấm 2 ở B. Quan sát câu chữ xuất hiện ở A và phản hồi quay về B.
A chưa chạy
Chạy thật: sửa chuỗi trong file client nếu bạn thay thông điệp ở đây.
Bắt đầu: bấm “1. Chạy tcp_server.py” ở A. Sau đó bấm nút gửi ở B.
Một lần gửi xong thì server TCP mẫu thoát. Muốn gửi lần nữa, chạy lại A.
Muốn chạy thật? Đây là file và lệnh cho hai cửa sổ
Tạo thư mục transfer-lab, tải các file dưới đây vào đó. Mở hai cửa sổ terminal, cả hai đều cd vào thư mục này. A chạy trước; server mẫu thoát sau một kết nối.
Nếu sửa thông điệp trong demo, sửa dòng gửi tương ứng trong file Python trước khi chạy thật:
Demo phía trên chỉ mô phỏng trong trình duyệt, không thực thi Python hay mở kết nối mạng. Các file tải xuống là chương trình Python thật dùng trong bài.
Xem thêm sơ đồ giải thích
TCP: một cuộc hội thoại giữa hai chương trình
Theo dõi Terminal A và B. Mỗi cảnh tương ứng một nhóm lệnh trong hai file Python.
bind() → listen() → accept()bind chọn địa chỉ/port, listen bật chế độ chờ, accept đợi client. Không có dòng mới trên terminal lúc này là bình thường: A chưa có người nói chuyện.
Điều cần hiểu: Chỉ chạy B sau khi A báo đang chờ.
socket.create_connection(("127.0.0.1", 9001))Hệ điều hành thực hiện bắt tay TCP. accept trả về socket conn dùng riêng cho cuộc hội thoại; server socket vẫn là nơi nhận kết nối.
Điều cần hiểu: IP và port phải trùng nơi A đang nghe.
B: sendall() → A: recv() → A: sendall()encode biến chữ thành byte, không phải mã hóa bảo mật. A nhận dữ liệu và thêm tiền tố Server da nhan trước khi trả lời B.
Điều cần hiểu: B in được phản hồi từ A.
Chạy lại tcp_server.py trước mỗi lần thử mớiCode TCP tối giản không có vòng lặp phục vụ nhiều client. B chạy lại khi A đã thoát sẽ thường báo Connection refused.
Điều cần hiểu: Đừng nhầm server đã xong việc với mạng bị lỗi.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Máy chủ web là chương trình có sẵn. Giờ ta tự viết chương trình đơn giản nhất để hai máy nói chuyện, dùng . Cả hai file dưới đây ngắn, bạn đọc hiểu từng dòng được.
Bước 1. Tạo thư mục và hai file
mkdir ~/transfer-lab
cd ~/transfer-lab
pwdmkdir $HOME\transfer-lab
cd $HOME\transfer-lab
pwdmkdir ~/transfer-lab
cd ~/transfer-lab
pwdTạo tcp_server.py (bên nhận) và tcp_client.py (bên gửi):
tcp_server.py · Tải vào transfer-lab. Nếu tự lưu bằng trình soạn thảo, dùng đuôi .py, không thêm .txt.
Xem mã Python · tô màu và số dòng
# tcp_server.py - server TCP nho nhat: nhan mot dong chu, tra loi, roi dong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind(("127.0.0.1", 9001)) # doi 127.0.0.1 thanh 0.0.0.0 de may khac vao duoc
server.listen(1)
print("Dang cho ket noi o 127.0.0.1:9001 ...")
conn, addr = server.accept()
with conn:
print("Co ket noi tu", addr)
data = conn.recv(1024)
print("Nhan duoc:", data.decode("utf-8", errors="replace"))
conn.sendall(b"Server da nhan: " + data)
Cách khác: tạo file bằng lệnh terminal
cat > tcp_server.py <<'EOF'
# tcp_server.py - server TCP nho nhat: nhan mot dong chu, tra loi, roi dong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind(("127.0.0.1", 9001)) # doi 127.0.0.1 thanh 0.0.0.0 de may khac vao duoc
server.listen(1)
print("Dang cho ket noi o 127.0.0.1:9001 ...")
conn, addr = server.accept()
with conn:
print("Co ket noi tu", addr)
data = conn.recv(1024)
print("Nhan duoc:", data.decode("utf-8", errors="replace"))
conn.sendall(b"Server da nhan: " + data)
EOF@'
# tcp_server.py - server TCP nho nhat: nhan mot dong chu, tra loi, roi dong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind(("127.0.0.1", 9001)) # doi 127.0.0.1 thanh 0.0.0.0 de may khac vao duoc
server.listen(1)
print("Dang cho ket noi o 127.0.0.1:9001 ...")
conn, addr = server.accept()
with conn:
print("Co ket noi tu", addr)
data = conn.recv(1024)
print("Nhan duoc:", data.decode("utf-8", errors="replace"))
conn.sendall(b"Server da nhan: " + data)
'@ | Set-Content -Path tcp_server.py -Encoding asciicat > tcp_server.py <<'EOF'
# tcp_server.py - server TCP nho nhat: nhan mot dong chu, tra loi, roi dong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind(("127.0.0.1", 9001)) # doi 127.0.0.1 thanh 0.0.0.0 de may khac vao duoc
server.listen(1)
print("Dang cho ket noi o 127.0.0.1:9001 ...")
conn, addr = server.accept()
with conn:
print("Co ket noi tu", addr)
data = conn.recv(1024)
print("Nhan duoc:", data.decode("utf-8", errors="replace"))
conn.sendall(b"Server da nhan: " + data)
EOFtcp_client.py · Tải vào transfer-lab. Nếu tự lưu bằng trình soạn thảo, dùng đuôi .py, không thêm .txt.
Xem mã Python · tô màu và số dòng
# tcp_client.py - client TCP nho nhat: ket noi, gui mot dong chu, doc tra loi.
import socket
with socket.create_connection(("127.0.0.1", 9001), timeout=5) as conn: # doi IP thanh IP may server
conn.sendall("Xin chao tu client".encode("utf-8"))
print("Tra loi:", conn.recv(1024).decode("utf-8", errors="replace"))
Cách khác: tạo file bằng lệnh terminal
cat > tcp_client.py <<'EOF'
# tcp_client.py - client TCP nho nhat: ket noi, gui mot dong chu, doc tra loi.
import socket
with socket.create_connection(("127.0.0.1", 9001), timeout=5) as conn: # doi IP thanh IP may server
conn.sendall("Xin chao tu client".encode("utf-8"))
print("Tra loi:", conn.recv(1024).decode("utf-8", errors="replace"))
EOF@'
# tcp_client.py - client TCP nho nhat: ket noi, gui mot dong chu, doc tra loi.
import socket
with socket.create_connection(("127.0.0.1", 9001), timeout=5) as conn: # doi IP thanh IP may server
conn.sendall("Xin chao tu client".encode("utf-8"))
print("Tra loi:", conn.recv(1024).decode("utf-8", errors="replace"))
'@ | Set-Content -Path tcp_client.py -Encoding asciicat > tcp_client.py <<'EOF'
# tcp_client.py - client TCP nho nhat: ket noi, gui mot dong chu, doc tra loi.
import socket
with socket.create_connection(("127.0.0.1", 9001), timeout=5) as conn: # doi IP thanh IP may server
conn.sendall("Xin chao tu client".encode("utf-8"))
print("Tra loi:", conn.recv(1024).decode("utf-8", errors="replace"))
EOFTrong transfer-lab có cả tcp_server.py và tcp_client.py. Nếu dùng hai terminal, cả hai đều phải vào thư mục này.
Bước 2 · Terminal A chạy server trước
Mở hai cửa sổ terminal, cả hai đều cd vào transfer-lab. Terminal A (server) chạy trước:
python3 tcp_server.pypy tcp_server.pypython3 tcp_server.pyDang cho ket noi o 127.0.0.1:9001 ...A báo Dang cho ket noi o 127.0.0.1:9001 .... Chưa cần gõ thêm gì ở A.
Bước 3 · Terminal B chạy client sau
python3 tcp_client.pypy tcp_client.pypython3 tcp_client.pyTra loi: Server da nhan: Xin chao tu clientCo ket noi tu ('127.0.0.1', 52454)
Nhan duoc: Xin chao tu clientSố 52454 là port tạm của client (bài 5), mỗi lần chạy khác nhau. Server chỉ xử lý một kết nối rồi thoát, nên muốn thử lại thì chạy lại server.
Bước 4 · Đối chiếu code với việc vừa quan sát
| Phần | Ý nghĩa |
|---|---|
socket.socket(AF_INET, SOCK_STREAM) | Tạo một socket. AF_INET là địa chỉ IPv4; SOCK_STREAM là TCP (dòng byte tin cậy, đúng thứ tự). UDP sẽ dùng SOCK_DGRAM ở phần D. |
server.bind(("127.0.0.1", 9001)) | : nói với hệ điều hành "tôi nhận kết nối ở IP và port này". Đổi 127.0.0.1 thành 0.0.0.0 hoặc IP mạng để máy khác vào được (phần A). |
server.listen(1) | Chuyển sang trạng thái chờ kết nối. Số 1 là độ dài hàng đợi các kết nối chưa được nhận. |
server.accept() | Đứng chờ cho đến khi có client kết nối (bắt tay ba bước TCP của bài 6 xảy ra ở đây). Trả về một socket mới (conn) dành riêng cho client này và địa chỉ của nó. |
conn.recv(1024) | Đọc tối đa 1024 byte. Không bảo đảm đọc đủ một thông điệp (xem phần dưới). |
conn.sendall(...) | Gửi hết các byte đã cho. Dữ liệu gửi qua socket luôn là byte, nên chữ phải .encode("utf-8") trước khi gửi và .decode sau khi nhận. |
socket.create_connection((IP, port)) | Phía client: kết nối tới IP và port của server. Nếu không có ai nghe sẽ lỗi "Connection refused" (phần A). |
Hai máy thật: ở máy nhận đổi 127.0.0.1 thành 0.0.0.0 trong tcp_server.py; ở máy gửi đổi 127.0.0.1 thành IP máy nhận trong tcp_client.py. Sửa dòng print báo địa chỉ trong server cho khớp với địa chỉ bind mới; dòng thông báo không tự đổi. Nhớ firewall (phần A).
Bước 5 · Hiểu giới hạn của ví dụ nhỏ
Vì sao recv(1024) không có nghĩa “nhận đủ file”?
Dùng ví dụ chỉ 8 byte để nhìn rõ chuyện gộp và tách. Các chữ dưới đây đều là ASCII: mỗi chữ một byte.
sendall(b"AAAA"); sendall(b"BBBB")TCP giữ thứ tự byte, nhưng không gửi kèm nhãn đâu là lần gọi sendall thứ nhất hay thứ hai. Hai lần gửi của ứng dụng không nhất thiết là hai lần recv.
Điều cần hiểu: Hai lần gửi ≠ hai thông điệp ở phía nhận.
Ví dụ: recv → b"AAA", recv sau → b"ABBBB"Các lượt đọc có thể là 3 rồi 5 byte (AAA và ABBBB), hoặc 8 byte một lượt, hoặc nhiều phần nhỏ khác. recv(1024) chỉ đặt giới hạn trên cho lần đọc.
Điều cần hiểu: Muốn đủ 8 byte, phải đếm và lặp đến khi đủ.
read_header(conn) → read_exact(conn, size)Phần E gửi một dòng JSON mô tả tên, số byte, mã băm và token. Ký tự xuống dòng chốt header; trường size cho biết phải đọc bao nhiêu byte nội dung.
Điều cần hiểu: Ứng dụng tự đặt quy tắc chia thông điệp: framing.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Ví dụ C chỉ gọi recv một lần để dễ học các lệnh. Trên mạng thật, ngay cả câu ngắn cũng có thể được đọc thành nhiều phần; đừng dùng mẫu tối giản này để nhận file. Phần E có vòng lặp đọc đủ.
Điều khiến người mới viết socket vấp nhất: nếu bên gửi gọi sendall hai lần ("Xin" rồi "chao"), bên nhận có thể recv được "Xinchao" một lần, hoặc "Xi" rồi "nchao". TCP đảm bảo đủ byte và đúng thứ tự nhưng không giữ lại chỗ ngắt giữa các lần gửi. Ứng dụng phải tự đánh dấu, gọi là . Hai cách phổ biến:
- Gửi độ dài trước: "tiếp theo là 2048 byte", rồi gửi đúng 2048 byte. Bên nhận đọc đủ 2048 byte bằng vòng lặp.
- Dùng dấu kết thúc: mỗi thông điệp kết thúc bằng một ký tự xuống dòng (cách HTTP kết thúc header bằng dòng trống).
Phần E dùng cả hai: một dòng JSON mô tả (kết thúc bằng xuống dòng) và độ dài của file.
BÀI TẬP 3 · 8 PHÚT
Đọc và sửa code socket
- Chạy
tcp_client.pykhi chưa chạy server. Ghi lại lỗi, giải thích bằng bài 6. - Sửa
tcp_client.pyđể gửi tên bạn (Lan) thay vì "Xin chao tu client". Server in ra gì? - Bên gửi gọi
sendall(b"AAAA")rồisendall(b"BBBB"). Bên nhận gọirecv(1024). Chắc chắn nhận được gì?
Xem đáp án
ConnectionRefusedError("Connection refused"): gói tới được máy (chính máy mình) nhưng không có chương trình nào nghe ở port 9001. Đúng với trường hợp "bị từ chối ngay" ở bảng phần A.- Đổi chuỗi trong
.encode(...); server in "Nhan duoc: Lan". Chuỗi tiếng Việt có dấu vẫn ổn vì ta dùng UTF-8 cả hai phía. - Không chắc chắn được. Có thể nhận
b"AAAABBBB"(gộp) hoặcb"AAAA"trước vàb"BBBB"sau. Phải tự .
D. UDP: gửi nhanh, không bảo đảm
8 phútThử ngay: gửi UDP lúc có và không có người nhận
Bật A rồi gửi từ B. Sau đó dừng A và gửi lần nữa: B vẫn báo đã gửi, nhưng A không có dòng nhận mới.
A chưa chạy
Chạy thật: sửa chuỗi trong file client nếu bạn thay thông điệp ở đây.
Bắt đầu: bấm “1. Chạy udp_listen.py” ở A. Sau đó bấm nút gửi ở B.
UDP mẫu không chờ phản hồi. Chỉ dòng ở A mới cho biết chương trình A đã đọc được gói.
Muốn chạy thật? Đây là file và lệnh cho hai cửa sổ
Tạo thư mục transfer-lab, tải các file dưới đây vào đó. Mở hai cửa sổ terminal, cả hai đều cd vào thư mục này. A lắng nghe tới khi bạn nhấn Ctrl+C.
Nếu sửa thông điệp trong demo, sửa dòng gửi tương ứng trong file Python trước khi chạy thật:
Demo phía trên chỉ mô phỏng trong trình duyệt, không thực thi Python hay mở kết nối mạng. Các file tải xuống là chương trình Python thật dùng trong bài.
Xem thêm sơ đồ giải thích
UDP: “đã gửi” khác “bên kia đã nhận”
So sánh hai lần gửi: lúc A đang nghe và sau khi A đã dừng.
A: udp_listen.py → B: udp_send.pyKhông có bắt tay TCP. Nếu gói tới khi A đang nghe và không bị mất, A đọc được thông điệp cùng địa chỉ gửi.
Điều cần hiểu: A in nội dung là bằng chứng chương trình A đã nhận.
Dừng A → chạy lại udp_send.py ở BTrong ví dụ này B không đợi gói phản hồi. Việc sendto không báo lỗi không chứng minh chương trình A đã nhận hay xử lý dữ liệu.
Điều cần hiểu: Đọc chính xác: đã gửi, chưa biết đã tới.
Phản hồi OK là do chương trình nhận gửiTCP có cơ chế truyền lại và giữ thứ tự, nhưng TCP ACK không có nghĩa chương trình nhận đã lưu file. Cả TCP lẫn UDP đều cần quy ước phản hồi nếu muốn biết kết quả xử lý.
Điều cần hiểu: Ở phần E, chỉ báo thành công sau khi receiver trả OK.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Bước 1 · Chuẩn bị hai file trong transfer-lab
(bài 5, 6) gửi từng gói rời, không bắt tay, không biết bên kia có nhận được hay không. Nhẹ và nhanh nên dùng cho DNS, video, thoại và tìm thiết bị trong mạng (đoạn SSDP của bài 8). Thử với hai file nhỏ:
udp_listen.py · Tải vào transfer-lab. Nếu tự lưu bằng trình soạn thảo, dùng đuôi .py, không thêm .txt.
Xem mã Python · tô màu và số dòng
# udp_listen.py - lang nghe goi UDP tren port 9002. Moi goi la mot thong diep rieng, khong co bat tay.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(("127.0.0.1", 9002)) # doi thanh 0.0.0.0 de nhan tu may khac
print("Dang nghe UDP o 127.0.0.1:9002 ...")
while True:
data, addr = s.recvfrom(2048)
print(f"Tu {addr[0]}:{addr[1]} -> {data.decode('utf-8', errors='replace')}")
Cách khác: tạo file bằng lệnh terminal
cat > udp_listen.py <<'EOF'
# udp_listen.py - lang nghe goi UDP tren port 9002. Moi goi la mot thong diep rieng, khong co bat tay.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(("127.0.0.1", 9002)) # doi thanh 0.0.0.0 de nhan tu may khac
print("Dang nghe UDP o 127.0.0.1:9002 ...")
while True:
data, addr = s.recvfrom(2048)
print(f"Tu {addr[0]}:{addr[1]} -> {data.decode('utf-8', errors='replace')}")
EOF@'
# udp_listen.py - lang nghe goi UDP tren port 9002. Moi goi la mot thong diep rieng, khong co bat tay.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(("127.0.0.1", 9002)) # doi thanh 0.0.0.0 de nhan tu may khac
print("Dang nghe UDP o 127.0.0.1:9002 ...")
while True:
data, addr = s.recvfrom(2048)
print(f"Tu {addr[0]}:{addr[1]} -> {data.decode('utf-8', errors='replace')}")
'@ | Set-Content -Path udp_listen.py -Encoding asciicat > udp_listen.py <<'EOF'
# udp_listen.py - lang nghe goi UDP tren port 9002. Moi goi la mot thong diep rieng, khong co bat tay.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(("127.0.0.1", 9002)) # doi thanh 0.0.0.0 de nhan tu may khac
print("Dang nghe UDP o 127.0.0.1:9002 ...")
while True:
data, addr = s.recvfrom(2048)
print(f"Tu {addr[0]}:{addr[1]} -> {data.decode('utf-8', errors='replace')}")
EOFudp_send.py · Tải vào transfer-lab. Nếu tự lưu bằng trình soạn thảo, dùng đuôi .py, không thêm .txt.
Xem mã Python · tô màu và số dòng
# udp_send.py - gui mot goi UDP. Khong biet ben kia co nhan duoc hay khong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto("Toi la thiet bi A".encode("utf-8"), ("127.0.0.1", 9002)) # doi IP thanh IP may nhan
print("Da gui (khong co bao dam la da toi noi)")
Cách khác: tạo file bằng lệnh terminal
cat > udp_send.py <<'EOF'
# udp_send.py - gui mot goi UDP. Khong biet ben kia co nhan duoc hay khong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto("Toi la thiet bi A".encode("utf-8"), ("127.0.0.1", 9002)) # doi IP thanh IP may nhan
print("Da gui (khong co bao dam la da toi noi)")
EOF@'
# udp_send.py - gui mot goi UDP. Khong biet ben kia co nhan duoc hay khong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto("Toi la thiet bi A".encode("utf-8"), ("127.0.0.1", 9002)) # doi IP thanh IP may nhan
print("Da gui (khong co bao dam la da toi noi)")
'@ | Set-Content -Path udp_send.py -Encoding asciicat > udp_send.py <<'EOF'
# udp_send.py - gui mot goi UDP. Khong biet ben kia co nhan duoc hay khong.
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto("Toi la thiet bi A".encode("utf-8"), ("127.0.0.1", 9002)) # doi IP thanh IP may nhan
print("Da gui (khong co bao dam la da toi noi)")
EOFBước 2 · A nghe trước, B gửi sau
Trong cả hai terminal, cd vào transfer-lab. Giữ A chạy trong khi thử B:
python3 udp_listen.pypy udp_listen.pypython3 udp_listen.pypython3 udp_send.pypy udp_send.pypython3 udp_send.pyDang nghe UDP o 127.0.0.1:9002 ...
Tu 127.0.0.1:61947 -> Toi la thiet bi ABước 3 · Dừng A rồi gửi lại từ B
Giờ tắt udp_listen.py (Ctrl+C) rồi chạy lại udp_send.py. Bên gửi vẫn in "Da gui" như bình thường dù không ai nhận: Ví dụ UDP này không chờ phản hồi để xác nhận. Với TCP, kết nối tới cổng không có ai nghe thường bị từ chối; TCP cũng không tự xác nhận việc ứng dụng đã lưu file.
| TCP | UDP | |
|---|---|---|
| Bắt tay trước khi gửi | Có (SYN, SYN/ACK, ACK) | Không |
| Bảo đảm đủ và đúng thứ tự | Có | Không, ứng dụng tự lo |
| Biết chương trình kia đã xử lý xong chưa | Cần phản hồi của ứng dụng, như OK | Cũng cần phản hồi của ứng dụng |
| Ranh giới thông điệp | Không (dòng byte) | Có: mỗi gói là một thông điệp |
| Dùng khi | File, web, hầu hết ứng dụng cần đúng | Tìm thiết bị, thoại và video, truy vấn nhỏ |
| Giả mạo người gửi | Khó hơn (cần bắt tay) | Dễ: địa chỉ gửi ghi trong gói có thể bị giả |
Muốn thông báo tới mọi thiết bị trong mạng, UDP có quảng bá (broadcast) và dùng nhóm multicast. Mạng nhà không hề bảo đảm chỉ thiết bị bạn muốn mới nghe: bất kỳ thiết bị nào lắng nghe đúng port đều nhận được, và bất kỳ thiết bị nào cũng có thể gửi gói giả tới port của bạn. Vì thế đừng tin gói UDP chỉ vì nó "đến từ IP quen".
A in được câu gửi từ B ở lần đầu. Khi dừng A, B vẫn có thể báo đã gửi nhưng A không nhận. Dừng udp_listen.py trước khi dùng Terminal A cho lab E.
BÀI TẬP 4 · 5 PHÚT
Chọn TCP hay UDP
Chọn giao thức hợp lý và giải thích ngắn:
- Chuyển file danh sách thành viên.
- Máy in "thông báo" cho mọi máy trong mạng rằng nó vừa bật.
- Gửi lệnh "mở cửa" tới thiết bị điều khiển cửa.
Xem đáp án
- TCP: cần đủ và đúng thứ tự, mất một byte là file hỏng.
- UDP (broadcast/multicast): thông báo nhỏ, đến mọi nơi, mất một gói cũng không sao vì sẽ báo lại.
- TCP, kèm xác thực và mã hóa (TLS): lệnh quan trọng cần phản hồi xác nhận đã xử lý, và không được để người cùng mạng giả mạo. Gửi UDP trần là quá rủi ro vì ai cũng giả được.
E. Lab: gửi file có kiểm tra toàn vẹn
25 phútDemo 2 terminal: chuyển hẳn một file CSV từ B sang A
B có file gốc; A chưa có bản nhận. Chạy A, gửi ở B rồi nhìn file received/thanh-vien.csv xuất hiện bên A.
A chưa chạy
Chưa nhận được file.
Gửi file dưới tên thanh-vien.csv. Demo tự đếm số byte UTF-8.
Bắt đầu: bấm “1. Chạy receiver.py” ở A. Sau đó bấm nút gửi ở B.
A chỉ trả OK sau khi kiểm tra và lưu file. So sánh hai ô nội dung để biết file đã đến đủ.
Muốn chạy thật? Đây là file và lệnh cho hai cửa sổ
Tạo thư mục transfer-lab, tải các file dưới đây vào đó. Mở hai cửa sổ terminal, cả hai đều cd vào thư mục này. Ở B, dùng nút “Tải CSV này” và đặt file cạnh sender.py.
Demo phía trên chỉ mô phỏng trong trình duyệt, không thực thi Python hay mở kết nối mạng. Các file tải xuống là chương trình Python thật dùng trong bài.
Xem thêm sơ đồ giải thích
Một file qua 5 chặng: gửi, kiểm tra, lưu, xác nhận
Ví dụ dưới dùng 35 byte trên macOS/Linux; trên Windows xuống dòng khác có thể làm số byte lớn hơn.
body = read_bytes(); size = len(body)Sender đọc file thành byte, đếm kích thước và tính SHA-256. Header chứa thông tin để receiver biết cần nhận gì. Token mặc định chỉ phục vụ lab.
Điều cần hiểu: Chưa có TLS: token và nội dung vẫn truyền chữ rõ.
read_header() → compare_digest() → kiểm tra size, nameReceiver xử lý header trước khi đọc nội dung ở cấp ứng dụng. Sender thực tế có thể đã gửi cả nội dung vào kết nối; token sai khiến receiver từ chối xử lý và lưu file.
Điều cần hiểu: Kiểm tra đầu vào trước khi dùng nó.
read_exact(conn, size)Một lần recv có thể trả ít hơn size. read_exact lặp cho tới khi đủ số byte đã khai; nếu kết nối đứt giữa chừng thì báo lỗi.
Điều cần hiểu: Đọc đủ byte trước khi tính băm và lưu.
sha256(body) == header["sha256"] → write_bytes()Băm không khớp thì không lưu file mới. Băm khớp chỉ cho biết khớp với giá trị bên gửi khai; riêng mã băm không chứng minh danh tính người gửi.
Điều cần hiểu: Token, băm và TLS giải quyết các việc khác nhau.
A lưu → A trả OK → B in kết quảKết quả mong đợi là B thấy OK và A có file trong received/. Kiểm tra nội dung bản nhận với bản gốc để hoàn tất bài thực hành.
Điều cần hiểu: Không chỉ nhìn “đã gửi”; hãy kiểm tra cả phía nhận.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
B có file gốc → chạy A để nhận → chạy B để gửi → B thấy OK → A có file trong received/. Hãy làm một máy trước; chưa bật TLS trong lần thử đầu.
Ghép socket TCP thành chương trình gửi file hoàn chỉnh, có thứ tự và kiểm tra theo đúng tinh thần các bài trước: toàn vẹn (bài 1), xác thực (bài 3), kiểm tra dữ liệu đầu vào. Giao thức tự đặt, rất ngắn:
1. Bên gửi: một dòng JSON kết thúc bằng xuống dòng:
{"name": "thanh-vien.csv", "size": 1280, "sha256": "ab12...", "token": "..."}
2. Ngay sau đó: đúng 1280 byte nội dung file.
3. Bên nhận trả lời một dòng: "OK" hoặc "LOI <ly do>".Bước 1 · Vào thư mục lab, chuẩn bị chương trình và file giả
Dùng thư mục transfer-lab đã tạo ở C. Nếu bắt đầu từ đây, tạo ~/transfer-lab (Windows: $HOME\transfer-lab) và cd vào đó. Tải hai file dưới đây vào cùng thư mục.
receiver.py · Tải vào transfer-lab. Nếu tự lưu bằng trình soạn thảo, dùng đuôi .py, không thêm .txt.
Xem mã Python · tô màu và số dòng
# receiver.py - nhan file qua TCP. Dung chung cho bai 9. Chay: python3 receiver.py [--tls] [--host IP]
# Co 5 lop bao ve: token, gioi han kich thuoc, lam sach ten file, kiem tra SHA-256, tuy chon TLS.
import argparse
import hashlib
import hmac
import json
import os
import socket
import ssl
from pathlib import Path
MAX_HEADER = 4096
MAX_SIZE = 50 * 1024 * 1024
SAVE_DIR = Path("received")
def read_header(conn):
data = b""
while b"\n" not in data:
chunk = conn.recv(1)
if not chunk or len(data) > MAX_HEADER:
raise ValueError("header sai hoac qua dai")
data += chunk
return json.loads(data.decode("utf-8"))
def read_exact(conn, size):
parts, left = [], size
while left > 0:
chunk = conn.recv(min(65536, left))
if not chunk:
raise ValueError("ket noi dong giua chung")
parts.append(chunk)
left -= len(chunk)
return b"".join(parts)
def handle(conn, token):
header = read_header(conn)
if not hmac.compare_digest(str(header.get("token", "")), token):
raise PermissionError("sai token")
size = int(header["size"])
if not 0 <= size <= MAX_SIZE:
raise ValueError("kich thuoc khong hop le")
name = os.path.basename(str(header["name"]).replace("\\", "/"))
if not name or name.startswith("."):
raise ValueError("ten file khong hop le")
body = read_exact(conn, size)
if hashlib.sha256(body).hexdigest() != header["sha256"]:
raise ValueError("SHA-256 khong khop: du lieu bi loi hoac bi sua")
SAVE_DIR.mkdir(exist_ok=True)
(SAVE_DIR / name).write_bytes(body)
return name, size
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1", help="IP de nghe; 0.0.0.0 la moi card mang")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay", help="chuoi bi mat chung giua hai ben")
ap.add_argument("--tls", action="store_true", help="bat TLS, can cert.pem va key.pem")
args = ap.parse_args()
context = None
if args.tls:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("cert.pem", "key.pem")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((args.host, args.port))
server.listen(5)
print(f"Dang nghe {args.host}:{args.port} ({'TLS' if args.tls else 'KHONG ma hoa'}). Ctrl+C de dung.")
while True:
conn, addr = server.accept()
conn.settimeout(10)
try:
with (context.wrap_socket(conn, server_side=True) if context else conn) as c:
try:
name, size = handle(c, args.token)
c.sendall(b"OK\n")
print(f"Nhan {name} ({size} byte) tu {addr[0]}")
except (ValueError, PermissionError, KeyError, json.JSONDecodeError) as err:
c.sendall(f"LOI {err}\n".encode())
print(f"Tu choi {addr[0]}: {err}")
except (OSError, ssl.SSLError) as err:
print(f"Loi ket noi tu {addr[0]}: {err}")
if __name__ == "__main__":
main()
Cách khác: tạo file bằng lệnh terminal
cat > receiver.py <<'EOF'
# receiver.py - nhan file qua TCP. Dung chung cho bai 9. Chay: python3 receiver.py [--tls] [--host IP]
# Co 5 lop bao ve: token, gioi han kich thuoc, lam sach ten file, kiem tra SHA-256, tuy chon TLS.
import argparse
import hashlib
import hmac
import json
import os
import socket
import ssl
from pathlib import Path
MAX_HEADER = 4096
MAX_SIZE = 50 * 1024 * 1024
SAVE_DIR = Path("received")
def read_header(conn):
data = b""
while b"\n" not in data:
chunk = conn.recv(1)
if not chunk or len(data) > MAX_HEADER:
raise ValueError("header sai hoac qua dai")
data += chunk
return json.loads(data.decode("utf-8"))
def read_exact(conn, size):
parts, left = [], size
while left > 0:
chunk = conn.recv(min(65536, left))
if not chunk:
raise ValueError("ket noi dong giua chung")
parts.append(chunk)
left -= len(chunk)
return b"".join(parts)
def handle(conn, token):
header = read_header(conn)
if not hmac.compare_digest(str(header.get("token", "")), token):
raise PermissionError("sai token")
size = int(header["size"])
if not 0 <= size <= MAX_SIZE:
raise ValueError("kich thuoc khong hop le")
name = os.path.basename(str(header["name"]).replace("\\", "/"))
if not name or name.startswith("."):
raise ValueError("ten file khong hop le")
body = read_exact(conn, size)
if hashlib.sha256(body).hexdigest() != header["sha256"]:
raise ValueError("SHA-256 khong khop: du lieu bi loi hoac bi sua")
SAVE_DIR.mkdir(exist_ok=True)
(SAVE_DIR / name).write_bytes(body)
return name, size
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1", help="IP de nghe; 0.0.0.0 la moi card mang")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay", help="chuoi bi mat chung giua hai ben")
ap.add_argument("--tls", action="store_true", help="bat TLS, can cert.pem va key.pem")
args = ap.parse_args()
context = None
if args.tls:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("cert.pem", "key.pem")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((args.host, args.port))
server.listen(5)
print(f"Dang nghe {args.host}:{args.port} ({'TLS' if args.tls else 'KHONG ma hoa'}). Ctrl+C de dung.")
while True:
conn, addr = server.accept()
conn.settimeout(10)
try:
with (context.wrap_socket(conn, server_side=True) if context else conn) as c:
try:
name, size = handle(c, args.token)
c.sendall(b"OK\n")
print(f"Nhan {name} ({size} byte) tu {addr[0]}")
except (ValueError, PermissionError, KeyError, json.JSONDecodeError) as err:
c.sendall(f"LOI {err}\n".encode())
print(f"Tu choi {addr[0]}: {err}")
except (OSError, ssl.SSLError) as err:
print(f"Loi ket noi tu {addr[0]}: {err}")
if __name__ == "__main__":
main()
EOF@'
# receiver.py - nhan file qua TCP. Dung chung cho bai 9. Chay: python3 receiver.py [--tls] [--host IP]
# Co 5 lop bao ve: token, gioi han kich thuoc, lam sach ten file, kiem tra SHA-256, tuy chon TLS.
import argparse
import hashlib
import hmac
import json
import os
import socket
import ssl
from pathlib import Path
MAX_HEADER = 4096
MAX_SIZE = 50 * 1024 * 1024
SAVE_DIR = Path("received")
def read_header(conn):
data = b""
while b"\n" not in data:
chunk = conn.recv(1)
if not chunk or len(data) > MAX_HEADER:
raise ValueError("header sai hoac qua dai")
data += chunk
return json.loads(data.decode("utf-8"))
def read_exact(conn, size):
parts, left = [], size
while left > 0:
chunk = conn.recv(min(65536, left))
if not chunk:
raise ValueError("ket noi dong giua chung")
parts.append(chunk)
left -= len(chunk)
return b"".join(parts)
def handle(conn, token):
header = read_header(conn)
if not hmac.compare_digest(str(header.get("token", "")), token):
raise PermissionError("sai token")
size = int(header["size"])
if not 0 <= size <= MAX_SIZE:
raise ValueError("kich thuoc khong hop le")
name = os.path.basename(str(header["name"]).replace("\\", "/"))
if not name or name.startswith("."):
raise ValueError("ten file khong hop le")
body = read_exact(conn, size)
if hashlib.sha256(body).hexdigest() != header["sha256"]:
raise ValueError("SHA-256 khong khop: du lieu bi loi hoac bi sua")
SAVE_DIR.mkdir(exist_ok=True)
(SAVE_DIR / name).write_bytes(body)
return name, size
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1", help="IP de nghe; 0.0.0.0 la moi card mang")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay", help="chuoi bi mat chung giua hai ben")
ap.add_argument("--tls", action="store_true", help="bat TLS, can cert.pem va key.pem")
args = ap.parse_args()
context = None
if args.tls:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("cert.pem", "key.pem")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((args.host, args.port))
server.listen(5)
print(f"Dang nghe {args.host}:{args.port} ({'TLS' if args.tls else 'KHONG ma hoa'}). Ctrl+C de dung.")
while True:
conn, addr = server.accept()
conn.settimeout(10)
try:
with (context.wrap_socket(conn, server_side=True) if context else conn) as c:
try:
name, size = handle(c, args.token)
c.sendall(b"OK\n")
print(f"Nhan {name} ({size} byte) tu {addr[0]}")
except (ValueError, PermissionError, KeyError, json.JSONDecodeError) as err:
c.sendall(f"LOI {err}\n".encode())
print(f"Tu choi {addr[0]}: {err}")
except (OSError, ssl.SSLError) as err:
print(f"Loi ket noi tu {addr[0]}: {err}")
if __name__ == "__main__":
main()
'@ | Set-Content -Path receiver.py -Encoding asciicat > receiver.py <<'EOF'
# receiver.py - nhan file qua TCP. Dung chung cho bai 9. Chay: python3 receiver.py [--tls] [--host IP]
# Co 5 lop bao ve: token, gioi han kich thuoc, lam sach ten file, kiem tra SHA-256, tuy chon TLS.
import argparse
import hashlib
import hmac
import json
import os
import socket
import ssl
from pathlib import Path
MAX_HEADER = 4096
MAX_SIZE = 50 * 1024 * 1024
SAVE_DIR = Path("received")
def read_header(conn):
data = b""
while b"\n" not in data:
chunk = conn.recv(1)
if not chunk or len(data) > MAX_HEADER:
raise ValueError("header sai hoac qua dai")
data += chunk
return json.loads(data.decode("utf-8"))
def read_exact(conn, size):
parts, left = [], size
while left > 0:
chunk = conn.recv(min(65536, left))
if not chunk:
raise ValueError("ket noi dong giua chung")
parts.append(chunk)
left -= len(chunk)
return b"".join(parts)
def handle(conn, token):
header = read_header(conn)
if not hmac.compare_digest(str(header.get("token", "")), token):
raise PermissionError("sai token")
size = int(header["size"])
if not 0 <= size <= MAX_SIZE:
raise ValueError("kich thuoc khong hop le")
name = os.path.basename(str(header["name"]).replace("\\", "/"))
if not name or name.startswith("."):
raise ValueError("ten file khong hop le")
body = read_exact(conn, size)
if hashlib.sha256(body).hexdigest() != header["sha256"]:
raise ValueError("SHA-256 khong khop: du lieu bi loi hoac bi sua")
SAVE_DIR.mkdir(exist_ok=True)
(SAVE_DIR / name).write_bytes(body)
return name, size
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1", help="IP de nghe; 0.0.0.0 la moi card mang")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay", help="chuoi bi mat chung giua hai ben")
ap.add_argument("--tls", action="store_true", help="bat TLS, can cert.pem va key.pem")
args = ap.parse_args()
context = None
if args.tls:
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain("cert.pem", "key.pem")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((args.host, args.port))
server.listen(5)
print(f"Dang nghe {args.host}:{args.port} ({'TLS' if args.tls else 'KHONG ma hoa'}). Ctrl+C de dung.")
while True:
conn, addr = server.accept()
conn.settimeout(10)
try:
with (context.wrap_socket(conn, server_side=True) if context else conn) as c:
try:
name, size = handle(c, args.token)
c.sendall(b"OK\n")
print(f"Nhan {name} ({size} byte) tu {addr[0]}")
except (ValueError, PermissionError, KeyError, json.JSONDecodeError) as err:
c.sendall(f"LOI {err}\n".encode())
print(f"Tu choi {addr[0]}: {err}")
except (OSError, ssl.SSLError) as err:
print(f"Loi ket noi tu {addr[0]}: {err}")
if __name__ == "__main__":
main()
EOFsender.py · Tải vào transfer-lab. Nếu tự lưu bằng trình soạn thảo, dùng đuôi .py, không thêm .txt.
Xem mã Python · tô màu và số dòng
# sender.py - gui file qua TCP toi receiver.py. Chay: python3 sender.py FILE [--host IP] [--tls]
import argparse
import hashlib
import json
import socket
import ssl
from pathlib import Path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("file")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay")
ap.add_argument("--tls", action="store_true", help="dung TLS va tin cert.pem")
ap.add_argument("--name", help="ghi de ten file gui di (dung de thu nghiem)")
args = ap.parse_args()
body = Path(args.file).read_bytes()
header = {"name": args.name or Path(args.file).name, "size": len(body),
"sha256": hashlib.sha256(body).hexdigest(), "token": args.token}
try:
raw = socket.create_connection((args.host, args.port), timeout=10)
if args.tls:
context = ssl.create_default_context(cafile="cert.pem")
raw = context.wrap_socket(raw, server_hostname="localhost")
with raw as conn:
conn.sendall(json.dumps(header).encode("utf-8") + b"\n" + body)
print("May nhan tra loi:", conn.recv(200).decode("utf-8", errors="replace").strip())
except (OSError, ssl.SSLError) as err:
raise SystemExit(f"Khong gui duoc: {err}")
if __name__ == "__main__":
main()
Cách khác: tạo file bằng lệnh terminal
cat > sender.py <<'EOF'
# sender.py - gui file qua TCP toi receiver.py. Chay: python3 sender.py FILE [--host IP] [--tls]
import argparse
import hashlib
import json
import socket
import ssl
from pathlib import Path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("file")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay")
ap.add_argument("--tls", action="store_true", help="dung TLS va tin cert.pem")
ap.add_argument("--name", help="ghi de ten file gui di (dung de thu nghiem)")
args = ap.parse_args()
body = Path(args.file).read_bytes()
header = {"name": args.name or Path(args.file).name, "size": len(body),
"sha256": hashlib.sha256(body).hexdigest(), "token": args.token}
try:
raw = socket.create_connection((args.host, args.port), timeout=10)
if args.tls:
context = ssl.create_default_context(cafile="cert.pem")
raw = context.wrap_socket(raw, server_hostname="localhost")
with raw as conn:
conn.sendall(json.dumps(header).encode("utf-8") + b"\n" + body)
print("May nhan tra loi:", conn.recv(200).decode("utf-8", errors="replace").strip())
except (OSError, ssl.SSLError) as err:
raise SystemExit(f"Khong gui duoc: {err}")
if __name__ == "__main__":
main()
EOF@'
# sender.py - gui file qua TCP toi receiver.py. Chay: python3 sender.py FILE [--host IP] [--tls]
import argparse
import hashlib
import json
import socket
import ssl
from pathlib import Path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("file")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay")
ap.add_argument("--tls", action="store_true", help="dung TLS va tin cert.pem")
ap.add_argument("--name", help="ghi de ten file gui di (dung de thu nghiem)")
args = ap.parse_args()
body = Path(args.file).read_bytes()
header = {"name": args.name or Path(args.file).name, "size": len(body),
"sha256": hashlib.sha256(body).hexdigest(), "token": args.token}
try:
raw = socket.create_connection((args.host, args.port), timeout=10)
if args.tls:
context = ssl.create_default_context(cafile="cert.pem")
raw = context.wrap_socket(raw, server_hostname="localhost")
with raw as conn:
conn.sendall(json.dumps(header).encode("utf-8") + b"\n" + body)
print("May nhan tra loi:", conn.recv(200).decode("utf-8", errors="replace").strip())
except (OSError, ssl.SSLError) as err:
raise SystemExit(f"Khong gui duoc: {err}")
if __name__ == "__main__":
main()
'@ | Set-Content -Path sender.py -Encoding asciicat > sender.py <<'EOF'
# sender.py - gui file qua TCP toi receiver.py. Chay: python3 sender.py FILE [--host IP] [--tls]
import argparse
import hashlib
import json
import socket
import ssl
from pathlib import Path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("file")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=9000)
ap.add_argument("--token", default="doi-token-nay")
ap.add_argument("--tls", action="store_true", help="dung TLS va tin cert.pem")
ap.add_argument("--name", help="ghi de ten file gui di (dung de thu nghiem)")
args = ap.parse_args()
body = Path(args.file).read_bytes()
header = {"name": args.name or Path(args.file).name, "size": len(body),
"sha256": hashlib.sha256(body).hexdigest(), "token": args.token}
try:
raw = socket.create_connection((args.host, args.port), timeout=10)
if args.tls:
context = ssl.create_default_context(cafile="cert.pem")
raw = context.wrap_socket(raw, server_hostname="localhost")
with raw as conn:
conn.sendall(json.dumps(header).encode("utf-8") + b"\n" + body)
print("May nhan tra loi:", conn.recv(200).decode("utf-8", errors="replace").strip())
except (OSError, ssl.SSLError) as err:
raise SystemExit(f"Khong gui duoc: {err}")
if __name__ == "__main__":
main()
EOFVà một file thử, chỉ có dữ liệu giả:
printf 'ten,so_tien\nLan,200000\nMinh,150000\n' > thanh-vien.csvSet-Content -Path thanh-vien.csv -Value "ten,so_tien","Lan,200000","Minh,150000" -Encoding asciiprintf 'ten,so_tien\nLan,200000\nMinh,150000\n' > thanh-vien.csvCó đủ ba file cạnh nhau: receiver.py, sender.py, thanh-vien.csv. Cả hai terminal đều ở transfer-lab.
Đọc thêm: các hàm receiver.py làm gì?
| Phần | Ý nghĩa |
|---|---|
read_header(conn) | Đọc từng byte cho tới khi gặp xuống dòng, giới hạn 4096 byte. Header quá dài hoặc đứt giữa chừng thì từ chối. Phân định thông điệp bằng ký tự xuống dòng (phần C). |
read_exact(conn, size) | Đọc đúng size byte bằng vòng lặp, vì một lần recv có thể chưa đủ. Kết nối đóng giữa chừng thì báo lỗi. |
hmac.compare_digest(...token...) | Kiểm tra token (chuỗi bí mật chung hai bên biết). Dùng compare_digest thay vì == để so sánh không để lộ thông tin qua thời gian xử lý. Sai token thì từ chối, không xử lý và lưu nội dung file. |
MAX_SIZE = 50 MB | Giới hạn kích thước. Không giới hạn thì một người gửi "size" cực lớn làm đầy bộ nhớ máy nhận. |
os.path.basename(...) | Làm sạch tên file: chỉ giữ phần tên cuối cùng, bỏ mọi đường dẫn. Chống : tên như ../../.ssh/authorized_keys bị cắt còn authorized_keys và vẫn chỉ ghi vào thư mục received/. |
hashlib.sha256(body) | Tính của dữ liệu nhận được và so với giá trị header khai. Khác nhau (hỏng hoặc bị sửa giữa đường) thì không lưu. Chỉ cho biết dữ liệu đúng như bên gửi khai; không chứng minh ai là bên gửi. |
conn.settimeout(10) | Mỗi kết nối chỉ được 10 giây im lặng. Không có thì một máy kết nối rồi treo sẽ giữ chương trình mãi. |
--host, --token, --tls | Tham số: IP nghe, chuỗi bí mật, có dùng TLS hay không (phần F). |
Bước 2 · Terminal A: chạy receiver và giữ cửa sổ mở
Terminal A (nhận):
python3 receiver.pypy receiver.pypython3 receiver.pyDang nghe 127.0.0.1:9000 (KHONG ma hoa). Ctrl+C de dung.A hiện Dang nghe 127.0.0.1:9000 (KHONG ma hoa). A đang chạy nên B mới kết nối được.
Bước 3 · Terminal B: gửi file
python3 sender.py thanh-vien.csvpy sender.py thanh-vien.csvpython3 sender.py thanh-vien.csvMay nhan tra loi: OKNhan thanh-vien.csv (35 byte) tu 127.0.0.1Bước 4 · Kiểm tra file thực sự đã đến
Số 35 byte là ví dụ với file dùng xuống dòng LF; PowerShell có thể tạo file lớn hơn do xuống dòng CRLF. Chương trình tự đếm byte, không cần sửa size bằng tay.
Thư mục transfer-lab/received/ có file thanh-vien.csv mới. So sánh nội dung với file gốc: giống hệt.
cat received/thanh-vien.csv
cmp thanh-vien.csv received/thanh-vien.csvGet-Content received\thanh-vien.csv
Get-FileHash thanh-vien.csv
Get-FileHash received\thanh-vien.csvcat received/thanh-vien.csv
cmp thanh-vien.csv received/thanh-vien.csvcmp không in gì và thoát thành công khi hai file giống nhau. Trên Windows, hai giá trị Hash phải trùng.
B thấy May nhan tra loi: OK, A báo đã nhận file và nội dung bản nhận khớp bản gốc. Receiver A còn chạy để bạn thử tiếp.
Bước 5 · Thử các trường hợp receiver cần chặn
- Sai token:
python3 sender.py thanh-vien.csv --token saipy sender.py thanh-vien.csv --token saipython3 sender.py thanh-vien.csv --token saiMay nhan tra loi: LOI sai token- Tên file có đường dẫn đi ngược lên thư mục cha (thử tấn công , chỉ trên máy bạn):
python3 sender.py thanh-vien.csv --name "../../thoat.txt"py sender.py thanh-vien.csv --name "../../thoat.txt"python3 sender.py thanh-vien.csv --name "../../thoat.txt"Receiver trả lời OK nhưng file được lưu là received/thoat.txt, không phải hai cấp thư mục bên trên. Giữ nguyên cách làm sạch tên. Chỉ cần kiểm tra received/thoat.txt đã đủ thấy đường dẫn gửi tới không quyết định thư mục ghi file. Nếu đã có file cùng tên, mẫu hiện tại sẽ ghi đè nó. Đây là lỗi kinh điển của chức năng tải file lên.
- Dữ liệu hỏng giữa đường: sửa tạm
sender.py, thay"sha256": hashlib.sha256(body).hexdigest()bằng một chuỗi bất kỳ, hoặc đổi một byte củabodysau khi tính băm. Receiver inLOI SHA-256 khong khopvà không lưu file.
Trước khi tiếp tục: nếu vừa sửa SHA-256 trong sender.py, khôi phục công thức ban đầu hoặc tải lại file mẫu. Gửi lại file hợp lệ, phải nhận OK.
Bước 6 · Chuyển sang hai máy thật sau khi một máy đã chạy được
Chuyển từ một máy sang hai máy: đổi đúng chỗ
Ví dụ máy A nhận có IP 192.168.1.23. Thay địa chỉ này bằng IP thực tế của máy nhận.
A: receiver.py --host 192.168.1.23127.0.0.1 chỉ nhận trên chính A. Muốn B kết nối tới IP mạng của A, receiver phải nghe ở IP đó (hoặc 0.0.0.0 nếu cần). Chỉ cho phép kết nối cần thiết trong firewall.
Điều cần hiểu: A vẫn chạy trước; ghi lại IP và port của A.
B: sender.py thanh-vien.csv --host 192.168.1.23B dùng IP thật của A. 127.0.0.1 trên B lại chỉ về B; 0.0.0.0 là địa chỉ nghe của server, không phải IP đích để đưa vào bài thực hành.
Điều cần hiểu: Đổi IP ở cả bên nghe lẫn bên kết nối.
Kiểm tra file ở A → Ctrl+C receiverNếu thất bại, kiểm tra lần lượt: IP đích, địa chỉ bind, receiver còn chạy, firewall và cách ly mạng. Dùng file giả cho lần thử TCP chưa TLS; áp dụng phần F để mã hóa.
Điều cần hiểu: Đừng vừa đổi IP, port, token và TLS trong cùng lần thử.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Dừng receiver thử một máy bằng Ctrl+C trước. Trên máy A nhận, thư mục lab cần receiver.py; trên B gửi, cần sender.py và file CSV. Khi đổi máy, cd tới đúng thư mục trước khi chạy.
Ở máy nhận cho nghe ở IP mạng (hoặc 0.0.0.0), nhớ firewall:
python3 receiver.py --host 192.168.1.23py receiver.py --host 192.168.1.23python3 receiver.py --host 192.168.1.23Ở máy gửi (cùng Wi-Fi), copy sender.py và file cần gửi sang, rồi:
python3 sender.py thanh-vien.csv --host 192.168.1.23py sender.py thanh-vien.csv --host 192.168.1.23python3 sender.py thanh-vien.csv --host 192.168.1.23Kiểm tra từng bước bằng bảng phần A nếu không kết nối được.
Quan sát bằng Wireshark: token và nội dung nằm chữ rõ
Với lab một máy, bắt trên giao diện loopback (macOS: lo0, Linux: lo, Windows: adapter loopback của Npcap). Với hai máy, chọn card Wi-Fi/Ethernet đang dùng. theo hướng dẫn bài 6, dùng bộ lọc tcp.port == 9000, rồi gửi lại file. Chuột phải một gói → Follow → TCP Stream. Bạn đọc được nguyên văn dòng JSON kèm token và cả nội dung file. Thử bộ lọc frame contains "token" hoặc frame contains "Lan".
Lỗi thường gặp
| Dấu hiệu | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Khong gui duoc: Connection refused | Chưa chạy receiver, sai port, hoặc receiver bind ở 127.0.0.1 | Chạy receiver trước; kiểm tra --host và --port |
Khong gui duoc: timed out | Firewall máy nhận, sai IP, hoặc khác mạng | Bảng ở phần A |
LOI sai token | Hai bên đặt --token khác nhau | Dùng cùng chuỗi ở cả hai phía |
LOI SHA-256 khong khop | Dữ liệu hỏng hoặc bị sửa; hoặc sửa sender.py làm lệch | Gửi lại; kiểm tra mạng; kiểm tra lại code |
Address already in use | Port 9000 đang bị chương trình khác (hoặc receiver cũ) giữ | Ctrl+C receiver cũ hoặc đổi --port cả hai phía |
BÀI TẬP 5 · 12 PHÚT
Thử phá và vá chương trình nhận file
- Gửi file có
--name ".hidden"và--name "../". Chuyện gì xảy ra, vì sao đoạn code đó có tronghandle()? - Hạ
MAX_SIZExuống 10 byte rồi gửithanh-vien.csv. Kết quả? - Receiver này chưa có giới hạn số lần thử sai token. Kẻ trong mạng có thể làm gì, và cách giảm?
Xem đáp án
- Cả hai bị từ chối ("ten file khong hop le"). Tên bắt đầu bằng dấu chấm tạo file ẩn (ví dụ
.bashrc), còn../sau khi làm sạch thành tên rỗng, không thành tên file; chặn trước để không ghi file nhạy cảm kiểu cấu hình ẩn. - Receiver trả "LOI kich thuoc khong hop le" và đóng ngay, không đọc nội dung. Giới hạn kích thước chặn việc gửi dữ liệu khổng lồ để làm cạn bộ nhớ hoặc ổ đĩa.
- Kẻ trong mạng thử hàng nghìn token (đoán mật khẩu). Giảm bằng: token dài ngẫu nhiên (không phải chữ dễ đoán), giới hạn số lần sai từ một IP rồi chặn tạm, ghi log mỗi lần từ chối, và chỉ cho phép IP của máy gửi biết trước (firewall).
F. Bảo vệ đường truyền: TLS cho socket và công cụ có sẵn
15 phútDemo 2 terminal: vẫn gửi file đó, lần này qua TLS
Demo giả định đã chuẩn bị chứng chỉ như bước 1 bên dưới. A giữ cert.pem + key.pem; B tin cert.pem của A.
A chưa chạy
Chưa nhận được file.
Gửi file dưới tên thanh-vien.csv. Demo tự đếm số byte UTF-8.
Bắt đầu: bấm “1. Chạy receiver.py” ở A. Sau đó bấm nút gửi ở B.
TLS che token và nội dung trên đường truyền. Sau giải mã, A vẫn cần kiểm tra và lưu file rồi mới trả OK.
Muốn chạy thật? Đây là file và lệnh cho hai cửa sổ
Tạo thư mục transfer-lab, tải các file dưới đây vào đó. Mở hai cửa sổ terminal, cả hai đều cd vào thư mục này. Ở B, dùng nút “Tải CSV này” và đặt file cạnh sender.py.
Trước khi chạy TLS: tạo cert.pem và key.pem bằng lệnh ở bước 1 bên dưới. Với lab cùng một máy, hai terminal dùng chung các file này trong transfer-lab.
Demo phía trên chỉ mô phỏng trong trình duyệt, không thực thi Python hay mở kết nối mạng. Các file tải xuống là chương trình Python thật dùng trong bài.
Xem thêm sơ đồ giải thích
TLS như chiếc hộp khóa cho cuộc hội thoại
File vẫn đi qua TCP. TLS thêm mã hóa và kiểm tra máy nhận trước khi trao đổi token, header và nội dung.
receiver.py / sender.py không thêm --tlsTrên chính máy làm lab, Wireshark Follow TCP Stream có thể đọc nội dung. Việc người khác quan sát được còn phụ thuộc vị trí bắt gói và cấu hình mạng.
Điều cần hiểu: Mã UTF-8 và mã băm không che nội dung.
A: tạo cert.pem + key.pem → B: chép cert.pemcert.pem là chứng chỉ công khai; key.pem là khóa riêng của máy nhận. B cần có đúng chứng chỉ tin cậy để kiểm tra máy nhận trong lab tự ký.
Điều cần hiểu: Không chép key.pem sang máy gửi.
A: receiver.py --tls → B: sender.py … --tlsDừng receiver cũ trước khi bật bản TLS. Với code mẫu, B kiểm tra tên localhost trong chứng chỉ. Trên hai máy, tên TLS đó vẫn là localhost do sender đặt cố định; --host chỉ đổi địa chỉ kết nối.
Điều cần hiểu: Cả hai phải bật --tls; không bỏ kiểm tra chứng chỉ.
May nhan tra loi: OKTLS không thay thế kiểm tra token, kích thước, tên file và băm trong receiver. Nó bảo vệ cuộc trao đổi khi chứng chỉ và khóa được dùng đúng.
Điều cần hiểu: Đủ dữ liệu, đúng bên và được che là ba câu hỏi riêng.
Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.
Bước 0 · Dừng receiver cũ và kiểm tra thư mục
Nhấn Ctrl+C ở A để giải phóng port 9000. Cả A và B vẫn ở transfer-lab; giữ nguyên receiver.py, sender.py và CSV.
Chương trình trên kiểm tra được dữ liệu theo thông tin người gửi khai, nhưng chưa che nội dung. Thêm (bài 6): bọc socket TCP trong một lớp mã hóa, dùng chính cách bạn đã làm ở Lab 2 của bài 6.
Bước 1. Tạo chứng chỉ tự ký cho localhost
Để tránh dùng chứng chỉ cũ thiếu tên localhost hoặc đã hết hạn, hãy tạo cặp mới sau đây trong transfer-lab. Lệnh ghi đè cert.pem/key.pem nếu đã có; chỉ dùng thư mục lab:
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 7 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"& "C:\Program Files\Git\usr\bin\openssl.exe" req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 7 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 7 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"Ý nghĩa các tham số đã giải thích ở bài 6 (Lab 2, bước 1). key.pem là khóa riêng: giữ bí mật. cert.pem là chứng chỉ công khai.
Có thêm cert.pem và key.pem trong transfer-lab. Không gửi khóa riêng cho bên kia.
Bước 2 · Chạy A trước với --tls
python3 receiver.py --tlspy receiver.py --tlspython3 receiver.py --tlsChờ A báo (TLS), rồi qua B:
python3 sender.py thanh-vien.csv --tls --name an-toan.csvpy sender.py thanh-vien.csv --tls --name an-toan.csvpython3 sender.py thanh-vien.csv --tls --name an-toan.csvTerminal A: Dang nghe 127.0.0.1:9000 (TLS). Ctrl+C de dung.
Terminal B: May nhan tra loi: OK
Terminal A: Nhan an-toan.csv (35 byte) tu 127.0.0.1| Phần | Ý nghĩa |
|---|---|
ssl.SSLContext(PROTOCOL_TLS_SERVER) + load_cert_chain | Phía nhận tạo "ngữ cảnh TLS" và nạp chứng chỉ cùng khóa riêng. wrap_socket(..., server_side=True) biến mỗi kết nối thành kết nối TLS: bắt tay TLS diễn ra rồi mới đến dữ liệu. |
ssl.create_default_context(cafile="cert.pem") | Phía gửi tạo ngữ cảnh chỉ tin đúng chứng chỉ này. Như vậy phía gửi kiểm tra "đang nói đúng máy nhận mà mình tin", không phải một máy bất kỳ có chứng chỉ tự ký khác. Trong lab này, chứng chỉ tự ký được nạp làm nguồn tin cậy để kiểm tra máy nhận. |
server_hostname="localhost" | Tên cần khớp với tên trong chứng chỉ (localhost có trong SAN). Code mẫu luôn kiểm tra tên localhost, kể cả khi --host là IP của máy khác. Địa chỉ TCP và tên kiểm tra TLS là hai giá trị riêng trong ví dụ này. |
Thử gửi không có --tls tới receiver đang chạy --tls (hoặc ngược lại): kết nối thất bại với lỗi giao thức. Hai bên phải thống nhất mã hóa hay không.
Hai máy thật: thêm TLS sau khi phần E đã chạy được
- A: tạo chứng chỉ bằng lệnh trên, giữ
key.pemtại A. Chứng chỉ phải cóDNS:localhostvì sender mẫu kiểm tra tên đó. - B: chép đúng
cert.pemtừ A bằng kênh tin cậy, đặt cạnhsender.py; không cần khóa riêng. - A: chạy
receiver.py --host 192.168.1.23 --tls; thay bằng IP thật của A. - B: chạy
sender.py thanh-vien.csv --host 192.168.1.23 --tls --name an-toan.csv. Trên Windows dùngpy, macOS/Linux dùngpython3trước tên file.
Không tắt kiểm tra chứng chỉ để chữa lỗi. --host chọn IP kết nối; code hiện đặt server_hostname="localhost" cố định. Nếu đổi tên này trong code, phải tạo chứng chỉ có SAN tương ứng.
Bước 3. Bắt gói và so sánh
Chọn giao diện loopback cho một máy hoặc Wi-Fi/Ethernet cho hai máy, lọc tcp.port == 9000, gửi một lần không TLS và một lần có TLS. Dùng bộ lọc:
frame contains "token"Với dữ liệu mẫu, luồng không mã hóa thường hiện gói chứa chữ token. Luồng TLS chưa được giải mã không cho đọc header đó. Bộ lọc từng gói có thể bỏ sót chữ nằm qua ranh giới gói: hãy dùng Follow TCP Stream để đối chiếu cả luồng. Follow TCP Stream của luồng TLS chỉ cho ký tự lộn xộn. Nội dung file, tên file và token đều được che. Chỉ còn lộ metadata: IP, port, kích thước và thời điểm (bài 6).
B nhận OK; A có received/an-toan.csv. Nội dung khớp file gốc, dù quan sát đường truyền TLS chưa giải mã không đọc được chữ rõ. Khi xong, Ctrl+C ở A.
Khi nào dùng công cụ có sẵn thay vì tự viết
Tự viết để hiểu. Với việc thật, dùng công cụ đã được kiểm chứng: chúng có quy trình bảo trì, cập nhật và xử lý các trường hợp mà ví dụ lab chưa bao quát.
| Công cụ | Làm gì | Ghi chú |
|---|---|---|
scp, sftp, rsync -e ssh | Chép file giữa máy qua SSH (mã hóa và xác thực bằng mật khẩu hoặc khóa) | Có sẵn trên macOS, Linux và Windows 10 trở lên. Cần dịch vụ SSH ở máy nhận. Ví dụ: scp thanh-vien.csv [email protected]:~/ |
| AirDrop, Nearby Share, Quick Share | Gửi giữa điện thoại và máy tính cùng hệ sinh thái | Có mã hóa. Dùng ở chế độ "chỉ danh bạ" thay vì "mọi người" |
| Syncthing | Đồng bộ thư mục giữa các máy, mã hóa, xác thực thiết bị | Dành cho đồng bộ thường xuyên, không cần máy chủ trung gian |
| Máy chủ HTTPS có đăng nhập | Chia sẻ qua web có người dùng và quyền | Thường là cách của website thật |
scp thanh-vien.csv [email protected]:~/. Lần đầu SSH hỏi có tin khóa của máy kia không (nó hiện dấu vân tay); nhập yes chỉ khi bạn chắc đúng máy mình, đây là bước xác thực máy nhận giống như kiểm tra chứng chỉ.G. Danh sách kiểm tra khi mở một cổng nhận dữ liệu
10 phútChương trình nhận dữ liệu từ mạng là chương trình nhận đầu vào từ người lạ. Mọi thứ bài 1 đến bài 6 về kiểm tra đầu vào, xác thực và mã hóa đều áp dụng. Tám điều nên đối chiếu trước khi mở cổng:
| # | Việc cần làm | Trong receiver.py |
|---|---|---|
| 1 | Chỉ nghe đúng nơi cần. Dùng 127.0.0.1 hoặc IP cụ thể thay vì 0.0.0.0 khi được | --host |
| 2 | Firewall chỉ cho phép đúng máy gửi (IP biết trước), trên mạng tin cậy | Làm ngoài code |
| 3 | Mã hóa đường truyền bằng TLS | --tls |
| 4 | Xác thực bên gửi (token, mật khẩu, chứng chỉ khách). Token dài, ngẫu nhiên, không đặt trong code để đưa lên Git | --token (nên đọc từ biến môi trường thay vì gõ trên dòng lệnh) |
| 5 | Giới hạn kích thước, độ dài header, thời gian chờ | MAX_SIZE, MAX_HEADER, settimeout |
| 6 | Làm sạch mọi thứ do bên gửi cung cấp: tên file, đường dẫn | os.path.basename, chặn tên rỗng và tên ẩn |
| 7 | Kiểm tra toàn vẹn dữ liệu nhận | SHA-256 |
| 8 | Ghi log, không chạy hay mở file nhận được, và tắt khi xong | Dòng print; không thực thi dữ liệu |
Receiver mẫu chưa làm hết: nó chưa giới hạn số lần thử sai, chưa chống nhiều kết nối đồng thời, chưa xoay vòng log. Đó là chủ ý để bạn thấy một chương trình mạng "chạy được" cách xa "đủ an toàn" thế nào, và vì sao ngoài đời nên ưu tiên công cụ chuẩn.
BÀI TẬP 6 · Về nhà, 25 PHÚT
Nâng cấp receiver và viết báo cáo rủi ro
Chọn hai cải tiến cho receiver.py và làm được:
- Chỉ nhận file có đuôi cho phép (ví dụ
.csvvà.txt). - Ghi log vào file kèm giờ, IP, tên file, kết quả.
- Đọc token từ biến môi trường
RECEIVER_TOKENthay vì tham số dòng lệnh. - Từ chối IP gửi nếu không nằm trong danh sách cho phép.
Sau đó viết ngắn: nếu chương trình này chạy thật trong câu lạc bộ, ba rủi ro lớn nhất còn lại và cách giảm từng rủi ro. Không đưa token hay chứng chỉ thật lên nơi công khai.
Xem đáp án
Không có đáp án duy nhất. Ví dụ: (1) thêm if not name.lower().endswith((".csv", ".txt")): raise ValueError(...) sau khi làm sạch tên; (2) mở file log chế độ append và ghi một dòng trong mỗi nhánh thành công/lỗi; (3) token = os.environ.get("RECEIVER_TOKEN", "") và thoát nếu rỗng; (4) so addr[0] với một tập IP cho phép, đóng kết nối nếu không có. Rủi ro còn lại có thể là: dò đoán token (giới hạn lần thử), lộ chứng chỉ/khóa (quản lý khóa, hết hạn ngắn), máy nhận bị chiếm (cập nhật, quyền tối thiểu cho tài khoản chạy chương trình), người dùng quên tắt chương trình (đặt hẹn giờ tự thoát).
Tóm tắt
- Hai máy nói chuyện được khi đúng ba điều: đúng IP và port, có chương trình đang lắng nghe đúng nơi, không bị firewall chặn. "Bị từ chối ngay" khác "treo rồi timed out".
- Máy chủ web tạm tiện nhưng không mật khẩu, không mã hóa; mọi thiết bị cùng mạng truy cập được. Tắt ngay khi xong.
- TCP là dòng byte, không giữ ranh giới thông điệp. Ứng dụng tự bằng độ dài hoặc ký tự kết thúc.
- nhẹ và nhanh nhưng không bảo đảm, không biết bên kia có nhận không, và dễ giả địa chỉ gửi.
- Chương trình nhận dữ liệu phải kiểm tra mọi thứ từ bên gửi: xác thực, giới hạn kích thước, làm sạch tên file (), kiểm tra .
- Xác thực không có ý nghĩa nếu đường truyền chữ rõ. Dùng hoặc công cụ chuẩn như SSH/
scp, đừng tự phát minh giao thức cho việc thật.
Tự kiểm tra · 6 câu có giải thích +
Ghi chú cá nhân +
Tự lưu trên trình duyệt. Không nhập thông tin bí mật.