我只是把代码作为一个没有经验的爱好,一年前我发现数字战斗模拟器(DCS,一种军事飞行模拟)有一个强大的 lua 脚本界面,它使我们能够将游戏玩法引入一个普通的模拟器。很明显,我们需要一个网页来将信息传递给玩家,因为模拟器不允许大量的 GUI(除了在右上角放置文本)。
所以,起初我在同一台机器上运行多人服务器和网络服务器。将 lua-tables 解析为 php 以将它们转换为 SQL。我认为由于 DCS-Lua 引擎也支持 TCP,因此最好使用 TCP 连接传输数据。
前言结束,这是我希望找到帮助的地方!
我让 TCP 连接正常工作,所有数据似乎都可以正常传输,但是php TCPServer 会每 12-24 小时冻结一次。几年前我几乎没有 php 经验,只是挖掘了套接字以使网络服务器与游戏服务器脱离同一台机器。该代码基本上可以正常工作,并且可以执行应有的操作,但是在某个随机点,php-server 会停止响应。
几个星期以来,我一直在努力寻找原因并且有点放弃,直到我认为这可能是我寻找有经验的人可以帮助我确定错误的最后手段。它特别难以调试,因为它每天只发生一次或两次。我希望对 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")