Một Ví Dụ Đơn Giản Về Skynet
Như đã đề cập trước đó, skynet chỉ là một khung nhẹ (lightweight framework), không phải là một engine sẵn sàng sử dụng ngay lập tức. Việc sử dụng hiệu quả hay không phụ thuộc vào việc người dùng có hiểu rõ mục tiêu của mình hay không. Nếu bạn định dùng skynet để xây dựng server game online, điều kiện tiên quyết là phải nắm rõ nguyên tắc thiết kế server game online.
Trong thư mục example của bản phát hành skynet, có các dịch vụ như gate, watchdog, agent… Tuy nhiên đây không phải là mô hình duy nhất để xây dựng game server trên skynet. Tôi muốn trình bày một ví dụ khác để minh họa cách xây dựng game server với skynet nhưng theo mô hình khác.
Tôi đã dành hai ngày để viết một sample như vậy và đưa lên github. Trong ví dụ này, tôi muốn thể hiện các điểm chính sau:
1. Đa dạng hóa quản lý kết nối
GateServer không phải là mô hình duy nhất để quản lý kết nối. Trong skynet, người dùng hoàn toàn có thể tùy biến các phương pháp khác để xử lý lượng lớn kết nối từ bên ngoài. Ví dụ này sử dụng một module khác mà tôi đã phát triển gần đây, module này không nằm trong bản phát hành chính thức của skynet.
2. Mô hình hub service linh hoạt
Ví dụ triển khai một dịch vụ hub tương tự chức năng của gate. Tuy nhiên, dịch vụ này chỉ đơn thuần lắng nghe cổng kết nối và chuyển các kết nối mới đến dịch vụ xử lý phù hợp. Theo luồng trong ví dụ, mỗi kết nối mới sẽ được chuyển trực tiếp đến auth service. Chỉ khi auth service xác nhận kết nối hợp lệ, kết nối đó mới được chuyển tiếp đến manager service.
3. Mở rộng dịch vụ theo nhu cầu
Cả auth và manager đều là dịch vụ đơn lẻ trong ví dụ. Trong thực tế, nếu gặp vấn đề hiệu năng, auth service có thể được mở rộng thành nhiều instance để cân bằng tải. Trong trường hợp cần thiết, có thể thêm một dịch vụ xếp hàng để quản lý luồng kết nối.
4. Quản lý agent thông minh
Khi manager nhận được danh tính kết nối, nó sẽ phân công agent service xử lý yêu cầu dựa trên danh tính đó. Điều quan trọng cần lưu ý: Mã nguồn trong ví dụ KHÔNG đơn giản khởi tạo một agent mới cho mỗi kết nối được xác thực. Đây là một hiểu lầm phổ biến của nhiều người mới tiếp cận skynet - nghĩ rằng skynet luôn tạo một máy ảo Lua độc lập cho mỗi kết nối.
Nguyên tắc quản lý agent của manager như sau:
- Nếu chưa tồn tại agent cho người dùng cụ thể, sẽ khởi tạo mới
- Ngay cả khi người dùng ngắt kết nối, agent không nhất thiết phải dừng ngay lập tức
- Việc dừng agent do chính agent quyết định
- Manager chỉ đảm nhiệm vai trò liên kết người dùng với agent đang hoạt động
Liên kết này dựa trên danh tính người dùng chứ không phải kết nối vật lý. Điều này cho phép nhiều kết nối đồng thời được xác thực và liên kết với cùng một agent (ví dụ: nhiều thiết bị cùng đăng nhập một tài khoản).
5. Tiềm năng mở rộng manager
Hiện tại manager được triển khai khá đơn giản, nhưng chỉ cần cải tiến nhẹ có thể hỗ trợ liên kết nhiều người dùng với cùng một agent. Ví dụ: Trong server game bài, có thể thiết kế để các người chơi cùng một bàn được xử lý bởi cùng một agent.
6. Xử lý logic nghiệp vụ qua agent
Agent service đảm nhiệm xử lý logic nghiệp vụ. Trong ví dụ hiện tại, agent chỉ xử lý được yêu cầu login và ping. Chúng tôi phân biệt rõ ràng giữa signin và login:
- Signin: Người dùng đã qua xác thực nhưng chưa thể thực hiện yêu cầu nghiệp vụ
- Login: Người dùng được agent chấp nhận
Trong ví dụ, khi một người dùng login thành công, họ không thể login lại cho đến khi ngắt kết nối hiện tại. Tuy nhiên, bạn có thể điều chỉnh để cho phép:
- Phiên login mới thay thế phiên cũ
- Cho phép nhiều phiên login đồng thời
7. Cách tiếp cận dịch vụ mới
Ví dụ cung cấp một cách đóng gói (encapsulation) khác biệt so với snax. Nó minh họa cách không sử dụng mô hình dịch vụ có tên (named service) truyền thống của skynet, thay vào đó dùng skynet.uniqueservice
để thay thế.
Trong lớp đóng gói này, bạn chỉ cần khai báo tên các dịch vụ phụ thuộc là có thể khởi động chúng theo thứ tự chính xác. Lớp này đơn giản hóa các bước phức tạp như skynet.dispatch
, skynet.info_func
… khi viết service skynet. Cách hoạt động của nó có thể dễ hiểu hơn snax và cũng dễ sử dụng hơn.
8. Cải tiến client-side
Phía client sử dụng thư viện lsocket mã nguồn mở thay vì module clientsocket đơn giản trong bản skynet. Điều này giúp minh họa rõ hơn cách xây dựng client skynet chuyên nghiệp.
Mã xử lý giao thức sproto trong client cũng được tổ chức rõ ràng hơn với một lớp đóng gói giúp tăng tính đọc hiểu so với ví dụ gốc của skynet. Phần tương tác giữa server và client cũng có module hỗ trợ tương ứng.
9. Triết lý thiết kế client
Tôi đề cao mô hình request/response trong client, không hỗ trợ server push dữ liệu. Nếu cần push, có thể dùng kỹ thuật long polling.
Khác với server, client phải xử lý đồng thời:
- Tương tác với giao diện người dùng
- Render hình ảnh
- Quản lý yêu cầu mạng
Vì vậy, việc áp dụng nguyên lý RPC của server lên client là không phù hợp. Trong ví dụ, tôi không sử dụng coroutine cho gọi RPC.
10. Mô hình callback tối ưu
Callback có vẻ phù hợp hơn với client-side, nhưng không phải theo kiểu rpc_call(request, cb)
. Thay vào đó, phương pháp xử lý phản hồi của từng yêu cầu được đăng ký trong một bảng.
Ví dụ: Với yêu cầu ping, client định nghĩa trước:
|
|
Khi cần ping server (thường do thao tác UI của người dùng), chỉ cần:
|
|
Khi nhận được phản hồi ping từ server, hàm ping trên sẽ được gọi lại với các tham số req (dữ liệu yêu cầu ban đầu), resp (phản hồi từ server), và session.
Nếu ping là yêu cầu vô trạng thái (stateless), session có thể bỏ qua. Hàm callback có thể truy cập dữ liệu req ban đầu nên không cần phụ thuộc vào trạng thái bên ngoài.
11. Xử lý luồng nghiệp vụ phức tạp
Một số quy trình nghiệp vụ yêu cầu nhiều lần trao đổi với server. Với các tương tác có ngữ cảnh (context), có thể cân nhắc dùng coroutine để đóng gói các gọi RPC này. Tuy nhiên, trong thực tế, các tương tác đa bước chủ yếu xuất hiện trong quy trình đăng nhập/xác thực, nên chưa cần thiết kế quá phức tạp cho trường hợp này.