I only code as a hobby with little experience, a year ago I found that the Digital Combat Simulator (DCS, a military flight-sim) had a powerfull lua scripting interface which enabled us to introduce gameplay into a plain simulator. It became evident that we needed a webpage to relay information to the players, as the simulator does not allow a lot of GUI (apart from placing text in the top-right corner).
So, at first I was running the Multiplayer-server and the webserver on the same machine. Parsing lua-tables into php to get them into SQL. I figured since the DCS-Lua-engine also supports TCP it would be best to transfer the data using a TCP Connection.
end of preface, this is where I hope to find help!
I got the TCP connection working and all the data seems to transfer fine, however the php TCPServer would freeze every 12-24hours. I have little php experience from years ago and only dug into sockets to get the webserver off the same machine as the game server. The code basically works fine and does what its supposed to, but at one random point the php-server would stop responding.
I've been trying to find the reason for weeks and have kind of given up, until I figured this may be my last resort to find somebody experienced who can help me pin down the error. It is particularly hard to debug, since it only occurs once or twice a day. My hope would be that somebody with deeper knowledge of tcp sockets might see it in one glimpse and say: of course it would freeze and explain why! O:)
So here is the code of the PHP-Server:
<?php
error_reporting(E_ALL);
include('source\Helper.php');
class koTCPServer {
private $address = '0.0.0.0'; // 0.0.0.0 means all available interfaces
//private $address = '127.0.0.1';
private $port = 52525; // the TCP port that should be used
private $maxClients = 10;
private $helper;
private $clients;
private $socket;
private $receivedScoreIDs;
private $dataBuffer = "";
private $numPacketsReceived = 0;
public function __construct() {
// Set time limit to indefinite execution
$this->log("constructing Socket");
set_time_limit(0);
error_reporting(E_ALL ^ E_NOTICE);
$this->helper = new TAW_Source_Helper();
$this->receivedScoreIDs = array();
}
public function start() {
$this->log("Starting Socket ...");
// Create a TCP Stream socket
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// Bind the socket to an address/port
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($this->socket, $this->address, $this->port);
// Start listening for connections
socket_listen($this->socket, $this->maxClients);
$this->clients = array('0' => array('socket' => $this->socket));
$this->log("socket started, looking for connection ...");
while (true) {
// Setup clients listen socket for reading
$read = array();
$read[0] = $this->socket;
for($i=1; $i < $this->maxClients+1; $i++) {
if($this->clients[$i] != NULL) {
$read[$i+1] = $this->clients[$i]['socket'];
}
}
$write = NULL;
//$write = $read;
$except = NULL;
// Set up a blocking call to socket_select()
$ready = socket_select($read, $write, $except, $tv_sec = NULL);
if (false === §ready) {
echo "socket_select() failed, reason: " . socket_strerror(socket_last_error()) . "\n";
}
/* if a new connection is being made add it to the client array */
if(in_array($this->socket, $read)) {
$this->message("new connection");
$newSocket = socket_accept($this->socket);
socket_getpeername($newSocket, $ip);
for($i=1; $i < $this->maxClients+1; $i++) {
if(!isset($this->clients[$i])) {
$this->clients[$i]['socket'] = $newSocket;
$this->clients[$i]['ipaddress'] = $ip;
$this->clients[$i]['txbuf'] = '';
$this->clients[$i]['rxbuf'] = '';
$this->log("New client #$i connected, ip: " . $this->clients[$i]['ipaddress'] . " maxclients = " . $this->maxClients);
break;
} elseif($this->clients[$i]['ipaddress'] == $ip) {
$this->clients[$i]['socket'] = $newSocket;
$this->log("client #".$i.", ip: ".$ip." reconnected");
$oldRXBuf = $this->clients[$i]['rxbuf'];
$this->log("- old rxbuf: '$oldRXBuf'");
break;
} elseif($i == $this->maxClients) {
$this->message('Too many Clients connected!');
}
if($ready < 1) {
continue;
}
}
}
// If a client is trying to write - handle it now
for($i=1; $i < $this->maxClients+1; $i++) {
// if theres something to read from the client
if(in_array($this->clients[$i]['socket'], $read)) {
$data = @socket_read($this->clients[$i]['socket'], 1024000, PHP_NORMAL_READ);
if($data == null) {
$this->log('Client #'.$i.', '.$this->clients[$i]['ipaddress'].' disconnected!');
//update server status
if($this->clients[$i]['status'] != "restarting")
$this->helper->setServerStatus($this->clients[$i]['serverID'], "disconnected");
unset($this->clients[$i]);
continue;
}
// Data received!
if(!empty($data)) {
$this->numPacketsReceived++;
$this->message($this->numPacketsReceived.": We have data!: '".strval($data)."'");
// check if data is complete
$dataBuffer =& $this->clients[$i]['rxbuf'];
$dataBuffer .= $data;
$endIdx = strpos($dataBuffer, "\n");
while($endIdx !== FALSE) {
//$this->message(" - complete data received - ");
// handling data here
// .
// .
// .
// remove json string from $dataBuffer
$dataBuffer = substr($dataBuffer, $endIdx+1, strlen($dataBuffer));
$endIdx = strpos($dataBuffer, PHP_EOL);
}
}
} // end if client read
// now send stuff
if(isset($this->clients[$i])) {
// if theres something so send to the client, send it
if(strlen($this->clients[$i]['txbuf']) > 0) {
//$this->message("sending to '".$this->clients[$i]['ipaddress']."' ...");
$length = strlen($this->clients[$i]['txbuf']);
$result = socket_write($this->clients[$i]['socket'], $this->clients[$i]['txbuf'], $length);
if($result === false) {
//$this->message("send failed");
} else {
$this->clients[$i]['txbuf'] = substr($this->clients[$i]['txbuf'], $result);
}
}
}
} // end for clients
} // end while
}
}
$TCPServer = new koTCPServer();
$TCPServer->start();
?>
This is how the connection is established in lua:
local require = require
local loadfile = loadfile
package.path = package.path..";.\\LuaSocket\\?.lua"
package.cpath = package.cpath..";.\\LuaSocket\\?.dll"
local JSON = loadfile("Scripts\\JSON.lua")()
local socket = require("socket")
koTCPSocket = {}
--koTCPSocket.host = "localhost"
koTCPSocket.host = "91.133.95.88"
koTCPSocket.port = 52525
koTCPSocket.JSON = JSON
koTCPSocket.serverName = "unknown"
koTCPSocket.txbufRaw = '{"type":"intro","serverName":"notstartedyet"}\n'
koTCPSocket.txbuf = ''
koTCPSocket.rxbuf = ''
koTCPSocket.txTableBuf = {} -- holds all runtime type of tables (savegame, radiolist, playerlist) which are NOT unique
koTCPSocket.txScoreBuf = {} -- holds all scores which are unique and need to be transmitted securely. Once transmitted, server will return the scoreID to be removed from the table, if server does not return the id, send it again!
koTCPSocket.txScoreDelayBuf = {} -- delay scores by 5 seconds before they are sent again!
koTCPSocket.bufferFileName = lfs.writedir() .. "Missions\\The Kaukasus Offensive\\ko_TCPBuffer.lua"
function koTCPSocket.startConnection()
koEngine.debugText("koTCPSocket.startconnection()")
-- start connection
koTCPSocket.connection = socket.tcp()
koTCPSocket.connection:settimeout(.0001)
koTCPSocket.connection:connect(koTCPSocket.host, koTCPSocket.port)
-- looping functions
mist.scheduleFunction(koTCPSocket.transmit, nil, timer.getTime(), 0.01)
mist.scheduleFunction(koTCPSocket.receive, nil, timer.getTime()+0.5, 0.01)
end
koTCPSocket.inTransit = false -- 1 if we need to delete the entry from the buffer after it was sent, 0 if there is no object in the buffer to delete
function koTCPSocket.transmit()
-- if scorebuffer is empty, check if we have sent scores that need to be sent again
if #koTCPSocket.txScoreBuf == 0 then
for i, msg in ipairs(koTCPSocket.txScoreDelayBuf) do
if (timer.getTime() - msg.sendTime) > 60 then -- if message is older than 60 seconds, and still hasn't been confirmed by webserver, send it again!
env.info("koTCPSocket.transmit(): found score in delay buffer that is older than 60 seconds ... sending again!")
msg.sendTime = nil
table.insert(koTCPSocket.txScoreBuf, msg)
table.remove(koTCPSocket.txScoreDelayBuf, i)
end
end
end
-- refresh score buffers
-- we have an object cued (#txTableBuf > 0) and we did not just finish a transmission (not inTransit)
if koTCPSocket.txbuf:len() == 0 and #koTCPSocket.txScoreBuf > 0 and not koTCPSocket.inTransit then
koTCPSocket.txbuf = koTCPSocket.txbuf..koTCPSocket.JSON:encode(koTCPSocket.txScoreBuf[1]).."\n" -- cue the next transmission
-- now move the score back in the buffer, its going to be deleted
local tmp = koTCPSocket.txScoreBuf[1]
tmp.sendTime = timer.getTime()
table.remove(koTCPSocket.txScoreBuf,1)
table.insert(koTCPSocket.txScoreDelayBuf, tmp)
elseif koTCPSocket.txbuf:len() == 0 and #koTCPSocket.txTableBuf > 0 and not koTCPSocket.inTransit then
koTCPSocket.txbuf = koTCPSocket.txbuf..koTCPSocket.JSON:encode(koTCPSocket.txTableBuf[1]).."\n" -- cue the next transmission
koTCPSocket.inTransit = true -- we started a new transmission
-- we have just finished a transmission (inTransit = true) and there is one more object in the txTableBuf (>1)
elseif koTCPSocket.txbuf:len() == 0 and #koTCPSocket.txTableBuf > 1 and koTCPSocket.inTransit then
table.remove(koTCPSocket.txTableBuf,1) -- remove the just transmitted object and cue the next transmission
koTCPSocket.txbuf = koTCPSocket.txbuf..koTCPSocket.JSON:encode(koTCPSocket.txTableBuf[1]).."\n"
-- we have just finished a transmission (inTransit = true) and there is no more object in the txTableBuf (==0)
elseif koTCPSocket.txbuf:len() == 0 and #koTCPSocket.txTableBuf == 1 and koTCPSocket.inTransit then
table.remove(koTCPSocket.txTableBuf,1) -- remove the just transmitted object
koTCPSocket.inTransit = false -- no more transmissions
end
-- handle actual transmission
if koTCPSocket.txbuf:len() > 0 then
--koEngine.debugText("koTCPSocket.transmit() - buffer available, sending ...")
local bytes_sent = nil
local ret1, ret2, ret3 = koTCPSocket.connection:send(koTCPSocket.txbuf)
if ret1 then
--koEngine.debugText(" - Transmission complete!")
bytes_sent = ret1
else
koEngine.debugText("could not send koTCPSocket: "..ret2)
if ret3 == 0 then
if ret2 == "closed" then
if MissionData then
koTCPSocket.txbuf = koTCPSocket.txbuf..koTCPSocket.txbufRaw..'\n'
else
koTCPSocket.txbuf = koTCPSocket.txbuf..'{"type":"dummy"}\n'
end
koTCPSocket.rxbuf = ""
koTCPSocket.connection = socket.tcp()
koTCPSocket.connection:settimeout(.0001)
koEngine.debugText("koTCPSocket: socket was closed")
end
--koEngine.debugText("reconnecting to "..tostring(koTCPSocket.host)..":"..tostring(koTCPSocket.port))
koTCPSocket.connection:connect(koTCPSocket.host, koTCPSocket.port)
return
end
bytes_sent = ret3
koEngine.debugText("bytes sent: "..tostring(bytes_sent))
koEngine.debugText(" - sent string: '"..koTCPSocket.txbuf:sub(1, bytes_sent).."'")
end
koTCPSocket.txbuf = koTCPSocket.txbuf:sub(bytes_sent + 1)
end
end
function koTCPSocket.receive()
--env.info("koTCPSocket.receive()")
local line, err, partRes = koTCPSocket.connection:receive('*l')
if partRes and partRes:len() > 0 then
--env.info("koTCPSocket.receive(), partRes = '"..tostring(partRes).."'")
koTCPSocket.rxbuf = koTCPSocket.rxbuf .. partRes
env.info("koTCPSocket.receive() - partRes = '"..partRes.."'")
local line = koTCPSocket.rxbuf:sub(1, koTCPSocket.rxbuf:find("\\n")-1)
koTCPSocket.rxbuf = koTCPSocket.rxbuf:sub(koTCPSocket.rxbuf:find("\\n")+2, -1)
while line:len() > 0 do
local msg = JSON:decode(line)
--env.info("koTCPSocket.receive(): msg = "..koEngine.TableSerialization(msg))
if msg.type == "alive" then
env.info("koTCPSocket.receive() - Alive packet received and returned")
koTCPSocket.txbuf = koTCPSocket.txbuf .. '{"type":"alive","serverName":"'..koTCPSocket.serverName..'"}\n'
elseif msg.type == "scoreReceived" then
env.info("koTCPSocket.receive() - scoreID '"..msg.scoreID.."' received, checking buffer!")
--env.info("txScoreBuf = "..koEngine.TableSerialization(txScoreBuf))
for i, scoreTable in ipairs(koTCPSocket.txScoreBuf) do
for ucid, score in pairs(scoreTable.data) do
--env.info("score = "..koEngine.TableSerialization(score))
--env.info("comparing "..score.scoreID.." with "..msg.scoreID)
if tonumber(score.scoreID) == tonumber(msg.scoreID) then
table.remove(koTCPSocket.txScoreBuf, i)
env.info("- found score in table, removed index "..i)
end
end
end
for i, scoreTable in ipairs(koTCPSocket.txScoreDelayBuf) do
for ucid, score in pairs(scoreTable.data) do
if tonumber(score.scoreID) == tonumber(msg.scoreID) then
table.remove(koTCPSocket.txScoreDelayBuf, i)
env.info("- found score in delay-table, removed index "..i)
end
end
end
end
if koTCPSocket.rxbuf:len() > 0 and koTCPSocket.rxbuf:find("\\n") then
line = koTCPSocket.rxbuf:sub(1, koTCPSocket.rxbuf:find("\\n")-1)
koTCPSocket.rxbuf = koTCPSocket.rxbuf:sub(koTCPSocket.rxbuf:find("\\n")+2, -1)
env.info("koTCPSocket.receive() - rxbuf in loop = '"..koTCPSocket.rxbuf.."'")
else
line = ""
end
end
end
end
function koTCPSocket.close(reason)
koTCPSocket.send({reason = "reason"},"shutdown")
koTCPSocket.connection:close()
end
env.info("koTCPSocket loaded")