Khung Truyền Thông Mạng Cho Client MMORPG - nói dối e blog

Khung Truyền Thông Mạng Cho Client MMORPG

Trong một cuộc trò chuyện ngẫu hứng hôm qua, chúng tôi đã thảo luận về mô hình truyền thông mạng nên áp dụng cho client MMORPG. Dù đã từng viết về chủ đề này từ lâu, nhưng dường như bài viết đã thất lạc somewhere. Nhân dịp này, tôi xin trình bày lại quan điểm của mình một cách hệ thống.

Theo tôi, trong các game MMO, server đóng vai trò là thế giới ảo trung tâm - nơi mọi trạng thái và sự thay đổi đều được xác định và cập nhật bởi hệ thống. Client, ngược lại, bản chất là một “trình hiển thị trạng thái” - có nhiệm vụ phản ánh chính xác những gì người chơi nhìn thấy trong thế giới ảo thông qua các gói tin mạng. Chính vì vậy, khi thiết kế hệ thống xử lý tin nhắn mạng cho client, chúng ta cần lấy vai trò này làm trung tâm.

Các yêu cầu từ client có thể chia thành hai dạng chính:

  1. Thông báo cho server về sự thay đổi trạng thái của nhân vật do người chơi điều khiển, yêu cầu server xác nhận và cập nhật
  2. Yêu cầu cập nhật trạng thái mới nhất của các đối tượng trong game

Dạng thứ hai có thể được xử lý theo hai cách: server chủ động đẩy dữ liệu (push) hoặc client chủ động yêu cầu (pull). Sự khác biệt chủ yếu nằm ở cách kiểm soát lưu lượng mạng.

Khi nhìn nhận client như một “trình hiển thị trạng thái”, mối quan hệ giữa request và response không còn mang tính ràng buộc chặt chẽ như trong mô hình RPC (Remote Procedure Call). Dù RPC có thể được triển khai bằng callback hay coroutine, mô hình này vẫn không phù hợp vì nó tạo ra sự phụ thuộc mạnh giữa quá trình gửi yêu cầu và xử lý phản hồi.

Vấn đề cốt lõi nằm ở chỗ: Khi trạng thái server thay đổi, client cần quan tâm là “làm thế nào để hiển thị sự thay đổi đó”, chứ không phải “ứng phó với phản hồi của yêu cầu cụ thể nào đó”. RPC khiến logic xử lý phản hồi bị lệ thuộc vào ngữ cảnh lúc gửi yêu cầu, dẫn đến việc quản lý trạng thái trở nên phức tạp và dễ sinh lỗi.

Một ví dụ điển hình là bug từng xảy ra trong dự án của công ty tôi: Khi người dùng nhấn nút trên giao diện UI, hệ thống dùng callback để thực hiện RPC. Hàm xử lý phản hồi sau đó tiến hành cập nhật các thành phần giao diện. Tuy nhiên, khi phản hồi đến, cửa sổ UI đã bị đóng, và do cơ chế giải phóng bộ nhớ, một số đối tượng liên quan đã bị huỷ. Kết quả là chương trình cố gắng thao tác trên các đối tượng không còn tồn tại, gây ra lỗi nghiêm trọng.

Dù có thể nhìn nhận vấn đề này từ góc độ khác (như xây dựng framework UI chắc chắn hơn), nhưng theo tôi, nguyên nhân gốc rễ vẫn là sự ràng buộc quá mức giữa request và response.

Chúng ta nên tiếp cận sự kiện nhấn nút như sau: Đó chỉ là hành động kích hoạt một sự kiện trên server, dẫn đến thay đổi trạng thái của một đối tượng nào đó. Gói phản hồi đơn thuần chỉ là thông báo về kết quả thay đổi đó. Nhiệm vụ thực sự của client là làm thế nào để hiển thị chính xác trạng thái mới này.

Nếu áp dụng cách nhìn này cho toàn bộ quá trình xử lý gói tin mạng, chúng ta có thể xây dựng một framework đơn giản và nhất quán. Khi nhận được gói tin (dù là push từ server hay response cho request trước đó), client chỉ cần xác định loại gói tin, sau đó kích hoạt quy trình xử lý tương ứng gắn với loại đó, chứ không phải gắn với từng request cụ thể.

Về bản chất, xử lý response và push từ server không khác nhau nhiều. Sự khác biệt chỉ nằm ở chi tiết triển khai. Với response, quy trình xử lý cần có thêm thông tin về request gốc (tham số yêu cầu ban đầu) và đối tượng client liên quan.

Để thực hiện điều này, chúng ta thường dùng một session ID để liên kết request và response. Khi gửi yêu cầu, không chỉ gửi nội dung đến server mà còn lưu trữ thông tin request này kèm session ID. Khi nhận được phản hồi, hệ thống có thể dùng session ID để truy xuất lại thông tin yêu cầu ban đầu, từ đó xác định cách xử lý phù hợp. Điều này cho phép server chỉ cần gửi sự thay đổi trạng thái thay vì toàn bộ dữ liệu, client tự tìm được nội dung cần cập nhật.

Trong thực tế, chúng ta còn nên gắn mỗi session với một đối tượng local trên client. Dù request gốc đã chứa thông tin cần thiết (nếu không, server sẽ không biết cần xử lý đối tượng nào), việc có thêm đối tượng local giúp thao tác thuận tiện hơn. Đối tượng này có thể lưu trữ một số trạng thái local nhỏ, và khi nhận response, hệ thống có thể trực tiếp thao tác trên đối tượng này.

Nếu dùng Lua để triển khai, có thể xây dựng như sau:

1
2
3
4
5
6
7
8
9
-- Gửi yêu cầu với đối tượng liên kết
send_request(obj, req_type, req_data)

-- Hàm xử lý gói tin loại req_type
function net.req_type(obj, req_data, resp_data)
    -- Xử lý dựa trên obj (đối tượng local), 
    -- req_data (tham số yêu cầu gốc) 
    -- và resp_data (dữ liệu phản hồi từ server)
end

Mô hình này giúp tách biệt rõ ràng giữa logic mạng và logic giao diện, đồng thời giảm thiểu rủi ro từ các callback chồng chéo. Khi thiết kế hệ thống mạng cho game MMO, việc nhận thức rõ vai trò “trình hiển thị trạng thái” của client sẽ giúp xây dựng kiến trúc ổn định và dễ bảo trì hơn.

0%