BẢO MẬTCĂN BẢNTUẦN 01–04
BÀI 09

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ột file đi từ máy gửi qua router tới máy nhận: cần đúng địa chỉ, đủ và đúng dữ liệu, và kênh được bảo vệ.
Một file đi từ máy gửi qua router tới máy nhận: cần đúng địa chỉ, đủ và đúng dữ liệu, và kênh được bảo vệ.
01

Mạch bài: một file đi từ máy này sang máy kia

5 phút
Máy gửiKết nối tới IP:port
Mạng nhàRouter, Wi-Fi
Máy nhậnĐang lắng nghe

Bà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 đó?

Tình huốngLan có file 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áchDùng khiMã hóa, xác thựcPhần
Máy chủ web tạm (http.server)Chia sẻ nhanh một thư mụcKhông, ai cùng mạng cũng tải đượcB
Socket TCP tự viếtHiểu cách các máy nói chuyện; chương trình riêngTự làmC, E
Socket UDPThông báo nhanh, tìm thiết bịKhôngD
TCP + TLS + tokenGửi file có bảo vệ trong chương trình tự viếtCóF
SSH/scp, ứng dụng chuyên dụngViệc thật, dùng thường xuyênCó, đã được kiểm chứngF
XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1Thử trước trên một máy
“A và B là hai cửa sổ, không phải hai laptop.”Terminal AChương trình chờChạy trước127.0.0.1Quay về chính máy nàyKhông qua Wi-FiTerminal BChương trình kết nốiChạy sau

127.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.

2Gửi file: B đẩy sang A
“Lan có file; máy thủ quỹ mở cửa nhận.”B · Máy Lansender.py + file CSVCó file gốcWi-Fi nội bộĐến IP của máy AIP ví dụ .23A · Máy thủ quỹreceiver.py :9000Nhận rồi lưu

Ở 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.

3Chia sẻ web: B kéo file từ A
“Đổi cách: máy có file mở web cho bên kia tải.”A · Máy có filehttp.server :8000Cung cấp filethanh-vien.txtPhản hồi HTTPĐi từ A sang BB · Máy cần fileTrình duyệt / curlTải bản sao

Ở 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ắt đầu bằng demo hai terminal

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.

Lộ trình: mỗi lần chỉ thêm một việc mới
  1. A–B: hiểu địa chỉ và tải một file bằng web.
  2. C–D: gửi một câu bằng TCP, rồi so với UDP.
  3. E: gửi file, nhận OK, kiểm tra bản nhận.
  4. 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.
Quy tắc an toànChỉ dùng file giả khi thực hành, không gửi dữ liệu thật của người khác. Chỉ mở cổng nhận dữ liệu trên mạng tin cậy và tắt chương trình ngay khi xong. Chỉ kết nối tới máy của mình hoặc máy được cho phép.
02

A. Điều kiện để hai máy nói chuyện được với nhau

12 phút
Đúng địa chỉIP và port của máy nhận
Có người ngheChương trình đang lắng nghe
Không bị chặnFirewall cho phép

Chươ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àoAi kết nối đượcDùng khi
127.0.0.1Chỉ 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ạngChia sẻ trong mạng nhà
0.0.0.0Mọ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:

MACOS / LINUX
ping -c 3 192.168.1.23
nc -vz 192.168.1.23 9000
WINDOWS POWERSHELL
ping 192.168.1.23
Test-NetConnection 192.168.1.23 -Port 9000
LINUX
ping -c 3 192.168.1.23
nc -vz 192.168.1.23 9000
PhầnÝ nghĩa
pingHỏ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 PORTnc (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 PORTLệ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ĩaViệc kiểm tra tiếp
succeeded / TrueCó đường đi và có chương trình đang ngheChuyể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ốiChươ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 outGói không tới nơi, hoặc bị chặn im lặngFirewall của máy nhận, hai máy khác mạng/mạng khách, sai IP
No route to hostKhông tìm được đường tới IPSai 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:

  1. nc -vz 192.168.1.23 9000 báo "Connection refused". Trên máy nhận, chương trình in "Dang nghe 127.0.0.1:9000".
  2. 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.
  3. 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
  1. Chương trình nghe ở 127.0.0.1 nê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).
  2. 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ử).
  3. 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).
