[0.1.1-4] server.port defined. also, heartbeat server starting to take shape, though you can't actually READ the list from another comp yet.

This commit is contained in:
Ben Russell (300178622) 2013-07-31 13:19:26 +12:00
parent 44766398ff
commit a8119ad35a
9 changed files with 461 additions and 48 deletions

View File

@ -55,6 +55,11 @@ common.base_dir @
writing to this string will *not* change the base dir,
so *don't write to it* or else you'll just screw up your own code!
server.port @
port we are bound to
there is NO alias to common here! if you want it, do it manually.
common.version = {cmp={w,x,y,a,z},num,str="w.x.ya-z"} @
current engine version

View File

@ -4,74 +4,74 @@ Main heartbeat server:
play.iceballga.me port 27790
(port is for UDP *and* TCP)
Iceball clients should connect to:
play.iceballga.me port 27795
[ NOT IMPLEMENTED ]
Iceball only supports IPv4 UDP ports at the time of writing, sorry.
Feel free to host your own heartbeat server, though!
Note, S = heartbeat server, C = heartbeat client (AKA iceball server).
You should always confirm you have received a packet from the heartbeat server.
To do this correctly, send packets every 3 seconds until you get a response.
If you get no response after 60 seconds, give up.
You might want to retry after 180 seconds.
The heartbeat server will terminate your session after 120 seconds of inactivity.
ALL PROTOCOLS ARE LITTLE ENDIAN WHERE APPROPRIATE, GUYS!
Iceball servers interface the heartbeat server using this UDP protocol...
0x01 hbversion.u16 port.u16 addr.z:
C->S Request ID (conf 0x02)
This should be the first thing you send to the heartbeat server.
"1CEB" hbversion.u16 ibversion.u32 port.u16 players_current.u16 players_max.u16 name.z[30] mode.z[10] map.z[30]
I->H
Announce a server.
At the time of writing, hbversion should be 1.
Note, remember to send a burst of 5 of these
with a second in between each packet,
and do this every 40 seconds
0x02 id.u32 port.u16 addr.z:
S->C Here Is Your ID
(that is, a gap of 36 seconds between the last of a burst
and the first of the next).
Every time you receive this packet, CHANGE YOUR ID.
Note, the strings are terminated with a NUL (char 0)
unless they hit their string limit,
in which case there is no NUL. BE WARY OF THIS.
0x03 id.u32 field.u8 name.z:
C->S Set String Field (conf 0x03)
S->C This Is Your String Field
"MSOK"
H->I
Valid string fields are:
0x01: server name
0x02: server mode
Your packet was accepted.
0x04 id.u32 field.u8 data.s32:
C->S Set Integer Field (conf 0x04)
S->C This Is Your Integer Field
If your server gets this message, you can stop sending your burst.
Valid integer fields are:
0x01: player count
0x02: max players
"BADV" hbversion.u16 ibversion.u32
H->I
0x05 id.u32:
C->S Hello Me Not Dead
Your version of Iceball and/or the heartbeat protocol are too old and/or new.
Here's the version we expect.
Every 30 seconds, send 5 of these at 1 second intervals.
If your server gets this message, give up.
0x06:
S->C I'll Need To See Your ID
Alternatively, if you support the given heartbeat version, try falling back to it.
If you send any packets and you don't have an active session
(either you didn't initiate one or the server terminated your session),
you'll get this. Apply for a new ID.
"BADF"
H->I
0x07 version.u16:
S->C Version Mismatch
The packet you sent was complete horseshit and you should feel bad for writing such bad code.
You are using the wrong version of the heartbeat protocol.
This should give you the correct version.
If your server gets this message, crash in shame, because your code is seriously broken.
Or alternatively just don't send any more data to the server.
But crashing with an error is a much better idea.
OK, so that's the UDP side of the master server.
The TCP side is a minimal HTTP server with two interfaces:
The TCP side is a minimal HTTP server at the same port with these interfaces:
[ INCOMPLETE IMPLEMENTATION THAT DOESN'T ACTUALLY WORK ]
/:
/index.html:
An HTML page following a template.
/style.css:
A stylesheet you can provide.
/master.json:
The raw serverlist info in JSON format.
Here's the expected format!
@ -80,12 +80,14 @@ The TCP side is a minimal HTTP server with two interfaces:
"version": int,
"servers": [
{
"name": str,
"address": str,
"port": int,
"basedir": str,
"players_current": int,
"players_max": int,
"name": str,
"mode": str,
"map": str,
"version": str
}
]
}

View File

@ -16,6 +16,249 @@
# along with Iceball. If not, see <http://www.gnu.org/licenses/>.
#
import sys
import socket
import heapq, socket, struct, sys, time
CONN_PORT = int(sys.argv[1])
def ib_version_str(n):
z = n & ((1<<10)-1); n >>= 10
a = n & ((1<<5)-1); n >>= 5
y = n & ((1<<7)-1); n >>= 7
x = n & ((1<<5)-1); n >>= 5
w = n
s = "%i.%i" % (w, x)
if y > 0:
s += ".%i" % y
if a > 0:
s += "." + chr(ord('a')+a-1)
if z > 0:
s += "-%i" % z
return s
def calc_ib_version(w,x,y,a,z):
return (((((((w<<5) + x
)<<7) + y
)<<5) + a
)<<10) + z
HB_LIFETIME = 120
HB_VERSION = 1
IB_VERSION_CMP = (0,1,1,0,0)
IB_VERSION = calc_ib_version(*IB_VERSION_CMP)
# ignore "Z" version
IB_VERSION_MASK = ~((1<<10)-1)
# if you wish to ignore "A" version as well, use this instead:
# IB_VERSION_MASK = ~((1<<(10+5))-1)
def stripnul(s):
idx = s.find("\x00")
return (s if idx == -1 else s[:idx])
class HTTPClient:
def __init__(self, reactor, server, sockfd):
self.reactor = reactor
self.server = server
self.sockfd = sockfd
self.buf = ""
def is_dead(self, ct):
return self.sockfd == None
def update(self, ct):
self.get_msgs(ct)
def collect(self, ct):
# TODO!
pass
def get_msgs(self, ct):
if not self.sockfd:
return
try:
msg = self.sockfd.recv(2048)
if msg == None:
self.sockfd.close()
self.sockfd = None
self.buf += msg
self.collect(ct)
except socket.timeout:
pass
class HReactor:
def __init__(self):
self.evq = []
def run(self):
while len(self.evq) > 0:
ct = self.get_time()
dt = self.evq[0][0] - ct
if dt > 0:
time.sleep(dt)
self.update()
# we've run out of tasks, so exit.
def update(self):
ct = self.get_time()
while self.evq and ct >= self.evq[0][0]:
et, fn = heapq.heappop(self.evq)
fn(et, ct)
def get_time(self):
return time.time()
def push(self, et, fn):
heapq.heappush(self.evq, (et, fn))
class HServer:
def __init__(self, ct, reactor, af, port):
self.af = af
self.port = port
self.reactor = reactor
self.clients = {}
self.http_clients = {}
self.bans = set([]) # TODO: use a proper banlist
self.http_sockfd = socket.socket(af, socket.SOCK_STREAM)
self.http_sockfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.http_sockfd.bind(("", self.port))
self.http_sockfd.settimeout(0)
self.sockfd = socket.socket(af, socket.SOCK_DGRAM)
self.sockfd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sockfd.bind(("", self.port))
self.sockfd.settimeout(0.05)
self.reactor.push(ct, self.update)
def update(self, et, ct):
self.get_hb_packets(ct)
self.update_http_clients(ct)
self.kill_old_clients(ct)
self.reactor.push(ct+0.05, self.update)
def get_hb_packets(self, ct):
try:
(msg, adtup) = self.sockfd.recvfrom(2048)
if msg and adtup:
(addr, port) = adtup
self.on_msg(ct, self.af, addr, port, msg)
except socket.timeout:
pass
def update_http_clients(self, ct):
for v in self.http_clients:
v.update(ct)
def kill_old_clients(self, ct):
# TODO: use a priority queue for the clients
# that'd mean we'd get this running in O(1) time instead of O(n)
kill = []
for k,v in self.clients.iteritems():
if v.is_dead(ct):
kill.append(k)
for k in kill:
self.clients.pop(k)
def on_msg(self, ct, af, addr, port, msg):
client = self.get_client(ct, af, addr, port)
if client:
client.on_msg(ct, msg)
def get_client(self, ct, af, addr, port):
tup = (af, addr, port)
if tup in self.bans:
return None
if tup not in self.clients:
self.clients[tup] = HClient(ct, self.reactor, self, af, addr, port)
return self.clients[tup]
def get_ib_fields(self):
l = []
for k, v in self.clients.iteritems():
d = v.get_fields()
if d:
l.append(d)
return l
class HClient:
def __init__(self, ct, reactor, server, af, addr, port):
self.reactor = reactor
self.server = server
self.af = af
self.addr = addr
self.port = port
self.ibdata = None
self.not_dead(ct)
def get_fields(self):
return self.ibdata
def is_dead(self, ct):
return ct > self.last_msg + HB_LIFETIME
def not_dead(self, ct):
self.last_msg = ct
def send_delayed(self, t, msg):
self.reactor.push(lambda et, ct : self.send(msg))
def send(self, msg):
self.server.sockfd.sendto(msg, (self.addr, self.port))
def on_msg(self, ct, msg):
if len(msg) < 4:
# empty message - do nothing
return
typ = msg[:4]
msg = msg[4:]
if typ == "1CEB" and len(msg) >= 6:
hbver, ibver = struct.unpack("<HI", msg[:6])
msg = msg[6:]
if hbver != HB_VERSION or ((ibver^IB_VERSION)&IB_VERSION_MASK) != 0:
# version is incorrect
self.send("BADV" + struct.pack("<HI", HB_VERSION, IB_VERSION))
elif len(msg) != (2+2+2+30+10+30):
# bad format
print "BADF", len(msg)
self.send("BADF")
else:
# parse this!
d = {}
d["address"] = str(self.addr)
d["port"], d["players_current"], d["players_max"] = struct.unpack("<HHH", msg[:6])
msg = msg[6:]
d["name"] = stripnul(msg[:30])
msg = msg[30:]
d["mode"] = stripnul(msg[:10])
msg = msg[10:]
d["map"] = stripnul(msg[:30])
d["version"] = ib_version_str(ibver)
self.ibdata = d
self.not_dead(ct)
self.send("MSOK")
print "MSOK", d
hb_reactor = HReactor()
hb_server = HServer(hb_reactor.get_time(), hb_reactor, socket.AF_INET, CONN_PORT)
hb_reactor.run()

View File

@ -19,7 +19,7 @@
#define VERSION_X 1
#define VERSION_Y 1
#define VERSION_A 0
#define VERSION_Z 3
#define VERSION_Z 4
// Remember to bump "Z" basically every time you change the engine!
// Remember to bump the version in Lua too!
// Remember to document API changes in a new version!

View File

@ -137,6 +137,6 @@ do
common.map_set(lmap)
print("gen finished")
return ret
return ret, "classic("..mw..","..mh..")"
end

View File

@ -36,6 +36,6 @@ do
end
end
print("gen finished")
return ret
return ret, "flat("..mx..","..mz..","..my..")"
end

131
pkg/base/lib_heartbeat.lua Normal file
View File

@ -0,0 +1,131 @@
--[[
This file is part of Ice Lua Components.
Ice Lua Components is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Ice Lua Components is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with Ice Lua Components. If not, see <http://www.gnu.org/licenses/>.
]]
HB_VERSION = 1
heartbeat_sockfd = nil
heartbeat_t_nextmsg = nil
heartbeat_t_nextburst = nil
heartbeat_burstsleft = nil
heartbeat_cooloff = true
local function pad_nul(n, s)
while s:len() < n do
s = s .. "\0"
end
if s:len() > n then
s = s:sub(1, n)
end
return s
end
function heartbeat_init()
-- check if we have this actually enabled
if not server_config.heartbeat_send then return end
-- open the socket
heartbeat_sockfd = common.udp_open()
end
function heartbeat_update(sec_current, sec_delta)
-- we need to let the timer "cool off" as the first few values are just plain wrong.
if heartbeat_cooloff then
if sec_current < 60 and sec_current >= 2 then
heartbeat_cooloff = nil
end
return
end
-- if we're using the wrong heartbeat and/or iceball version,
-- heartbeat_sockfd will be nil,
-- because we have given up.
-- (it'll also be nil if we haven't enabled the heartbeat client.)
if not heartbeat_sockfd then return end
-- versions before 0.1.1-4 don't have server.port,
-- so we need to rip the port from server.hook_connect.
if not server.port then return end
-- check if we received any messages
while true do
local msg, host, port = common.udp_recvfrom(heartbeat_sockfd)
if msg == "" then
break
elseif msg == false then
error("UDP socket used to connect to master servers broke horribly. What the hell?!")
elseif msg == "MSOK" then
-- we're ignoring MSOK messages for now,
-- until we can track ALL master/heartbeat servers properly
elseif msg == "BADF" then
error("heartbeat server \""..host.."\" port "..port.." reports bad packet format - FIX ME OR REMOVE THIS SERVER")
elseif msg:len() >= 4 and msg:sub(1,4) == "BADV" then
error("heartbeat server \""..host.."\" port "..port.." reports bad version - UPGRADE OR REMOVE THIS SERVER")
end
end
-- check if we need to send a new burst
heartbeat_t_nextburst = heartbeat_t_nextburst or sec_current
if sec_current >= heartbeat_t_nextburst then
heartbeat_t_burstsleft = 5
heartbeat_t_nextmsg = heartbeat_t_nextburst
heartbeat_t_nextburst = heartbeat_t_nextburst + 40
end
-- check if we need to send a new message
if heartbeat_t_burstsleft and heartbeat_t_nextmsg and sec_current >= heartbeat_t_nextmsg then
-- get player count
local players_max = players.max
local players_current = 0
local i
for i=1,players_max do
if players[i] then
players_current = players_current + 1
end
end
-- assemble message
local msg = "1CEB" .. common.net_pack("HI", HB_VERSION, common.version.num)
msg = msg .. common.net_pack("HHH", server.port, players_current, players_max)
msg = msg .. pad_nul(30, server_config.name)
msg = msg .. pad_nul(10, game_hb_mode)
msg = msg .. pad_nul(30, map_name)
--print("HEARTBEAT MESSAGE")
-- send message
local i
local hbl = server_config.heartbeat
for i=1,#hbl do
local host, port = hbl[i][1], hbl[i][2]
common.udp_sendto(heartbeat_sockfd, msg, host, port)
end
-- give time for next message if necessary
heartbeat_t_burstsleft = heartbeat_t_burstsleft - 1
if heartbeat_t_burstsleft <= 0 then
heartbeat_t_burstsleft = nil
heartbeat_t_nextmsg = nil
else
heartbeat_t_nextmsg = heartbeat_t_nextmsg + 1
end
end
end

View File

@ -66,6 +66,7 @@ load_mod_list(getfenv(), mod_data.mods, {"preload", "preload_server"}, server_co
dofile("pkg/base/common.lua")
dofile("pkg/base/commands.lua")
dofile("pkg/base/lib_heartbeat.lua")
client_list = {fdlist={}, banned={}}
server_tick_accum = 0
@ -161,6 +162,9 @@ function server.hook_connect(neth, addrinfo)
addrinfo.addr and addrinfo.addr.ip,
addrinfo.addr and addrinfo.addr.cport)
-- workaround for pre-0.1.1-4 versions
server.port = server.port or (addrinfo.addr and addrinfo.addr.sport)
local source = false
if addrinfo.proto == "enet/ip6" or addrinfo.proto == "tcp/ip6" then
-- There are two variants:
@ -228,6 +232,8 @@ end
lflush = nil
function server.hook_tick(sec_current, sec_delta)
heartbeat_update(sec_current, sec_delta)
--print("tick",sec_current,sec_delta)
--[[
local xlen,ylen,zlen
@ -416,17 +422,38 @@ end
-- load map
if server_settings.gen then
map_loaded = loadfile(server_settings.gen)(loose, server_toggles, server_settings)
map_loaded, map_name = loadfile(server_settings.gen)(loose, server_toggles, server_settings)
elseif map_fname then
map_loaded = common.map_load(map_fname, "auto")
map_name = map_fname
while map_name do
local p = map_name:find("/", 1, true)
if not p then break end
map_name = map_name:sub(p+1)
end
else
map_loaded = loadfile("pkg/base/gen_classic.lua")(loose, server_toggles, server_settings)
map_loaded, map_name = loadfile("pkg/base/gen_classic.lua")(loose, server_toggles, server_settings)
end
if not map_name then
map_name = "<?>"
end
game_hb_mode = game_mode_file
while true do
local p = game_hb_mode:find("/", 1, true)
if not p then break end
game_hb_mode = game_hb_mode:sub(p+1)
end
common.map_set(map_loaded)
mode_create_server()
print("pkg/base/main_server.lua: Loading mods...")
load_mod_list(getfenv(), mod_data.mods, {"load", "load_server"}, server_config, mod_data)
print("Starting heartbeat server...")
heartbeat_init()
print("pkg/base/main_server.lua loaded.")

View File

@ -583,6 +583,11 @@ int icelua_init(void)
icelua_pushversion(lstate_server, "common");
icelua_pushversion(lstate_server, "server");
lua_getglobal(lstate_server, "server");
lua_pushinteger(lstate_server, net_port);
lua_setfield(lstate_server, -2, "port");
lua_pop(lstate_server, 1);
}
if(lstate_client != NULL)