PHP TCP-Server到LUA TCP-Client - 不规则冻结

时间:2017-09-10 15:35:23

标签: sockets tcp tcpsocket luasocket php-socket

一年前我发现数字战斗模拟器(DCS,军事飞行模拟器)有一个强大的lua脚本界面,使我们能够将游戏玩法引入普通的模拟器中。很明显,我们需要一个网页来向玩家传递信息,因为模拟器不允许大量的GUI(除了在右上角放置文本)。

所以,起初我在同一台机器上运行多人游戏服务器和网络服务器。将lua-tables解析为php以使其进入SQL。我想,既然DCS-Lua引擎也支持TCP,最好使用TCP连接传输数据。

前言的结尾,这是我希望找到帮助的地方!

我的TCP连接工作正常,所有数据似乎转移得很好,但 php TCPServer会每12-24小时冻结。我从几年前就有很少的PHP经验,只有挖掘插座才能将网络服务器与游戏服务器放在同一台机器上。代码基本上工作正常,并做它应该的,但在一个随机点的PHP服务器将停止响应。

我一直试图找到原因几周并且有点放弃,直到我认为这可能是我找到经验丰富的人可以帮助我确定错误的最后手段。它特别难以调试,因为它每天只发生一次或两次。我希望那些对tcp套接字有更深入了解的人可能会在一瞥中看到它并说:当然它会冻结并解释原因! ○:)

所以这是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();


?>

这是在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 个答案:

没有答案