Lập trình mạng Socket trong Python

1. Khái niệm Socket

Socket là đối tượng chuẩn hiện hành cung cấp khả năng di chuyển cao cho các ứng dụng mạng dựa trên các giao thức cụ thể như TCP/IP, ICMP/IP, UDP/IP và các bộ giao thức khác. Chúng cho phép chương trình thiết lập kết nối và truyền nhận dữ liệu. Để tạo kênh giao tiếp, mỗi điểm cuối của mạng cần có một đối tượng socket.

Module socket cung cấp một giao diện đơn giản dựa trên đối tượng để truy cập vào mạng kiểu BSD socket ở tầng thấp. Module này hỗ trợ xây dựng cả socket phía máy khách và máy chủ. Để tạo máy chủ đơn giản với TCP và stream socket trong Python, ta sử dụng module socket.

Socket đóng vai trò lớp trừu tượng trung gian giữa ứng dụng và họ giao thức TCP/IP, nó là một tập hợp các giao diện. Trong mẫu thiết kế, Socket thực chất là mẫu Facade, ẩn đi độ phức tạp của họ giao thức TCP/IP đằng sau giao diện socket. Với người dùng, chỉ cần một tập hợp các giao diện đơn giản, còn việc tổ chức dữ liệu theo giao thức sẽ do Socket đảm nhiệm. Vì vậy, chúng ta không cần hiểu sâu về giao thức tcp/udp vì socket đã bao bọc sẵn, chỉ cần tuân theo quy định của socket khi lập trình thì chương trình sẽ tự động tuân thủ tiêu chuẩn tcp/udp.

Bổ sung: Một số người cũng gọi socket là ip+port, trong đó ip xác định vị trí của một máy trong mạng internet, port xác định ứng dụng trên máy đó. Địa chỉ IP được cấu hình trên card mạng, còn port do ứng dụng mở ra. Sự kết hợp giữa ip và port xác định duy nhất một ứng dụng trong mạng internet, trong khi pid là định danh cho các tiến trình hoặc luồng khác nhau trên cùng một máy.

Python cung cấp hai cấp độ dịch vụ mạng:

  • Cấp độ thấp hỗ trợ socket cơ bản, cung cấp API BSD sockets chuẩn, có thể truy cập toàn bộ phương thức của giao diện socket hệ điều hành nền tảng
  • Cấp độ cao hơn là module socketServer, cung cấp các lớp trung tâm máy chủ giúp đơn giản hóa việc phát triển máy chủ mạng.

2. Các phương thức của module Socket

3. Quy trình làm việc của Socket

Thông thường việc thiết lập kết nối socket cần sáu bước: tạo đối tượng socket bằng socket.socket(), ràng buộc địa chỉ vào đối tượng socket bằng bind, lắng nghe cổng địa chỉ bằng listen, chặn chờ chấp nhận yêu cầu kết nối bằng accept, xử lý dữ liệu giao tiếp bằng các phương thức send và recv, và đóng kết nối bằng close.

4. Sử dụng các phương thức Socket

Tạo kết nối socket phía máy chủ

(1) Tạo đối tượng socket: socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6999)

socket.socket(socket.AF_INET,socket.SOCK_STREAM) tạo một socket mới với họ địa chỉ, loại socket và số giao thức đã cho.

family xác định họ địa chỉ:

  • socket.AF_UNIX: chỉ dùng cho giao tiếp giữa các tiến trình Unix đơn
  • socket.AF_INET: giao tiếp mạng giữa các máy chủ (giao thức TCP và UDP ipv4), mặc định là giá trị này
  • socket.AF_INET6: giao tiếp mạng ipv6 giữa các máy chủ

type xác định loại socket:

  • socket.SOCK_STREAM: TCP hướng kết nối, mặc định là giá trị này
  • socket.SOCK_DGRAM: UDP không hướng kết nối
  • socket.SOCK_RAW: socket nguyên thủy, socket thông thường không thể xử lý các gói tin mạng ICMP, IGMP, nhưng socket_RAW có thể; ngoài ra, SOCK_RAW cũng có thể xử lý các gói tin IPV4 đặc biệt; thêm nữa, thông qua tùy chọn socket IP_HDRINCL, người dùng có thể xây dựng header IP.
  • socket.SOCK_RDM là dạng UDP đáng tin cậy, tức là đảm bảo truyền tải gói tin nhưng không đảm bảo thứ tự. SOCK_RDM dùng để cung cấp quyền truy cập cấp thấp đến giao thức nguyên thủy, được sử dụng khi cần thực hiện một số thao tác đặc biệt như gửi gói tin ICMP. Thường thì socket.SOCK_RDM chỉ giới hạn cho người dùng nâng cao hoặc quản trị viên chạy chương trình.

(2) Ràng buộc địa chỉ và cổng cho đối tượng socket

Địa chỉ phải là tuple gồm hai phần tử, bao gồm (host, port) tên máy chủ hoặc địa chỉ IP + số cổng. Nếu cổng hoặc địa chỉ sai sẽ gây ra ngoại lệ socket.error.

(3) Đối tượng socket lắng nghe kết nối cổng địa chỉ

socket.listen(backlog)

backlog xác định số lượng kết nối tối đa, ít nhất là 1, sau khi nhận yêu cầu kết nối, các yêu cầu này phải đợi trong hàng đợi, nếu hàng đợi đầy sẽ từ chối yêu cầu.

(4) socket.accept đối tượng chặn chờ chấp nhận kết nối

fd, addr = self._accept()

Khi gọi phương thức accept, socket sẽ vào trạng thái chặn 'waiting', khi khách hàng yêu cầu kết nối, phương thức sẽ thiết lập kết nối và trả về máy chủ.

Phương thức accept trả về một tuple chứa hai phần tử, (fd,addr). Phần tử đầu tiên là đối tượng socket mới, máy chủ dùng nó để giao tiếp với khách hàng. Phần tử thứ hai là thông tin địa chỉ và cổng của khách hàng.

(5) Giai đoạn xử lý, máy chủ và khách hàng giao tiếp thông qua các phương thức send và recv (truyền dữ liệu)

Gọi đối tượng kết nối mới để giao tiếp với khách hàng hoặc máy chủ:

socket.recv(buffersize): nhận dữ liệu từ khách hàng hoặc máy chủ, buffersize xác định kích thước dữ liệu nhận, đơn vị là byte.

socket.send(data): gửi thông tin cho khách hàng hoặc máy chủ, thông tin phải được chuyển đổi thành byte mới gửi được.

(6) Kết thúc truyền tải, đóng kết nối

socket.close() đóng kết nối

Tạo kết nối socket phía khách hàng:

1) s = socket.socket() tạo đối tượng socket

2) s.connect('127.0.0.1','80') ràng buộc địa chỉ cổng kết nối máy chủ

3) s.send(data) gửi dữ liệu đến máy chủ

4) s.recv(1024) nhận dữ liệu từ máy chủ

5) s.close() đóng kết nối

5. Ví dụ thực tế

Máy khách

# Mã nguồn: Anliu
# Máy khách
import socket
ketnoi_maykhach = socket.socket()  # Khai báo loại socket, đồng thời tạo đối tượng kết nối socket
ketnoi_maykhach.connect(("localhost",6999))
ketnoi_maykhach.send(b"xin chao the gioi")
du_lieu_nhan = ketnoi_maykhach.recv(1024)
print(du_lieu_nhan)
ketnoi_maykhach.close()

Máy chủ

# Mã nguồn: Anliu
import socket
ketnoi_maychu = socket.socket()
ketnoi_maychu.bind(('localhost',6999))  # Ràng buộc cổng lắng nghe
ketnoi_maychu.listen()  # Lắng nghe
print("Bắt đầu đợi cuộc gọi...")

ket_noi, dia_chi = ketnoi_maychu.accept()  # Chờ dữ liệu
# print(ket_noi)  # Kết nối khách hàng, phiên bản kết nối do máy chủ tạo
# print(dia_chi)   # Địa chỉ khách hàng
print("Có cuộc gọi đến...")

du_lieu_nhan = ket_noi.recv(1024)
print("nhan_duoc",du_lieu_nhan)

ket_noi.send(du_lieu_nhan.upper())
ketnoi_maychu.close()

