Xử Lý Phân Tách Gói Tin TCP Trong Skynet
Lõi của skynet không quy định cụ thể cách xử lý luồng dữ liệu TCP. Tuy nhiên trong phát triển game mạng, chúng ta thường tuân theo quy tắc chia luồng dữ liệu thành từng gói tin riêng biệt thông qua kết nối TCP. Phương pháp phổ biến để chuyển đổi luồng dữ liệu thành các gói tin riêng lẻ là thêm thông tin độ dài gói vào luồng dữ liệu. Tôi đặc biệt khuyến khích việc sử dụng 2 byte để biểu diễn độ dài gói. Skynet cung cấp một khuôn mẫu GateServer để hỗ trợ triển khai cổng kết nối này.
Khuôn mẫu cổng kết nối này áp dụng mô hình đẩy dữ liệu (push model). Sau khi khởi tạo, nó tự động phân chia luồng dữ liệu theo giao thức (2 byte độ dài + nội dung gói tin), sau đó gọi hàm callback xử lý. Bạn có thể khởi động một dịch vụ độc lập riêng biệt xử lý kết nối mới trong hàm callback, hoặc xử lý trực tiếp tại dịch vụ chính. Những vấn đề này sẽ không được bàn luận chi tiết trong bài viết này.
Hôm nay tôi muốn giới thiệu một mô hình xử lý phân tách gói tin khác, có thể phù hợp hơn với nhu cầu tùy biến mô-đun của từng game cụ thể so với việc sử dụng trực tiếp khuôn mẫu GateServer. Trong skynet, việc khởi tạo và duy trì một dịch vụ C để xử lý kết nối là hành động rất nhẹ nhàng. Dù dịch vụ Lua về bản chất cũng là dịch vụ C, nhưng nó phải khởi tạo thêm máy ảo Lua VM nên chi phí cao hơn. Nếu cấu trúc dữ liệu của dịch vụ C đơn giản, thì cả về mặt bộ nhớ lẫn trình lập lịch của skynet đều không tạo gánh nặng đáng kể.
Chúng ta có thể khởi tạo một dịch vụ C riêng biệt quản lý mỗi kết nối TCP. Dịch vụ này chịu trách nhiệm đọc 2 byte tiêu đề gói tin, tách riêng các gói dữ liệu từ luồng vào. Đối với luồng ra, dịch vụ sẽ thêm độ dài gói vào dữ liệu nội bộ trước khi đóng gói. Nếu muốn, bạn thậm chí có thể tích hợp chức năng mã hóa/giải mã giao tiếp tại đây.
Khác với mô hình đẩy dữ liệu của GateServer, chúng ta có thể áp dụng mô hình yêu cầu-phản hồi (request-response). Lớp ứng dụng (thường là agent) sẽ gửi yêu cầu lấy gói tin mới đến dịch vụ proxy kết nối (được viết bằng C). Khi gói tin chưa được nhận đủ hoặc không có dữ liệu mới, dịch vụ proxy sẽ tạm treo yêu cầu. Sau mỗi lần tách thành công một gói tin từ kết nối, dịch vụ sẽ lấy một yêu cầu từ hàng đợi chưa xử lý ra và phản hồi.
Với cơ chế proxy kết nối này, API sử dụng trở nên đơn giản và trực quan hơn nhiều. Bạn chỉ cần liên tục yêu cầu gói tin mới, và khi kết nối bên ngoài bị ngắt sẽ ném ra ngoại lệ. Trong trường hợp bạn chưa sẵn sàng xử lý dữ liệu, dịch vụ proxy sẽ tự động lưu trữ (buffer) các gói tin chưa xử lý. Dịch vụ này cũng có thể tự quản lý cơ chế timeout.
Mô hình yêu cầu-phản hồi rất tiện lợi khi dữ liệu từ một kết nối cần được xử lý luân phiên bởi nhiều dịch vụ nội bộ khác nhau. Ví dụ: ban đầu bạn có thể giao cho dịch vụ xác thực xử lý kết nối mới để kiểm tra danh tính người dùng; sau khi xác thực thành công, mới chuyển kết nối sang cho agent xử lý tiếp.
Nếu lo ngại về độ trễ của mô hình yêu cầu-phản hồi khi xử lý luồng dữ liệu đầu vào tần suất cao, hoàn toàn có thể áp dụng cơ chế pipeline (gửi liên tiếp nhiều yêu cầu mà không chờ phản hồi từng cái). Mã nguồn triển khai nằm tại kho lưu trữ độc lập được tách riêng khỏi kho chính của skynet.
Bạn có thể tùy biến và sử dụng thư viện này làm nền tảng. Thư viện này gồm 3 thành phần chính:
-
skynet_package.c
- Biên dịch thànhpackage.so
, đây là dịch vụ proxy kết nối thuần C. Mỗi dịch vụ package chỉ quản lý duy nhất một kết nối TCP. Khi khởi động, bạn phải truyền tham số fd (file descriptor) để khởi tạo, và chỉ dịch vụ quản lý mới được phép khởi tạo nó. -
service/socket_proxyd.lua
- Dịch vụ quản lý trung tâm, chịu trách nhiệm điều phối tất cả các dịch vụ package. Vì package là dịch vụ C nên không thể quản lý qua launcher, cũng không hiển thị trong debug console. Dịch vụ quản lý này đồng thời đảm nhiệm chức năng giám sát trạng thái tất cả package. Bạn có thể gửi lệnh “info” từ debug console để xem thông tin. -
lualib/socket_proxy.lua
- Thư viện tiện ích bao bọc (wrapper), giúp tránh việc gửi trực tiếp tin nhắn đến dịch vụ quản lý hoặc package proxy. Thư viện cung cấp 4 API chính:- subscribe: Đăng ký một fd vào dịch vụ quản lý
- read: Đọc một gói tin từ fd đã đăng ký
- write: Gửi đi một gói tin
- close: Đóng kết nối fd bắt buộc
Lưu ý: Khác với gate, thư viện này không đảm nhiệm chức năng lắng nghe (listen) cổng kết nối. Bạn phải tự thực hiện listen và đăng ký fd từ kết nối accept vào dịch vụ quản lý. Ví dụ cụ thể xem trong file test/main.lua
.
Thư mục test cũng chứa file client.lua
- một chương trình kiểm thử đơn giản. Nó đọc chuỗi ký tự từ giao diện điều khiển, thêm tiêu đề 2 byte vào trước khi gửi đi; đồng thời có thể nhận và phân tách các gói dữ liệu phản hồi từ server theo đúng giao thức.
Chương trình kiểm thử này triển khai một dịch vụ echo đơn giản: mọi gói tin nhận được sẽ được phản hồi ngược trở lại. Ngay khi nhận thấy gói tin chứa lệnh “quit”, kết nối sẽ tự động bị ngắt.
(Thông tin bổ sung: Ví dụ này giả định bạn đang dùng Linux và đã tải về, biên dịch skynet tại thư mục $HOME/skynet
. Nếu môi trường bạn khác biệt, hãy chỉnh sửa file Makefile phù hợp).