简体   繁体   中英

PHP Socket Server Memory Leak

I have checked the memory whilst sending and receiving data over one connection, and I appear to be correctly clearing variables, as the memory returns to its previous value.

But for some reason if I make a new connection, then close the connection, memory is leaked. I believe the problem may be occurring when a socket is accepted.

I am using PHP 5.2.10

Hopefully one of you can find the time to have a play with the source and figure out where its gone wrong. Thanks in advance

<?php
    Class SuperSocket
        {
            var $listen = array();
            var $status_listening = FALSE;
            var $sockets = array();
            var $event_callbacks = array();
            var $recvq = 1;
            var $parent;
            var $delay = 100; // 10,000th  of a second
            var $data_buffer = array();

            function SuperSocket($listen = array('127.0.0.1:123'))
                {
                    $listen = array_unique($listen);
                    foreach ($listen as $address)
                        {
                            list($address, $port) = explode(":", $address, 2);
                            $this->listen[] = array("ADDR" => trim($address), "PORT" => trim($port));
                        };
                }

            function start()
                {
                    if ($this->status_listening)
                        {
                            return FALSE;
                        };
                    $this->sockets = array();
                    $cursocket = 0;
                    foreach ($this->listen as $listen)
                        {
                            if ($listen['ADDR'] == "*")
                                {
                                    $this->sockets[$cursocket]['socket'] = socket_create_listen($listen['PORT']);
                                    $listen['ADDR'] = FALSE;
                                }
                            else
                                {
                                    $this->sockets[$cursocket]['socket'] = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
                                };
                            if ($this->sockets[$cursocket]['socket'] < 0)
                                {
                                    return FALSE;
                                };
                            if (@socket_bind($this->sockets[$cursocket]['socket'], $listen['ADDR'], $listen['PORT']) < 0)
                                {
                                    return FALSE;
                                };
                            if (socket_listen($this->sockets[$cursocket]['socket']) < 0)
                                {
                                    return FALSE;
                                };
                            if (!socket_set_option($this->sockets[$cursocket]['socket'], SOL_SOCKET, SO_REUSEADDR, 1))
                                {
                                    return FALSE;
                                };
                            if (!socket_set_nonblock($this->sockets[$cursocket]['socket']))
                                {
                                    return FALSE;
                                };
                            $this->sockets[$cursocket]['info'] = array("ADDR" => $listen['ADDR'], "PORT" => $listen['PORT']);
                            $this->sockets[$cursocket]['channels'] = array();
                            $this->sockets[$cursocket]['id'] = $cursocket;
                            $cursocket++;
                        };
                    $this->status_listening = TRUE;
                }

            function new_socket_loop(&$socket)
                {
                    $socket =& $this->sockets[$socket['id']];
                    if ($newchannel =  @stream_socket_accept($socket['socket'], 0));//@socket_accept($socket['socket']))
                        {
                            socket_set_nonblock($newchannel);
                            $socket['channels'][]['socket'] = $newchannel;
                            $channel = array_pop(array_keys($socket['channels']));
                            $this->remote_address($newchannel, $remote_addr, $remote_port);
                            $socket['channels'][$channel]['info'] = array('ADDR' => $remote_addr, 'PORT' => $remote_port);
                            $event = $this->event("NEW_SOCKET_CHANNEL");
                            if ($event)
                            $event($socket['id'], $channel, $this);
                        };
                }

    function endswith($string, $test) {
        $strlen = strlen($string);
        $testlen = strlen($test);
        if ($testlen > $strlen) return false;
        return substr_compare($string, $test, -$testlen) === 0;
    }

            function recv_socket_loop(&$socket)
                {
                    $socket =& $this->sockets[$socket['id']];
                    foreach ($socket['channels'] as $channel_id => $channel)
                        {
                            unset($buffer);#Flush buffer
                            $status = @socket_recv($channel['socket'], $buffer, $this->recvq, 0);
                            if ($status === 0 && $buffer === NULL)
                                {
                                    $this->close($socket['id'], $channel_id);
                                }
                            elseif (!($status === FALSE && $buffer === NULL))
                                {
                                    $sockid = $socket['id'];
                                    if(!isset($this->data_buffer[$sockid]))
                                        $this->data_buffer[$sockid]='';

                                    if($buffer!="\r"&&$buffer!="\n")
                                    {
                                        //Putty ends with \r\n
                                        $this->data_buffer[$sockid].=$buffer;
                                    }
                                    else if($buffer!="\n") //ignore the additional newline char \n
                                    {
                                        $event = $this->event("DATA_SOCKET_CHANNEL");
                                        if ($event)
                                            $event($socket['id'], $channel_id, $this->data_buffer[$sockid], $this);
                                        unset($this->data_buffer[$sockid]);
                                    }

                                };
                        }
                }

            function stop()
                {
                    $this->closeall();
                    $this->status_listening = FALSE;
                    foreach ($this->sockets as $socket_id => $socket)
                        {
                            socket_shutdown($socket['socket']);
                            socket_close($socket['socket']);
                        };
                    $event = $this->event("SERVER_STOP");
                    if ($event)
                    $event($this);
                }

            function closeall($socket_id = NULL)
                {
                    if ($socket_id === NULL)
                        {
                            foreach ($this->sockets as $socket_id => $socket)
                                {
                                    foreach ($socket['channels'] as $channel_id => $channel)
                                        {
                                            $this->close($socket_id, $channel_id);
                                        }
                                }
                        }
                    else
                        {
                            foreach ($this->sockets[$socket_id]['channels'] as $channel_id => $channel)
                                {
                                    $this->close($socket_id, $channel_id);
                                };
                        };
                }

            function close($socket_id, $channel_id)
                {
                    unset($this->data_buffer[$socket_id]); //clear the sockets data buffer
                    $arrOpt = array('l_onoff' => 1, 'l_linger' => 1);
                    @socket_shutdown($this->sockets[$socket_id]['channels'][$channel_id]['socket']);
                    @socket_close($this->sockets[$socket_id]['channels'][$channel_id]['socket']);
                    $event = $this->event("LOST_SOCKET_CHANNEL");
                    if ($event)
                    $event($socket_id, $channel_id, $this);
                }

            function loop()
                {
                    while ($this->status_listening)
                        {
                            usleep($this->delay);
                            foreach ($this->sockets as $socket)
                                {
                                    $this->new_socket_loop($socket);
                                    $this->recv_socket_loop($socket);
                                };
                            $event = $this->event("END_SOCKET_CHANNEL");
                            if ($event)
                            $event($this);
                        };
                }

            function write($socket_id, $channel_id, $buffer)
                {   
                    @socket_write($this->sockets[$socket_id]['channels'][$channel_id]['socket'], $buffer);
                    @socket_write($this->sockets[$socket_id]['channels'][$channel_id]['socket'], 'Server memory usage: '.memory_get_usage().'/'.memory_get_peak_usage(true)."\r\n");
                }

            function get_channel_info($socket_id, $channel_id)
                {
                    return $this->sockets[$socket_id]['channels'][$channel_id]['info'];
                }

            function get_socket_info($socket_id)
                {
                    $socket_info = $this->sockets[$socket_id]['info'];
                    if (empty($socket_info['ADDR']))
                        {
                            $socket_info['ADDR'] = "*";
                        };
                    return $socket_info;
                }

            function get_raw_channel_socket($socket_id, $channel_id)
                {
                    return $this->sockets[$socket_id]['channels'][$channel_id]['socket'];
                }

            function remote_address($channel_socket, &$ipaddress, &$port)
                {
                    socket_getpeername($channel_socket, $ipaddress, $port);
                }

            function event($name)
                {
                    if (isset($this->event_callbacks[$name]))
                    return $this->event_callbacks[$name];
                }

            function assign_callback($name, $function_name)
                {
                    $this->event_callbacks[$name] = $function_name;
                }
        };

    ?>

