Ghi Chú Phát Triển (4): Vòng Lặp Tin Nhắn Agent Và RPC - nói dối e blog

Ghi Chú Phát Triển (4): Vòng Lặp Tin Nhắn Agent Và RPC

Tiếp nối từ Ghi chú phát triển 1. Chúng ta sẽ cung cấp một dịch vụ agent riêng biệt cho từng người chơi kết nối đến. Agent này sẽ xử lý các gói dữ liệu do người chơi gửi tới và phản hồi lại kết quả xử lý.

Kiến trúc cơ bản của Agent

Dịch vụ agent là ví dụ điển hình của mô hình xử lý gói dữ liệu (packet-driven). Dù sử dụng framework Erlang hay xây dựng từ đầu bằng ZeroMQ, logic cốt lõi của agent không thay đổi nhiều. Cơ chế hoạt động dựa trên việc:

  1. Nhận đầu vào thông qua một cổng duy nhất do framework cung cấp.
  2. Phân tích loại gói tin, sau đó kích hoạt quy trình xử lý tương ứng.

Khi không có gói tin nào được gửi đến, agent sẽ ở trạng thái treo (suspended) thay vì liên tục kiểm tra đầu vào (polling). Điều này giúp tối ưu việc sử dụng CPU. Trách nhiệm quản lý hiệu suất thuộc về framework, vấn đề này sẽ được thảo luận trong tài liệu riêng.

Xử lý logic phức tạp trong game

So với các ứng dụng web truyền thống, logic game có độ phức tạp cao hơn nhiều. Các yêu cầu từ bên ngoài không thể coi là độc lập lẫn nhau. Nếu thiết kế theo kiểu REST hoặc dựa trên cơ sở dữ liệu session đơn giản, hệ thống sẽ không thể đáp ứng được yêu cầu đa dạng của trò chơi. Lý do:

  • Hầu hết gói tin đều liên quan mật thiết đến bối cảnh (context) trước đó.
  • Các gói tin có thể thuộc về nhiều quy trình xử lý đồng thời hoặc tuần tự.

Để giải quyết vấn đề này, chúng ta cần chia logic game thành các quy trình (process) với các đặc tính sau:

  1. Quy trình tuần tự:

    • Chỉ xử lý một loại gói tin duy nhất tại một thời điểm.
    • Khi nhận được gói tin đúng loại, hệ thống gọi hàm xử lý và quay lại lắng nghe gói tin tiếp theo.
    • Ví dụ: Quy trình đăng nhập → Xác thực tên người dùng → Kiểm tra mật khẩu.
  2. Quy trình con (sub-process):

    • Được khởi tạo trong quá trình xử lý một gói tin.
    • Cho đến khi quy trình con hoàn tất, agent sẽ không nhận các gói tin thuộc quy trình cha.
    • Ví dụ: Trong khi di chuyển trong bản đồ, người chơi kích hoạt quy trình giao dịch.
  3. Quy trình song song (parallel process):

    • Có thể chạy đồng thời với các quy trình khác.
    • Framework tự động phân loại gói tin và định tuyến đến quy trình phù hợp.
    • Ví dụ: Trong khi giao dịch, người chơi vẫn có thể tham gia chiến đấu hoặc chat.

Ngắt và tiếp tục quy trình

Mỗi quy trình có thể bị ngắt (break) giữa chừng, chuyển sang xử lý logic khác. Cơ chế này áp dụng cho cả quy trình tuần tự và song song. Khi một quy trình hoàn tất, hệ thống tiếp tục thực thi đoạn code phía sau điểm ngắt.

RPC như một quy trình song song đơn giản

Remote Procedure Call (RPC) có thể hiểu là một quy trình song song chờ phản hồi duy nhất. Khi nhận được phản hồi, hệ thống tiếp tục xử lý quy trình gốc.

Mô tả logic bằng DSL

Tôi đề xuất thiết kế một ngôn ngữ đặc tả (DSL) để biểu diễn logic game. Ví dụ mẫu dưới đây minh họa cách định nghĩa các quy trình:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...1  
listen :  
  case message A :  
    ...2  
    listen :  
      case message B:  
        ...3  
        break  
      case message C:  
        ...4  
      case message D:  
        ...5  
    ...6  
  case message E:  
    ...7  
    break  
...8  
listen :  
  case message F:  
    fork listen :  
      case message G:  
        ...9  
      case message H:  
        ...10  
        break  
    ...11  
  case message I:  
    ...12  

Giải thích:

  • Hệ thống bắt đầu từ ...1, lắng nghe tin nhắn A hoặc E.
  • Nếu nhận E: Thực thi ...7, thoát khỏi vòng lặp hiện tại và tiếp tục từ ...8.
  • Nếu nhận A: Chuyển sang trạng thái chờ B/C/D, thực thi logic phù hợp.
  • Khi nhận B: Kết thúc quy trình A, quay lại trạng thái ban đầu.
  • Quy trình F sử dụng fork để khởi tạo quy trình song song, đồng thời lắng nghe G/H và F/I.

Triển khai bằng Lua

Để hiện thực hóa DSL này, tôi chọn Lua nhờ hỗ trợ coroutine mạnh mẽ. Cơ chế chính của framework là trình lập lịch coroutine (scheduler):

  • Mỗi quy trình (case message) là một coroutine riêng biệt.
  • Khi gặp lệnh listen, fork, hoặc break, coroutine sẽ yield để trả quyền điều khiển cho scheduler.
  • Scheduler cập nhật bảng ánh xạ giữa loại tin nhắn và coroutine, quyết định coroutine nào được resume tiếp theo.

Quản lý ngữ cảnh và RPC

Hai thách thức chính:

  1. Tính nhìn thấy (visibility) của dữ liệu giữa các khối code.
  2. Thiết kế giao diện RPC thân thiện với người dùng.

Dưới đây là đoạn code mẫu triển khai nguyên mẫu (khoảng 100 dòng):

1
2
3
--- agent.lua  
local setmetatable = setmetatable  
local coroutine
0%