03

B. Cách nhanh nhất: máy chủ web tạm và rủi ro của nó

10 phút
Thư mục chia sẻChỉ chứa file cần gửi
http.serverMáy có file mở port 8000
Máy khácTải bằng trình duyệt

Má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.

XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1A: mở đúng thư mục
“Chỉ bày ra đúng file định chia sẻ.”Terminal AĐang ở ~/shareMáy có filethanh-vien.txtDữ liệu giả để tậpTrong thư mục sharePort 8000http.serverBắt đầu chờ

Server 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.

2B: yêu cầu và nhận bản sao
“Cho mình file thanh-vien.txt.”Terminal Bcurl hoặc trình duyệtGửi yêu cầuA · Port 8000GET /thanh-vien.txtTrả nội dung fileBản tải vềTrong thư mục của BKiểm tra nội dung

B 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ề.

3A: đóng quầy khi xong
“Tắt server thì người khác không tải tiếp được.”Terminal ANhấn Ctrl+CDừng chương trìnhPort 8000Không còn server nàyĐã đóngBản đã tảiVẫn nằm ở máy BKhông tự xóa

Bả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ả

MACOS
mkdir -p ~/share
cd ~/share
printf "Danh sach thanh vien (du lieu gia)\n" > thanh-vien.txt
WINDOWS POWERSHELL
New-Item -ItemType Directory -Force $HOME\share
cd $HOME\share
Set-Content thanh-vien.txt "Danh sach thanh vien (du lieu gia)" -Encoding ascii
LINUX
mkdir -p ~/share
cd ~/share
printf "Danh sach thanh vien (du lieu gia)\n" > thanh-vien.txt

share 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

TERMINAL A · MÁY CÓ FILE
python3 -m http.server 8000 --bind 127.0.0.1
TERMINAL A · MÁY CÓ FILE
py -m http.server 8000 --bind 127.0.0.1
TERMINAL A · MÁY CÓ FILE
python3 -m http.server 8000 --bind 127.0.0.1
KẾT QUẢ MONG ĐỢI Ở A
Serving HTTP on 127.0.0.1 port 8000 ...
✓ Kiểm tra trước khi đi tiếp

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

MACOS
mkdir -p ~/download-lab
cd ~/download-lab
curl -O http://127.0.0.1:8000/thanh-vien.txt
cat thanh-vien.txt
WINDOWS POWERSHELL
New-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.txt
LINUX
mkdir -p ~/download-lab
cd ~/download-lab
curl -O http://127.0.0.1:8000/thanh-vien.txt
cat thanh-vien.txt
KẾT QUẢ NỘI DUNG FILE Ở B
Danh 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.

✓ Kiểm tra trước khi đi tiếp

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ụ:

MÁY A · CÓ FILE · IP CHỈ MINH HỌA
python3 -m http.server 8000 --bind 192.168.1.23
MÁY A · CÓ FILE · IP CHỈ MINH HỌA
py -m http.server 8000 --bind 192.168.1.23
MÁY A · CÓ FILE · IP CHỈ MINH HỌA
python3 -m http.server 8000 --bind 192.168.1.23

Má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.

✓ Kiểm tra trước khi đi tiếp

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:

  1. 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ì?
  2. Dùng công cụ nào ở bài 8 để thấy server này trong bảng kiểm kê?
  3. Bắt gói trên Wireshark khi tải file: nội dung file thấy được không?
Xem đáp án
  1. Đượ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.
  2. lan_inventory.py sẽ in port 8000 là http-alt kèm banner Server: SimpleHTTP/0.6 Python/… ở thiết bị chạy server.
  3. 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.
04

C. Socket TCP từ đầu: một kết nối, một dòng chữ

15 phút
Serverbind, listen, accept
Kết nối TCPBắt tay ba bước
Clientconnect, gửi, đọc
TỰ BẤM THỬ · MÔ PHỎNG 2 TERMINAL TRÊN 1 MÁY

Thử 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 · BÊN NHẬN127.0.0.1:9001

A chưa chạy

B · BÊN GỬItcp_client.py

Chạy thật: sửa chuỗi trong file client nếu bạn thay thông điệp ở đây.

A · nhận
TCP · 127.0.0.1 · cùng một máy
B · gửi

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.

Terminal A · chạy trước
Terminal B · chạy sau

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
XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1A: mở cửa và chờ
“Tôi nhận kết nối ở 127.0.0.1:9001.”Terminal Atcp_server.pyChạy trước127.0.0.1:9001bind → listenChờ kết nốiTerminal BChưa chạy clientChuẩn bị

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ờ.

2B: kết nối đúng cửa
“Tôi muốn nói chuyện với server ở port 9001.”Terminal Btcp_client.pyChủ động kết nốiTCP :9001Thiết lập kết nốiHai bên sẵn sàngTerminal Aaccept trả về connCó kết nối

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.

3B gửi, A đọc và đáp
“Xin chao tu client” → “Server da nhan …”B · Clientencode → sendallGửi byteDữ liệu TCPChuỗi được mã UTF-8Chưa mã hóa TLSA · Serverrecv → sendallĐọc rồi trả lời

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.

4Kết thúc một lần thử
“Server mẫu chỉ phục vụ một lượt.”A · ServerRa khỏi khối withĐã thoátB · ClientIn phản hồi rồi thoátHoàn tấtMuốn thử lại?Chạy A trước, rồi BLặp đúng thứ tự

Code 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

MACOS
mkdir ~/transfer-lab
cd ~/transfer-lab
pwd
WINDOWS POWERSHELL
mkdir $HOME\transfer-lab
cd $HOME\transfer-lab
pwd
LINUX
mkdir ~/transfer-lab
cd ~/transfer-lab
pwd

Tạ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.

↓ Tải tcp_server.py
Xem mã Python · tô màu và số dòng
PYTHON · tcp_server.py
# 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
MACOS: TẠO tcp_server.py
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
WINDOWS POWERSHELL: TẠO tcp_server.py
@'
# 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 ascii
LINUX: TẠO tcp_server.py
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_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.

↓ Tải tcp_client.py
Xem mã Python · tô màu và số dòng
PYTHON · tcp_client.py
# 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
MACOS: TẠO tcp_client.py
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
WINDOWS POWERSHELL: TẠO tcp_client.py
@'
# 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 ascii
LINUX: TẠO tcp_client.py
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
✓ Kiểm tra trước khi đi tiếp

Trong 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:

TERMINAL A
python3 tcp_server.py
TERMINAL A
py tcp_server.py
TERMINAL A
python3 tcp_server.py
KẾT QUẢ MONG ĐỢI Ở A
Dang cho ket noi o 127.0.0.1:9001 ...
✓ Kiểm tra trước khi đi tiếp

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

TERMINAL B
python3 tcp_client.py
TERMINAL B
py tcp_client.py
TERMINAL B
python3 tcp_client.py
KẾT QUẢ Ở B (CLIENT)
Tra loi: Server da nhan: Xin chao tu client
KẾT QUẢ Ở A (SERVER), SAU KHI CLIENT CHẠY
Co ket noi tu ('127.0.0.1', 52454)
Nhan duoc: Xin chao tu client

Số 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ỏ

XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1Bên gửi có hai lần gửi
“AAAA” rồi “BBBB”.Lần gửi 1AAAA4 byteLuồng TCPAAAABBBB8 byte liên tụcLần gửi 2BBBB4 byte

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.

2Một lần đọc có thể thiếu
“Mình mới đọc được AAA; vẫn còn 5 byte.”Đọc lượt đầuAAA3 byteĐọc lượt tiếpABBBB5 byte còn lạiĐếm byte còn thiếu8 − 3 = 5Tiếp tục đọc

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 đủ.

3Nhãn trước, nội dung sau
“size = 8: đọc tiếp đúng 8 byte.”Header JSON{"size": 8} + xuống dòngĐọc đến dấu xuống dòngNội dungAAAABBBBĐọc đủ 8 byteKiểm tra kết thúcThiếu byte → báo lỗiKhông lưu file thiếu

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

  1. Chạy tcp_client.py khi chưa chạy server. Ghi lại lỗi, giải thích bằng bài 6.
  2. Sửa tcp_client.py để gửi tên bạn (Lan) thay vì "Xin chao tu client". Server in ra gì?
  3. Bên gửi gọi sendall(b"AAAA") rồi sendall(b"BBBB"). Bên nhận gọi recv(1024). Chắc chắn nhận được gì?
Xem đáp án
  1. 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.
  2. Đổ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.
  3. Không chắc chắn được. Có thể nhận b"AAAABBBB" (gộp) hoặc b"AAAA" trước và b"BBBB" sau. Phải tự .
05

D. UDP: gửi nhanh, không bảo đảm

8 phút
Gửisendto(IP, port)
MạngCó thể mất, đảo thứ tự
Ngherecvfrom()
TỰ BẤM THỬ · MÔ PHỎNG 2 TERMINAL TRÊN 1 MÁY

Thử 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 · BÊN NHẬN127.0.0.1:9002

A chưa chạy

B · BÊN GỬIudp_send.py

Chạy thật: sửa chuỗi trong file client nếu bạn thay thông điệp ở đây.

A · nhận
UDP · 127.0.0.1 · cùng một máy
B · gửi

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.

Terminal A · chạy trước
Terminal B · chạy sau

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
XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1A đang nghe
“Gửi một gói tới 127.0.0.1:9002.”B · udp_send.pysendto()Gửi một góiGói UDPToi la thiet bi AMột thông điệpA · udp_listen.pyrecvfrom()Đang nghe

Khô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.

2Dừng A rồi gửi lại
“B vẫn có thể báo Da gui.”B · udp_send.pysendto() trả vềĐã gửi khỏi chương trìnhGói UDPKhông có xác nhận ứng dụngChưa biết có đếnA đã dừngCtrl+C ở AKhông ai đọc

Trong 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.

3Cần chắc đã xử lý?
“Bên nhận phải gửi phản hồi theo quy ước.”Bên gửiChờ phản hồi / timeoutDo ứng dụng đặtBên nhậnXử lý rồi trả kết quảXác nhận ứng dụngGửi file ở bài nàyTCP + phản hồi OKDùng ở phần E

TCP 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.

↓ Tải udp_listen.py
Xem mã Python · tô màu và số dòng
PYTHON · udp_listen.py
# 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
MACOS: TẠO udp_listen.py
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
WINDOWS POWERSHELL: TẠO udp_listen.py
@'
# 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 ascii
LINUX: TẠO udp_listen.py
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_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.

↓ Tải udp_send.py
Xem mã Python · tô màu và số dòng
PYTHON · udp_send.py
# 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
MACOS: TẠO udp_send.py
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
WINDOWS POWERSHELL: TẠO udp_send.py
@'
# 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 ascii
LINUX: TẠO udp_send.py
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

Bướ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:

TERMINAL A
python3 udp_listen.py
TERMINAL A
py udp_listen.py
TERMINAL A
python3 udp_listen.py
TERMINAL B
python3 udp_send.py
TERMINAL B
py udp_send.py
TERMINAL B
python3 udp_send.py
KẾT QUẢ Ở A
Dang nghe UDP o 127.0.0.1:9002 ...
Tu 127.0.0.1:61947 -> Toi la thiet bi A

Bướ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.

TCPUDP
Bắt tay trước khi gửiCó (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ưaCần phản hồi của ứng dụng, như OKCũng cần phản hồi của ứng dụng
Ranh giới thông điệpKhông (dòng byte)Có: mỗi gói là một thông điệp
Dùng khiFile, web, hầu hết ứng dụng cần đúngTìm thiết bị, thoại và video, truy vấn nhỏ
Giả mạo người gửiKhó 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".

✓ Kiểm tra trước khi đi tiếp

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:

  1. Chuyển file danh sách thành viên.
  2. Máy in "thông báo" cho mọi máy trong mạng rằng nó vừa bật.
  3. Gửi lệnh "mở cửa" tới thiết bị điều khiển cửa.
Xem đáp án
  1. TCP: cần đủ và đúng thứ tự, mất một byte là file hỏng.
  2. 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.
  3. 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.
06

E. Lab: gửi file có kiểm tra toàn vẹn

25 phút
Header JSONTên, kích thước, SHA-256, token
Nội dung fileĐúng số byte đã khai
Kiểm traBăm khớp, tên an toàn
TỰ BẤM THỬ · MÔ PHỎNG 2 TERMINAL TRÊN 1 MÁY

Demo 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 · BÊN NHẬN127.0.0.1:9000

A chưa chạy

File ở A · received/thanh-vien.csv
Chưa nhận được file.
B · BÊN GỬIsender.py

Gửi file dưới tên thanh-vien.csv. Demo tự đếm số byte UTF-8.

A · nhận
TCP · 127.0.0.1 · cùng một máy
B · gửi

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.

Terminal A · chạy trước
Terminal B · chạy sau

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
XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1B đọc file và ghi nhãn
“Tên gì? Bao nhiêu byte? Mã băm nào?”File gốc ở Bthanh-vien.csvDữ liệu giảHeader JSONname · size · sha256Kèm tokenA · ReceiverĐang chờ :9000Chưa lưu file

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õ.

2A kiểm tra phần mô tả
“Token đúng? Size trong giới hạn? Tên hợp lệ?”Header nhận đượcMột dòng JSONĐọc tới xuống dòngKiểm tra đầu vàoToken · size · tênKhông đạt → LOIThư mục nhậnreceived/Chưa ghi file

Receiver 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ó.

3A đọc đủ nội dung
“Thiếu bao nhiêu byte, đọc tiếp bấy nhiêu.”Một phần dữ liệuVí dụ 10 / 35 byteChưa đủPhần còn lạiCòn 25 byteĐọc tiếpĐủ theo size35 / 35 byteChuyển sang kiểm tra

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.

4A so băm rồi mới lưu
“Băm khớp mới ghi received/thanh-vien.csv.”Nội dung nhậnTính SHA-256 lạiKhông tin lời khaiSo với headerBăm khớpCho phép lưureceived/thanh-vien.csvBản nhận ở A

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.

5A gửi kết quả, B mới kết luận
“OK: chương trình nhận đã lưu xong.”A · ReceiverLưu rồi sendall(b"OK\n")Trả kết quảPhản hồiOK hoặc LOI …Đi từ A về BB · SenderMay nhan tra loi: OKĐọc phản hồi

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.

Đích đến của lab E

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:

GIAO THỨC TỰ ĐẶT
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.

↓ Tải receiver.py
Xem mã Python · tô màu và số dòng
PYTHON · receiver.py
# 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
MACOS: TẠO receiver.py
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
WINDOWS POWERSHELL: TẠO receiver.py
@'
# 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 ascii
LINUX: TẠO receiver.py
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

sender.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.

↓ Tải sender.py
Xem mã Python · tô màu và số dòng
PYTHON · sender.py
# 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
MACOS: TẠO sender.py
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
WINDOWS POWERSHELL: TẠO sender.py
@'
# 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 ascii
LINUX: TẠO sender.py
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

Và một file thử, chỉ có dữ liệu giả:

MACOS
printf 'ten,so_tien\nLan,200000\nMinh,150000\n' > thanh-vien.csv
WINDOWS POWERSHELL
Set-Content -Path thanh-vien.csv -Value "ten,so_tien","Lan,200000","Minh,150000" -Encoding ascii
LINUX
printf 'ten,so_tien\nLan,200000\nMinh,150000\n' > thanh-vien.csv
✓ Kiểm tra trước khi đi tiếp

Có đủ 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 MBGiớ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, --tlsTham 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):

TERMINAL A
python3 receiver.py
TERMINAL A
py receiver.py
TERMINAL A
python3 receiver.py
KẾT QUẢ Ở A
Dang nghe 127.0.0.1:9000 (KHONG ma hoa). Ctrl+C de dung.
✓ Kiểm tra trước khi đi tiếp

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

TERMINAL B
python3 sender.py thanh-vien.csv
TERMINAL B
py sender.py thanh-vien.csv
TERMINAL B
python3 sender.py thanh-vien.csv
KẾT QUẢ Ở B
May nhan tra loi: OK
KẾT QUẢ Ở A SAU ĐÓ
Nhan thanh-vien.csv (35 byte) tu 127.0.0.1

Bướ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.

MACOS / LINUX
cat received/thanh-vien.csv
cmp thanh-vien.csv received/thanh-vien.csv
WINDOWS POWERSHELL
Get-Content received\thanh-vien.csv
Get-FileHash thanh-vien.csv
Get-FileHash received\thanh-vien.csv
LINUX
cat received/thanh-vien.csv
cmp thanh-vien.csv received/thanh-vien.csv

cmp 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.

✓ Kiểm tra trước khi đi tiếp

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

  1. Sai token:
TERMINAL B
python3 sender.py thanh-vien.csv --token sai
TERMINAL B
py sender.py thanh-vien.csv --token sai
TERMINAL B
python3 sender.py thanh-vien.csv --token sai
KẾT QUẢ
May nhan tra loi: LOI sai token
  1. 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):
TERMINAL B
python3 sender.py thanh-vien.csv --name "../../thoat.txt"
TERMINAL B
py sender.py thanh-vien.csv --name "../../thoat.txt"
TERMINAL B
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.

  1. 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ủa body sau khi tính băm. Receiver in LOI SHA-256 khong khop và 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

XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1A: mở cửa ở địa chỉ mạng
“Cho phép kết nối tới đúng IP của máy A.”A · Máy nhậnIP 192.168.1.23Xem trên máy A192.168.1.23:9000receiver.py --host …Chờ trên IP mạngreceiver.pyTrong transfer-labChạy từ đúng thư mục

127.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.

2B: gọi tới IP của A
“Không dùng 0.0.0.0 làm địa chỉ đích.”B · Máy gửisender.py + CSVCó file gốcMạng được phépKhông cách ly A và BĐường tới AA · Máy nhận192.168.1.23:9000Đang chờ

B 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.

3Xác nhận và kết thúc
“B thấy OK; A có bản nhận trong received/.”B · Máy gửiĐọc phản hồi OKĐã đối chiếuA · received/Bản nhận khớp gốcKiểm tra nội dungDừng ở máy ACtrl+CGỡ quyền mở tạm

Nế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:

MÁY NHẬN
python3 receiver.py --host 192.168.1.23
MÁY NHẬN
py receiver.py --host 192.168.1.23
MÁY NHẬN
python3 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:

MÁY GỬI (THAY 192.168.1.23 BẰNG IP MÁY NHẬN)
python3 sender.py thanh-vien.csv --host 192.168.1.23
MÁY GỬI (THAY 192.168.1.23 BẰNG IP MÁY NHẬN)
py sender.py thanh-vien.csv --host 192.168.1.23
MÁY GỬI (THAY 192.168.1.23 BẰNG IP MÁY NHẬN)
python3 sender.py thanh-vien.csv --host 192.168.1.23

Kiể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".

Token chạy trên đường truyền chữ rõ không bảo vệ được gìNgười quan sát được luồng TCP có thể thấy token và dùng lại (giống cookie ở bài 6). Cơ chế xác thực chỉ có ý nghĩa khi đường truyền được mã hóa. Phần F làm việc đó.

Lỗi thường gặp

Dấu hiệuNguyên nhân thường gặpCách xử lý
Khong gui duoc: Connection refusedChưa chạy receiver, sai port, hoặc receiver bind ở 127.0.0.1Chạy receiver trước; kiểm tra --host và --port
Khong gui duoc: timed outFirewall máy nhận, sai IP, hoặc khác mạngBảng ở phần A
LOI sai tokenHai bên đặt --token khác nhauDùng cùng chuỗi ở cả hai phía
LOI SHA-256 khong khopDữ liệu hỏng hoặc bị sửa; hoặc sửa sender.py làm lệchGửi lại; kiểm tra mạng; kiểm tra lại code
Address already in usePort 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

  1. Gửi file có --name ".hidden" và --name "../". Chuyện gì xảy ra, vì sao đoạn code đó có trong handle()?
  2. Hạ MAX_SIZE xuống 10 byte rồi gửi thanh-vien.csv. Kết quả?
  3. 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
  1. 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.
  2. 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.
  3. 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).
07

F. Bảo vệ đường truyền: TLS cho socket và công cụ có sẵn

15 phút
TCPCó luồng byte
TLSMã hóa và xác thực máy nhận
Gửi fileNội dung và token được che
TỰ BẤM THỬ · MÔ PHỎNG 2 TERMINAL TRÊN 1 MÁY

Demo 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 · BÊN NHẬN127.0.0.1:9000

A chưa chạy

File ở A · received/thanh-vien.csv
Chưa nhận được file.
B · BÊN GỬIsender.py

Gửi file dưới tên thanh-vien.csv. Demo tự đếm số byte UTF-8.

A · nhận
TCP + TLS · 127.0.0.1 · cùng một máy
B · gửi

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.

Terminal A · chạy trước
Terminal B · chạy sau

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
XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

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.

1Chưa TLS: nhìn được nội dung
“Trên đường truyền có header và token dạng rõ.”B · SenderHeader + fileTCP thông thườngGói quan sát đượctoken · tên · nội dungChưa được cheA · ReceiverNhận rồi kiểm traChưa có TLS

Trê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.

2A giữ khóa; B nhận chứng chỉ
“Chép chứng chỉ, giữ kín khóa riêng.”A · Máy nhậncert.pem + key.pemGiữ key.pemKênh trao tin cậyVí dụ USBChuyển cert.pemB · Máy gửiChỉ cần cert.pemTin máy A

cert.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.

3Hai bên bật cùng chế độ TLS
“Kiểm tra chứng chỉ rồi mới truyền dữ liệu.”B · Sender --tlsTin cert.pemKiểm tra máy nhậnKênh TLSHeader + file được mã hóaIP/port vẫn quan sát đượcA · Receiver --tlsDùng cert.pem + key.pemĐang chờ TLS

Dừ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ỉ.

4Lưu file như cũ, đường truyền khác
“Receiver vẫn kiểm tra rồi trả OK.”File đã nhậnreceived/an-toan.csvNội dung khớpBắt gói lần TLSKhông đọc được chữ rõKhi không nạp khóa TLSMetadata còn lộIP · port · thời điểmKhông phải nội dung

TLS 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:

MACOS
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"
WINDOWS POWERSHELL (OPENSSL ĐI KÈM GIT FOR WINDOWS)
& "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"
LINUX
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.

✓ Kiểm tra trước khi đi tiếp

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

