Xây Dựng Hệ Thống Trò Chuyện Mạng Bất Đồng Bộ Bằng Python

Khung Xử Lý Mạng Bất Đồng Bộ

Hệ thống này được xây dựng dựa trên hai mô-đun tiêu chuẩn của Python, cung cấp cơ chế xử lý I/O không chặn (non-blocking) cho các ứng dụng mạng TCP/IP.

asyncore

Mô-đun này đóng vai trò là lớp nền tảng, bao bọc trực tiếp các hàm selectsocket của hệ điều hành. Nó duy trì một vòng lặp sự kiện (event loop) để giám sát trạng thái của các kênh kết nối. Khi một socket sẵn sàng đọc hoặc ghi dữ liệu, vòng lặp sẽ kích hoạt các phương thức xử lý tương ứng mà không cần tạo luồng mới cho mỗi kết nối.

  • asyncore.loop(): Chạy vòng lặp giám sát cho đến khi không còn kênh nào hoạt động.
  • asyncore.dispatcher: Lớp cơ sở đại diện cho một socket. Các phương thức như readable(), writable(), handle_read(), handle_write() cho phép tùy chỉnh luồng xử lý sự kiện mạng.
  • handle_accept(): Được gọi khi server nhận được yêu cầu kết nối mới.

asynchat

Mở rộng từ asyncore, mô-đun này được tối ưu hóa cho các giao thức dựa trên bản tin (message-based). Nó tự động quản lý bộ đệm đầu vào/đầu ra và cho phép định nghĩa dấu hiệu kết thúc thông điệp (terminator), giúp việc phân tích chuỗi dữ liệu mạng trở nên trực quan hơn.

  • collect_incoming_data(): Gom các mảnh dữ liệu nhận được vào bộ đệm.
  • found_terminator(): Kích hoạt khi dữ liệu trong bộ đệm chạm đến dấu ngắt đã định.
  • push(): Đẩy dữ liệu vào hàng đợi gửi đi một cách an toàn.

Triển Khai Máy Chủ (Server)

Kiến trúc server được chia thành các lớp chịu trách nhiệm riêng biệt: quản lý socket lắng nghe, duy trì phiên kết nối người dùng, định tuyến lệnh và phân phối thông điệp theo từng trạng thái phòng.

import asyncore
import asynchat

SERVER_PORT = 5555

class SessionTerminated(Exception):
    pass

