Lập trình Socket - Nền tảng của Giao tiếp Mạng

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:

  1. Client nhập rỗng sẽ dừng lại
  2. 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)
  3. Server khởi động lại nhiều lần báo lỗi cổng
  4. Client đóng bất thường, server báo lỗi
    • Giải quyết bằng bắt ngoại lệ
  5. 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
  6. 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:

  1. 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.
  2. Đặ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")

Thẻ: lập trình socket giao thức mạng tcp/udp osi truyền dữ liệu

Đăng vào ngày 22 tháng 5 lúc 12:51