Một trong những câu hỏi thường gặp nhất từ cộng đồng người dùng Skynet chính là: “Tại sao khi tôi thay đổi thời gian hệ thống nhưng Skynet lại không phản hồi?” Khi tìm hiểu kỹ hơn, hóa ra phần lớn người dùng có nhu cầu này đều muốn xây dựng một hệ thống lịch trình tự động kích hoạt nhiệm vụ tại thời điểm cụ thể. Việc điều chỉnh thời gian hệ thống thường xuất phát từ nhu cầu kiểm thử.
Phải thừa nhận rằng thay đổi thời gian hệ thống để kiểm tra là một phương pháp trực quan nhưng cực kỳ thiếu an toàn. Các timer của Skynet hoàn toàn không phụ thuộc vào thời gian hệ thống, do đó việc chỉnh sửa thời gian máy tính là hành động vô ích.
Yêu cầu về dịch vụ lịch trình là nhu cầu phổ biến trong các game online. Nếu không có các nhiệm vụ theo mùa vụ hay副本 (câu hỏi này xuất hiện lỗi, cần sửa lại thành “chế độ thử thách theo tuần”) thì gần như không thể vận hành game. Bài viết này sẽ thảo luận cách triển khai dịch vụ lịch trình hiệu quả trong Skynet.
Phương pháp tối ưu là xây dựng một dịch vụ độc lập chuyên trách quản lý lịch trình. Như vậy, khi cần kiểm thử, bạn có thể thông qua các giao diện dự phòng để dịch vụ này giả lập việc đẩy nhanh thời gian, từ đó kích hoạt nhanh chóng các nhiệm vụ định thời.
Thông thường, các nhiệm vụ lịch trình không đòi hỏi độ chính xác cao đến từng giây, chỉ cần chính xác đến phút là đủ. Chúng ta ít khi thiết lập thời gian kích hoạt chính xác đến từng giây. Các thiết lập lịch trình thường liên quan đến ngày tháng cụ thể hoặc thứ trong tuần. Ví dụ điển hình như: “Mỗi thứ Bảy hàng tuần vào lúc 20:00”, “12:00 trưa thứ Năm tuần thứ hai của mỗi tháng”, “Sự kiện ngày Quốc tế Thiếu nhi 1/6”, v.v.
Để thiết kế dịch vụ lịch trình như vậy, thực tế chỉ cần một giao diện đăng ký sự kiện đơn giản, tương tự như cơ chế định thời một lần của skynet.timer. Giao diện kích hoạt sẽ nhận vào một mốc thời gian mô tả như các ví dụ trên. Trong dịch vụ sự kiện cụ thể, chúng ta sử dụng skynet.call tới dịch vụ lịch trình, và khi skynet.call trả về chính là lúc sự kiện được kích hoạt. Chẳng hạn, để cố định mở một sự kiện vào mỗi thứ Bảy lúc 20:00, ta có thể dùng vòng lặp while như sau:
1
2
3
4
|
while true do
skynet.call(schedule_service, "lua", { wday = 7 , hour = 20 } )
-- Thực hiện hoạt động
end
|
Dịch vụ lịch trình mỗi khi nhận yêu cầu đăng ký, sẽ tính toán mốc thời gian kích hoạt tiếp theo dựa trên tham số truyền vào, sau đó sử dụng skynet.sleep chờ đến thời điểm đó. Nếu cần điều chỉnh thời gian giả lập để kiểm thử, chỉ cần gửi thông báo tới dịch vụ và hiệu chỉnh độ lệch giữa thời gian giả lập và thời gian thật, đánh thức tất cả các yêu cầu đang chờ và tính toán lại thời gian chờ.
Dịch vụ lịch trình như vậy có độ phức tạp vừa phải. Tôi đã dành chút thời gian viết một bản triển khai mẫu, mời các bạn tham khảo. Bản này còn nhiều điểm có thể cải tiến, nhưng hy vọng sẽ giúp bạn hiểu rõ nguyên lý thiết kế:
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
local skynet = require "skynet"
local service = require "skynet.service"
local schedule = {}
local service_addr
-- { month=, day=, wday=, hour= , min= }
function schedule.submit(ti)
return skynet.call(service_addr, "lua", ti)
end
function schedule.changetime(ti)
local tmp = {}
for k,v in pairs(ti) do
tmp[k] = v
end
tmp.changetime = true
return skynet.call(service_addr, "lua", tmp)
end
skynet.init(function()
local schedule_service = function()
-- schedule service
local skynet = require "skynet"
local task = { session = 0, difftime = 0 }
local function next_time(now, ti)
local nt = {
year = now.year ,
month = now.month ,
day = now.day,
hour = ti.hour or 0,
min = ti.min or 0,
sec = ti.sec,
}
if ti.wday then
-- set week
assert(ti.day == nil and ti.month == nil)
nt.day = nt.day + ti.wday - now.wday
local t = os.time(nt)
if t < now.time then
nt.day = nt.day + 7
end
else
-- set day, no week day
if ti.day then
nt.day = ti.day
end
if ti.month then
nt.month = ti.month
end
local t = os.time(nt)
if t < now.time then
if ti.month then
nt.year = nt.year + 1 -- next year
else
nt.month = nt.month + 1 -- next month
end
end
end
return os.time(nt)
end
local function changetime(ti)
local ct = math.floor(skynet.time())
local current = os.date("*t", ct)
current.time = ct
if not ti.hour then
ti.hour = current.hour
end
if not ti.min then
ti.min = current.min
end
ti.sec = current.sec
local nt = next_time(current, ti)
skynet.error(string.format("Đổi thời gian thành %s", os.date(nil, nt)))
task.difftime = os.difftime(nt,ct)
for k,v in pairs(task) do
if type(v) == "table" then
skynet.wakeup(v.co)
end
end
skynet.ret()
end
local function submit(_, addr, ti)
if ti.changetime then
return changetime(ti)
end
local session = task.session + 1
task.session = session
repeat
local ct = math.floor(skynet.time()) + task.difftime
local current = os.date("*t", ct)
current.time = ct
local nt = next_time(current, ti)
task[session] = { time = nt, co = coroutine.running(), address = addr }
local diff = os.difftime(nt , ct)
print("sleep", diff)
until skynet.sleep(diff * 100) ~= "BREAK"
task[session] = nil
skynet.ret()
end
skynet.start(function()
skynet.dispatch("lua", submit)
skynet.info_func(function()
local info = {}
for k, v in pairs(task) do
if type(v) == "table" then
table.insert( info, {
time = os.date(nil, v.time),
address = skynet.address(v.address),
})
end
return info
end
end)
end)
-- end of schedule service
end
service_addr = service.new("schedule", schedule_service)
end)
return schedule
|
Bạn có thể dùng schedule.submit để đăng ký một mốc thời gian, khi đến thời điểm sẽ tự động trả về. Khi sử dụng, chỉ cần khởi động một