Như vậy đã thực hiện được việc truyền nhận dữ liệu đơn giản một lần.

Lưu ý rằng trong python3 thực hiện truyền dữ liệu socket chỉ hỗ trợ nhị phân.

Vì vậy khi nhập tiếng Trung chúng ta cần mã hóa và giải mã nội dung gửi khi khách hàng gửi và nhận:

# Mã nguồn: Anliu
# Máy khách
import socket
ketnoi_maykhach = socket.socket()   # Khai báo loại socket, đồng thời tạo đối tượng kết nối socket
ketnoi_maykhach.connect(("localhost",6999))

ketnoi_maykhach.send("Tôi là cô gái xinh đẹp aaa...".encode("utf-8"))
du_lieu_nhan = ketnoi_maykhach.recv(1024).decode()

print(du_lieu_nhan)
ketnoi_maykhach.close()

Tiếp theo chúng ta xem cách thực hiện nhận và gửi dữ liệu liên tục.

Chúng ta có thể dùng vòng lặp để thực hiện.

# Mã nguồn: Anliu
# Máy khách
import socket
ketnoi_maykhach = socket.socket()   # Khai báo loại socket, đồng thời tạo đối tượng kết nối socket
ketnoi_maykhach.connect(("localhost",6999))

while True:
    tin_nhan = input(">>".strip())
    ketnoi_maykhach.send(tin_nhan.encode("utf-8"))
    du_lieu_nhan = ketnoi_maykhach.recv(1024).decode()
    print(du_lieu_nhan)

ketnoi_maykhach.close()
# Mã nguồn: Anliu
import socket
ketnoi_maychu = socket.socket()
ketnoi_maychu.bind(('localhost',6999))  # Ràng buộc cổng lắng nghe
ketnoi_maychu.listen()  # Lắng nghe
print("Bắt đầu đợi cuộc gọi...")

ket_noi, dia_chi = ketnoi_maychu.accept()  # Chờ dữ liệu
while True:
    print("Có cuộc gọi đến...")
    du_lieu_nhan = ket_noi.recv(1024)
    print("nhan_duoc",du_lieu_nhan)
    ket_noi.send(du_lieu_nhan.upper())

ketnoi_maychu.close()

Tiếp theo chúng ta thực hiện chức năng ssh kết nối nhận gửi lệnh giữa khách hàng - máy chủ

Máy chủ:

# Mã nguồn: Anliu
# Máy chủ
import socket
import os

ketnoi_maychu = socket.socket()
ketnoi_maychu.bind(('localhost',6999))  # Ràng buộc cổng lắng nghe
ketnoi_maychu.listen(5)  # Lắng nghe
print("Bắt đầu đợi cuộc gọi...")

while True:
    ket_noi, dia_chi = ketnoi_maychu.accept()  # Chờ dữ liệu
    print(ket_noi)
    print(dia_chi)
    print("Có cuộc gọi đến...")
    while True:
        du_lieu_nhan = ket_noi.recv(1024)
        print("nhan_duoc:",type(du_lieu_nhan))
        if not du_lieu_nhan:
            print("máy khách đã mất kết nối ...")
            break
        ket_qua = os.popen(du_lieu_nhan.decode("utf-8")).read()
        ket_noi.send(ket_qua.encode())

Máy khách:

# Mã nguồn: Anliu
# Máy khách
import socket
ketnoi_maykhach = socket.socket()   # Khai báo loại socket, đồng thời tạo đối tượng kết nối socket
# ketnoi_maykhach.connect(("192.168.42.171",6999))
ketnoi_maykhach.connect(("localhost",6999))
while True:
    tin_nhan = input(">>".strip())
    if not tin_nhan:
        print("Nội dung nhập rỗng..")
        break
    ketnoi_maykhach.send(tin_nhan.encode("utf-8"))
    du_lieu_nhan = ketnoi_maykhach.recv(1024).decode()
    print(du_lieu_nhan)

ketnoi_maykhach.close()

Truyền file:

https://github.com/anliu520/python-wheel/tree/master/socket%20--%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93

Thẻ: python socket network-programming tcp-ip client-server

Đăng vào ngày 2 tháng 6 lúc 19:47