TERMINAL A (NHẬN, CÓ TLS)
python3 receiver.py --tls
TERMINAL A (NHẬN, CÓ TLS)
py receiver.py --tls
TERMINAL A (NHẬN, CÓ TLS)
python3 receiver.py --tls

Chờ A báo (TLS), rồi qua B:

TERMINAL B (GỬI, CÓ TLS)
python3 sender.py thanh-vien.csv --tls --name an-toan.csv
TERMINAL B (GỬI, CÓ TLS)
py sender.py thanh-vien.csv --tls --name an-toan.csv
TERMINAL B (GỬI, CÓ TLS)
python3 sender.py thanh-vien.csv --tls --name an-toan.csv
KẾT QUẢ
Terminal 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_chainPhí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
  1. A: tạo chứng chỉ bằng lệnh trên, giữ key.pem tại A. Chứng chỉ phải có DNS:localhost vì sender mẫu kiểm tra tên đó.
  2. B: chép đúng cert.pem từ A bằng kênh tin cậy, đặt cạnh sender.py; không cần khóa riêng.
  3. A: chạy receiver.py --host 192.168.1.23 --tls; thay bằng IP thật của A.
  4. B: chạy sender.py thanh-vien.csv --host 192.168.1.23 --tls --name an-toan.csv. Trên Windows dùng py, macOS/Linux dùng python3 trướ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:

Ô LỌC CỦA WIRESHARK
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).

✓ Kiểm tra trước khi đi tiếp

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 sshChé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 ShareGửi giữa điện thoại và máy tính cùng hệ sinh tháiCó 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ậpChia sẻ qua web có người dùng và quyềnThường là cách của website thật
Ví dụ lệnh scpTừ máy gửi tới máy nhận: 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ỉ.
08

G. Danh sách kiểm tra khi mở một cổng nhận dữ liệu

10 phút
Giới hạn phạm viBind, firewall, thời gian
Xác thựcAi được gửi
Kiểm tra dữ liệuKích thước, tên, băm

Chươ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àmTrong receiver.py
1Chỉ 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
2Firewall chỉ cho phép đúng máy gửi (IP biết trước), trên mạng tin cậyLàm ngoài code
3Mã hóa đường truyền bằng TLS--tls
4Xá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)
5Giới hạn kích thước, độ dài header, thời gian chờMAX_SIZE, MAX_HEADER, settimeout
6Làm sạch mọi thứ do bên gửi cung cấp: tên file, đường dẫnos.path.basename, chặn tên rỗng và tên ẩn
7Kiểm tra toàn vẹn dữ liệu nhậnSHA-256
8Ghi log, không chạy hay mở file nhận được, và tắt khi xongDò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:

  1. Chỉ nhận file có đuôi cho phép (ví dụ .csv và .txt).
  2. Ghi log vào file kèm giờ, IP, tên file, kết quả.
  3. Đọc token từ biến môi trường RECEIVER_TOKEN thay vì tham số dòng lệnh.
  4. 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

Tự kiểm tra · 6 câu có giải thích +
1. nc báo "Connection refused" ngay lập tức khi thử tới 192.168.1.23:9000. Điều gì đúng nhất?
2. Chương trình nhận bind ở 127.0.0.1:9000 trên máy A. Máy B cùng Wi-Fi kết nối tới IP của máy A. Kết quả?
3. Bên gửi gọi sendall hai lần, bên nhận recv một lần. Điều chắc chắn là gì?
4. Người gửi đặt tên file "../../x.txt". Cách chặn đúng là gì?
5. Token nằm trong header trên kênh TCP không mã hóa. Điều gì đúng?
6. Việc nào KHÔNG làm được nhờ giá trị SHA-256 gửi kèm file?

Ghi chú cá nhân +

Tự lưu trên trình duyệt. Không nhập thông tin bí mật.

Có thể bỏ đánh dấu bất cứ lúc nào.
Tài liệu tham khảo chính thức

Tra thuật ngữ