Tóm tắt kiến thức trước
Ôn tập lập trình hướng đối tượng (serialization JSON)
- Khái niệm đối tượng, lớp, lớp cha
- Ba đặc tính: đóng gói, kế thừa, đa hình
- Phương thức có hai dấu gạch dưới (__): tự động kích hoạt khi điều kiện cụ thể được đáp ứng
- __init__: kích hoạt tự động khi đối tượng được khởi tạo
- __str__: kích hoạt tự động khi đối tượng được in ra
- __call__: kích hoạt tự động khi đối tượng được gọi với dấu ngoặc đơn
- Phản xạ: sử dụng chuỗi để thao tác thuộc tính hoặc phương thức của đối tượng
- hasattr
- getattr
- setattr
Serialization JSON cho kiểu dữ liệu Python không mặc định
- Phổ biến: chuyển đổi thủ thành chuỗi
- Không phổ biến: sửa lại lớp trỏ đến bởi tham số cls
Kiến trúc phần mềm (Doanh nghiệp - Cá nhân)
- B: Thương mại hóa
- C: Cá nhân hóa
Bản chất: Kiến trúc b/s về cơ bản cũng là c/s
Lịch sử phát triển truyền dữ liệu từ xa
Hầu hết các công nghệ tiên tiến đều ra đời từ lĩnh vực quân sự. Để thực hiện truyền dữ liệu từ xa, cần có "phương tiện liên kết vật lý".
Giao thức 7 tầng OSI
Ứng dụng - Trình bày - Hội thoại - Truyền tải - Mạng - Dữ liệu - Vật lý
Giới thiệu về giao thức và phần cứng
Tầng vật lý
- Thẻ mạng, cáp mạng
Tầng liên kết dữ liệu
- Cách nhóm tín hiệu điện, giao thức Ethernet
- Địa chỉ MAC: số hex 16 chữ số
- Địa chỉ MAC chỉ có thể tương tác dữ liệu trong mạng cục bộ
- Bộ chuyển mạch (Switch)
- Bộ định tuyến (Router)
- Mạng cục bộ (LAN)
- Internet
- Truy cập Internet là đi dọc theo cáp mạng để truy cập tài nguyên trên máy tính khác (mạng chỉ an toàn hơn)
- Broadcast & Unicast: bão broadcast
Tầng mạng
- Giao thức IP: Địa chỉ IP dùng để xác định một máy tính kết nối Internet
- IPV4 & IPV6
- Giao thức PORT
- Dùng để xác định một ứng dụng cụ thể trên máy tính
- Cổng được phân bổ động, cổng hệ thống (0-1024), cổng chương trình phổ biến (1024-8000)
- IP + PORT: xác định duy nhất một ứng dụng cụ thể trên một máy tính
Tầng truyền tải
- TCP
- UDP
Tầng ứng dụng
- HTTP
- FTP
- HTTPS
TCP và UDP
TCP: Giao thức đáng tin cậy, giao thức luồng
- Bắt tay ba lần, thiết lập kết nối
- Vẫy tay bốn lần, ngắt kết nối
UDP: Giao thức không đáng tin cậy, giao thức gói tin
Tổng quan
- Lập trình socket
- Nắm vững viết mã cơ bản cho client và server
- Vòng lặp giao tiếp, vòng lặp kết nối, tối ưu hóa mã
- Hình tượng dính gói TCP (giao thức luồng)
- Gói tin, tạo header, module struct, hình thức đóng gói
Nội dung chi tiết
1. Socket lập trình
Yêu cầu: Viết một chương trình có thể trao đổi dữ liệu
Bất cứ khi nào liên quan đến trao đổi dữ liệu từ xa, phải thao tác với giao thức 7 tầng OSI, vì vậy có sẵn module để thực hiện trực tiếp (module socket).
Kiến trúc: Khởi động client trước, sau đó khởi động server
1.1. Viết chương trình socket
1.1.1. Server (đơn giản nhất)
import socket
# Tạo đối tượng socket, mặc định sử dụng giao thức TCP trên mạng
server = socket.socket()
# Gắn IP & cổng - tương tự như lắp thẻ SIM vào điện thoại
server.bind(('0.0.0.0', 8080))
# Lắng nghe - tương tự như bật điện thoại (lưu ý: client chờ trong hàng đợi)
sock, address = server.accept() # Lắng nghe - trạng thái bắt tay ba lần
data = sock.recv(1024) # Nhận tin nhắn từ client (1024 bytes) - tương tự như nghe người khác nói sau khi gọi thành công
print(data) # In ra lời nói
sock.send(b'hello my big baby') # Nói với người khác
sock.close() # Tắt cuộc gọi
server.close() # Tắt điện thoại
Trong Python, kiểu bytes có thể được xem như nhị phân
->: Hàm mũi tên, xác nhận kiểu trả về
Hàm trả về nhiều giá trị, mặc định là tuple
Tập thói quen đọc mã nguồn
1.1.2. Client (đơn giản nhất)
import socket
# Tạo đối tượng socket - tương tự như mua điện thoại
client = socket.socket()
# Kết nối đến server - tương tự như quay số
client.connect(('127.0.0.1', 8080))
# Gửi tin nhắn
client.send(b'hello')
# Nhận dữ liệu
data = client.recv(1024)
# In ra dữ liệu nhận được
print(data)
# Đóng kết nối
client.close()
1.2. Tùy chỉnh vòng lặp giao tiếp và tối ưu hóa mã
Vấn đề với mã trên:
- Client nhập rỗng sẽ dừng lại
- Server nhận dữ liệu rỗng cũng sẽ dừng lại (lưu ý: hệ thống Mac & Linux thì không)
- Server khởi động lại nhiều lần báo lỗi cổng
- Client đóng bất thường, server báo lỗi
- Giải quyết bằng bắt ngoại lệ
- Client kết thúc, server cũng kết thúc cùng lúc
- Giải quyết bằng vòng lặp kết nối
- Nửa kết nối (half-connection pool)
- Thiết lập số lượng client có thể chờ đợi
1.2.1. Server
import socket
from socket import SOL_SOCKET, SO_REUSEADDR
# Tạo đối tượng socket
server = socket.socket()
# Thiết lập tùy chọn để giải quyết vấn đề phục hồi cổng chậm của server
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# Gắn địa chỉ IP và cổng
server.bind(('0.0.0.0', 8080))
# Bắt đầu lắng nghe
server.listen(5)
# Vòng lặp chấp nhận kết nối
while True:
# Chấp nhận kết nối mới
sock, address = server.accept()
# Vòng lặp giao tiếp với client
while True:
try:
# Nhận dữ liệu từ client
data = sock.recv(1024)
# Nếu không có dữ liệu, bỏ qua
if len(data) == 0:
continue
# Giải mã và in dữ liệu
print(data.decode('utf8'))
# Gửi phản hồi
sock.send(b'hello')
except Exception:
# Nếu có lỗi, thoát vòng lặp
break
1.2.2. Client
import socket
# Tạo đối tượng socket
client = socket.socket()
# Kết nối đến server
client.connect(('127.0.0.1', 8080))
# Vòng lặp giao tiếp
while True:
# Nhập tin nhắn từ người dùng
msg = input('Nhập tin nhắn: ').strip()
# Nếu tin nhắn rỗng, bỏ qua
if len(msg) == 0:
continue
# Gửi tin nhắn đã mã hóa
client.send(msg.encode())
# Nhận phản hồi từ server
data = client.recv(1024)
# Giải mã và in dữ liệu (lưu ý: trên hệ thống Windows cần giải mã bằng gbk)
print(data.decode('utf8'))
2. Vấn đề dính gói (sticky packet)
Vấn đề:
- Khi dữ liệu nhận lớn hơn kích thước recv, đường ống truyền dữ liệu sẽ còn dữ liệu dư, khi recv lần nữa, dữ liệu nhận ra vẫn là dữ liệu dư từ lần trước.
- Dữ lượng nhỏ và khoảng cách gửi ngắn, giao thức TCP sẽ đóng gói và gửi cùng một lúc.
Nguyên nhân:
- Dữ liệu trong đường ống chưa được lấy hết, lần thực thi lệnh tiếp theo lấy ra vẫn là dữ liệu dư từ lần thực thi lệnh trước.
- Đặc tính tự có của giao thức TCP, khi dữ lượng nhỏ và thời gian giữa các lần gửi ngắn, TCP sẽ tự động đóng gói thành một dữ liệu để gửi.
Giải pháp:
- Header: Có thể xác định thông tin cụ thể của dữ liệu sắp đến, bao gồm kích thước dữ liệu
- Chiều dài header phải cố định
2.1. Giải quyết vấn đề dính gói bằng module struct
Module struct có thể biên dịch một lượng dữ liệu thành kích thước cố định (theo chế độ)
Dựa trên lý luận trên, có thể gửi kích thước thực tế của dữ liệu sắp gửi đi trước
Server
import struct # Nhập module chuyển đổi sang byte cố định
import socket # Nhập module truyền mạng
import json # Nhập module serialization JSON
# Tạo đối tượng socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Thiết lập tùy chọn để giải quyết vấn đề phục hồi cổng chậm của server
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Gắn địa chỉ IP và cổng
server.bind(('0.0.0.0', 8080))
# Bắt đầu lắng nghe
server.listen(5)
# Vòng lặp chấp nhận kết nối
while True:
# Chấp nhận kết nối mới
sock, address = server.accept()
# Vòng lặp giao tiếp với client
while True:
try:
# Nhận dữ liệu từ client
data = sock.recv(1024)
# Nếu không có dữ liệu, bỏ qua
if len(data) == 0:
continue
# In dữ liệu đã nhận
print(data.decode('utf8'))
# Chuẩn bị dữ liệu để gửi
file_data = "Dữ liệu cần gửi"
file_size = len(file_data)
# Tạo dictionary chứa thông tin
dic = {
'file_name': 'data.txt',
'file_size': file_size
}
# Serialize dictionary thành chuỗi JSON
data_json = json.dumps(dic)
# Tính độ dài của chuỗi JSON đã serialize
json_size = len(data_json)
# Tạo header từ độ dài chuỗi JSON
header = struct.pack('i', json_size)
# Gửi header trước
sock.send(header)
# Gửi dictionary đã serialize
sock.send(data_json.encode('utf8'))
# Gửi dữ liệu thực
sock.send(file_data.encode('utf8'))
except Exception as e:
# Xử lý lỗi
print(f"Lỗi: {e}")
break
Client
import json
import socket
import struct
# Tạo đối tượng socket
client = socket.socket()
# Kết nối đến server
client.connect(('127.0.0.1', 8080))
# Vòng lặp giao tiếp
while True:
# Nhập yêu cầu từ người dùng
msg = input('Nhập thông tin cần tải: ').strip()
# Nếu yêu cầu rỗng, bỏ qua
if len(msg) == 0:
continue
# Gửi yêu cầu đã mã hóa
client.send(msg.encode())
# Nhận header (4 bytes đầu tiên)
data = client.recv(4)
# Giải mã header để lấy kích thước dictionary
dict_size = struct.unpack('i', data)[0]
# Nhận dictionary đã serialize
dic = client.recv(dict_size)
# Deserialize dictionary từ JSON
dic_json = json.loads(dic.decode('utf8'))
# Lấy kích thước file từ dictionary
file_size = dic_json.get('file_size')
# Nhận dữ liệu thực
received_data = b''
while len(received_data) < file_size:
chunk = client.recv(1024)
if not chunk:
break
received_data += chunk
# Xử lý dữ liệu nhận được
print(f"Đã nhận {len(received_data)} bytes dữ liệu")
Mã tải lên file
import json
import socket
import struct
import os
# Tạo đối tượng socket
client = socket.socket()
# Kết nối đến server
client.connect(('127.0.0.1', 8080))
# Vòng lặp giao tiếp
while True:
# Đường dẫn đến thư mục chứa file
data_path = r'D:\金牌班级相关资料\网络并发day01\视频'
# Lấy danh sách file trong thư mục
file_list = os.listdir(data_path)
# Hiển thị danh sách file cho người dùng
for i, filename in enumerate(file_list, 1):
print(f"{i}. {filename}")
# Lấy lựa chọn từ người dùng
choice = input('Chọn số thứ tự file cần tải lên: ').strip()
# Kiểm tra lựa chọn hợp lệ
if choice.isdigit():
choice = int(choice)
if 1 <= choice <= len(file_list):
# Lấy tên file
filename = file_list[choice - 1]
# Tạo đường dẫn đầy đủ đến file
filepath = os.path.join(data_path, filename)
# Tạo dictionary chứa thông tin file
file_info = {
'file_name': filename,
'description': 'File quan trọng',
'size': os.path.getsize(filepath),
'info': 'Thông tin bổ sung'
}
# Serialize dictionary thành chuỗi JSON
json_data = json.dumps(file_info)
# Tạo header từ độ dài chuỗi JSON
header = struct.pack('i', len(json_data))
# Gửi header
client.send(header)
# Gửi dictionary đã serialize
client.send(json_data.encode('utf8'))
# Gửi file theo từng đoạn
with open(filepath, 'rb') as f:
for chunk in f:
client.send(chunk)
# Server nhận file
def receive_file(sock):
# Nhận header (4 bytes đầu tiên)
header = sock.recv(4)
# Giải mã header để lấy kích thước dictionary
dict_size = struct.unpack('i', header)[0]
# Nhận dictionary đã serialize
dict_data = sock.recv(dict_size)
# Deserialize dictionary từ JSON
file_info = json.loads(dict_data.decode('utf8'))
# Lấy thông tin từ dictionary
filename = file_info.get('file_name')
file_size = file_info.get('size')
# Nhận dữ liệu file
received_size = 0
with open(filename, 'wb') as f:
while received_size < file_size:
chunk = sock.recv(1024)
if not chunk:
break
received_size += len(chunk)
f.write(chunk)
print(f"Đã nhận xong file {filename}, tổng cộng {received_size} bytes")