class NetworkHub(asyncore.dispatcher):
    def __init__(self, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket()
        self.set_reuse_addr()
        self.bind(('0.0.0.0', port))
        self.listen(10)
        self.active_registry = {}
        self.central_space = MainLobby(self)

    def handle_accept(self):
        client_sock, _ = self.accept()
        ClientSession(self, client_sock)

class ClientSession(asynchat.async_chat):
    def __init__(self, hub, sock):
        asynchat.async_chat.__init__(self, sock=sock)
        self.hub = hub
        self.set_terminator(b'\r\n')
        self.buffer = []
        self.nickname = None
        self.switch_zone(AuthZone(self.hub))

    def switch_zone(self, zone):
        if hasattr(self, 'current_zone'):
            self.current_zone.leave(self)
        self.current_zone = zone
        zone.join(self)

    def collect_incoming_data(self, chunk):
        self.buffer.append(chunk.decode('utf-8', errors='ignore'))

    def found_terminator(self):
        raw_msg = ''.join(self.buffer).strip()
        self.buffer.clear()
        if raw_msg:
            try:
                self.current_zone.dispatch_cmd(self, raw_msg.encode('utf-8'))
            except SessionTerminated:
                self.close()

    def handle_close(self):
        super().handle_close()
        self.switch_zone(ExitZone(self.hub))

class RouteEngine:
    def dispatch_cmd(self, session, payload):
        decoded = payload.decode('utf-8')
        parts = decoded.split(maxsplit=1)
        cmd = parts[0]
        arg = parts[1].strip() if len(parts) > 1 else ''
        handler = getattr(self, f'exec_{cmd}', None)
        if handler:
            handler(session, arg)
        else:
            session.push(b'Unknown command.\r\n')

class BaseRoom(RouteEngine):
    def __init__(self, hub):
        self.hub = hub
        self.participants = []

    def join(self, session): self.participants.append(session)
    def leave(self, session):
        try: self.participants.remove(session)
        except ValueError: pass

    def relay(self, data):
        for p in self.participants:
            p.push(data)

    def exec_logout(self, _, __):
        raise SessionTerminated()

class AuthZone(BaseRoom):
    def join(self, session):
        super().join(session)
        session.push(b'Identify yourself. Format: login [name]\r\n')

    def exec_login(self, session, name):
        clean_name = name.strip()
        if not clean_name:
            session.push(b'Name cannot be empty.\r\n')
        elif clean_name in self.hub.active_registry:
            session.push(b'Name already in use.\r\n')
        else:
            session.nickname = clean_name
            self.hub.active_registry[clean_name] = session
            session.switch_zone(self.hub.central_space)

class MainLobby(BaseRoom):
    def join(self, session):
        session.push(b'Entered main hall.\r\n')
        self.relay(f'[{session.nickname}] joined.\r\n'.encode('utf-8'))
        self.hub.active_registry[session.nickname] = session
        super().join(session)

    def leave(self, session):
        super().leave(session)
        self.relay(f'[{session.nickname}] departed.\r\n'.encode('utf-8'))

    def exec_say(self, session, msg):
        formatted = f'[{session.nickname}]: {msg}\r\n'.encode('utf-8')
        self.relay(formatted)

    def exec_who(self, session, _):
        names = '\n'.join(p.nickname for p in self.participants if p.nickname)
        session.push(f'Active users:\n{names}\r\n'.encode('utf-8'))

class ExitZone(BaseRoom):
    def join(self, session):
        if session.nickname:
            self.hub.active_registry.pop(session.nickname, None)

if __name__ == '__main__':
    server = NetworkHub(SERVER_PORT)
    print(f'Listening on 0.0.0.0:{SERVER_PORT}')
    asyncore.loop()

Triển Khai Giao Diện Máy Khách (Client)

Phiên bản client sử dụng tkinter kết hợp với socket chuẩn để đảm bảo tính tương thích và dễ mở rộng. Một luồng phụ được sinh ra để lắng nghe dữ liệu từ server mà không làm đóng băng giao diện chính.

import tkinter as tk
import socket
import threading
import tkinter.messagebox as msgbox

class ConnectionPanel(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Authentication")
        self.geometry("320x200")
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        tk.Label(self, text="Endpoint (host:port):").pack(pady=8)
        self.ep_entry = tk.Entry(self)
        self.ep_entry.insert(0, "localhost:5555")
        self.ep_entry.pack(padx=20)

        tk.Label(self, text="Username:").pack(pady=8)
        self.name_entry = tk.Entry(self)
        self.name_entry.pack(padx=20)

        tk.Button(self, text="Connect", command=self.establish_link).pack(pady=15)

    def establish_link(self):
        try:
            host, port = self.ep_entry.get().split(':')
            self.sock.connect((host.strip(), int(port)))
            welcome = self.sock.recv(1024).decode('utf-8')
            if "Identify" in welcome:
                self.sock.sendall(f"login {self.name_entry.get().strip()}\r\n".encode('utf-8'))
                auth_resp = self.sock.recv(1024).decode('utf-8')
                if "hall" in auth_resp.lower() or "Identify" in auth_resp:
                    self.destroy()
                    ChatRoom(self.sock, self.name_entry.get().strip())
                else:
                    msgbox.showerror("Login Failed", auth_resp)
                    self.sock.close()
            else:
                self.sock.close()
        except Exception as err:
            msgbox.showerror("Connection Error", str(err))

class ChatRoom(tk.Toplevel):
    def __init__(self, conn, user):
        super().__init__()
        self.title(f"Chat Space - {user}")
        self.geometry("500x400")
        self.conn = conn

        self.display = tk.Text(self, state='disabled', wrap='word')
        self.display.pack(expand=True, fill='both', padx=10, pady=10)

        input_area = tk.Frame(self)
        input_area.pack(fill='x', padx=10, pady=10)

        self.msg_box = tk.Entry(input_area)
        self.msg_box.pack(side='left', fill='x', expand=True, padx=5)
        self.msg_box.bind("<Return>", self.transmit_msg)

        tk.Button(input_area, text="Send", command=lambda: self.transmit_msg(None)).pack(side='right', padx=5)
        tk.Button(input_area, text="Users", command=self.request_list).pack(side='right', padx=5)
        tk.Button(input_area, text="Exit", command=self.terminate).pack(side='right', padx=5)

        self.protocol("WM_DELETE_WINDOW", self.terminate)
        threading.Thread(target=self.poll_server, daemon=True).start()

    def transmit_msg(self, _):
        text = self.msg_box.get().strip()
        if text:
            self.conn.sendall(f"say {text}\r\n".encode('utf-8'))
            self.msg_box.delete(0, 'end')

    def request_list(self):
        self.conn.sendall(b"who\r\n")

    def terminate(self):
        try:
            self.conn.sendall(b"logout\r\n")
            self.conn.close()
        except: pass
        self.destroy()

    def poll_server(self):
        accumulator = b""
        try:
            while True:
                data = self.conn.recv(4096)
                if not data: break
                accumulator += data
                while b'\r\n' in accumulator:
                    line, accumulator = accumulator.split(b'\r\n', 1)
                    self.display.config(state='normal')
                    self.display.insert('end', line.decode('utf-8', errors='ignore') + '\n')
                    self.display.see('end')
                    self.display.config(state='disabled')
        except: pass

Phân Tích Chu Trình Vận Hành

Khi máy chủ khởi động, asyncore.dispatcher thiết lập socket lắng nghe và đưa vào vòng lặp giám sát của hệ thống. Ngay khi có kết nối TCP mới, handle_accept() được kích hoạt, tạo ra một thể hiện ClientSession kế thừa từ asynchat.async_chat. Phiên này ngay lập tức được đưa vào trạng thái xác thực (AuthZone) thông qua cơ chế chuyển vùng.

Dữ liệu từ client được asynchat gom vào bộ đệm đệm cho đến khi gặp ký tự ngắt dòng \r\n. Sự kiện found_terminator() sẽ cắt chuỗi, giải mã và chuyển tiếp đến dispatch_cmd(). Cơ chế phản xạ (reflection) getattr() sẽ ánh xạ tiền tố lệnh sang phương thức xử lý tương ứng (ví dụ: exec_say, exec_who). Nếu người dùng đang ở trạng thái xác thực, lệnh hợp lệ sẽ chuyển họ sang MainLobby, cập nhật danh sách người dùng và phát thông báo gia nhập. Ngược lại, mọi thông điệp chat hoặc truy vấn danh sách sẽ được relay() phát tán đồng loạt tới tất cả các phiên đang tồn tại trong danh sách participants. Khi client đóng kết nối hoặc gửi lệnh thoát, handle_close() dọn dẹp tài nguyên và xóa bản ghi khỏi registry toàn cục.

Thẻ: python asyncore asynchat socket-programming tcp-server

Đăng vào ngày 19 tháng 9 lúc 16:06