Server.php

include("supersocket.class.php");

function startswith($string, $test) {
    return strpos($string, $test, 0) === 0;
}

function newdata($socket_id, $channel_id, $buffer, &$server)
    {
        //$server->write($socket_id, $channel_id, ">".$buffer."\r\n");
        if($buffer=="STOP")
        {
            $server->stop();
        }
        else if($buffer=="DATETIME")
        {
            $server->write($socket_id, $channel_id, ">".date("dmYHis")."\r\n");
        }
        else
        {
            $server->write($socket_id, $channel_id, ">BAD\r\n");
        }

    };

function newclient($socket_id, $channel_id, &$server)
{
    $server->write($socket_id, $channel_id, "HEADER\n\r");
}

$socket = new SuperSocket(array('127.0.0.1:12345')); 
$socket->assign_callback("DATA_SOCKET_CHANNEL", "newdata");
$socket->assign_callback("NEW_SOCKET_CHANNEL", "newclient");
$socket->start();
//set_time_limit(60*2);
set_time_limit(60*60*24*5); //5 days
$socket->loop();

Edit: sorry you might need to change the socket accept back to: if ($newchannel = @socket_accept($socket['socket']))

then close the connection, memory is leaked

This is a tricky one - even the standard reference counting garbage collector only kicks in at intervals which are difficult to predict. Calling gc_collect_cycles () should trigger the gc though. Try calling that whenever you close a connection and see if it makes a difference.

If you're still seeing problems - then check if you've got the cyclic reference counter compiled in - if not, then get it.

The Channel array was never removed upon closing the connection, surprised no one picked up on this. Memory usage is now super tight.

unset($this->sockets[$socket_id]['channels'][$channel_id]);

But it does mean that any event for LOST_SOCKET_CHANNEL is pretty useless for the time being.

Will accept my own answer when stack over flow allows. Thanks for all your help ppl .. i guess..

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM