PHP TCP-Server to LUA TCP-Client - irregular freezes

266 views Asked by At

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")
0

There are 0 answers