PHP Sockets - Accept multiple connections

前端 未结 4 1356
刺人心
刺人心 2021-02-05 13:12

I\'m trying to create a simple client/server application and thus I am experimenting with sockets in PHP.

Now I have a simple client in C# which connects to the server we

相关标签:
4条回答
  • 2021-02-05 13:43

    Check This

    git clone https://github.com/lukaszkujawa/php-multithreaded-socket-server.git socketserver
    cd socketserver
    php server.php
    

    for more information go to: http://systemsarchitect.net/multi-threaded-socket-server-in-php-with-fork/

    0 讨论(0)
  • 2021-02-05 13:48

    This script is working perfectly for me

    <?php
        /*! @class      SocketServer
            @author     Navarr Barnier
            @abstract   A Framework for creating a multi-client server using the PHP language.
         */
        class SocketServer
        {
            /*! @var        config
                @abstract   Array - an array of configuration information used by the server.
             */
            protected $config;
    
            /*! @var        hooks
                @abstract   Array - a dictionary of hooks and the callbacks attached to them.
             */
            protected $hooks;
    
            /*! @var        master_socket
                @abstract   resource - The master socket used by the server.
             */
            protected $master_socket;
    
            /*! @var        max_clients
                @abstract   unsigned int - The maximum number of clients allowed to connect.
             */
            public $max_clients = 10;
    
            /*! @var        max_read
                @abstract   unsigned int - The maximum number of bytes to read from a socket at a single time.
             */
            public $max_read = 1024;
    
            /*! @var        clients
                @abstract   Array - an array of connected clients.
             */
            public $clients;
    
            /*! @function   __construct
                @abstract   Creates the socket and starts listening to it.
                @param      string  - IP Address to bind to, NULL for default.
                @param      int - Port to bind to
                @result     void
             */
            public function __construct($bind_ip,$port)
            {
                set_time_limit(0);
                $this->hooks = array();
    
                $this->config["ip"] = $bind_ip;
                $this->config["port"] = $port;
    
                $this->master_socket = socket_create(AF_INET, SOCK_STREAM, 0);
                socket_bind($this->master_socket,$this->config["ip"],$this->config["port"]) or die("Issue Binding");
                socket_getsockname($this->master_socket,$bind_ip,$port);
                socket_listen($this->master_socket);
                SocketServer::debug("Listenting for connections on {$bind_ip}:{$port}");
            }
    
            /*! @function   hook
                @abstract   Adds a function to be called whenever a certain action happens.  Can be extended in your implementation.
                @param      string  - Command
                @param      callback- Function to Call.
                @see        unhook
                @see        trigger_hooks
                @result     void
             */
            public function hook($command,$function)
            {
                $command = strtoupper($command);
                if(!isset($this->hooks[$command])) { $this->hooks[$command] = array(); }
                $k = array_search($function,$this->hooks[$command]);
                if($k === FALSE)
                {
                    $this->hooks[$command][] = $function;
                }
            }
    
            /*! @function   unhook
                @abstract   Deletes a function from the call list for a certain action.  Can be extended in your implementation.
                @param      string  - Command
                @param      callback- Function to Delete from Call List
                @see        hook
                @see        trigger_hooks
                @result     void
             */
            public function unhook($command = NULL,$function)
            {
                $command = strtoupper($command);
                if($command !== NULL)
                {
                    $k = array_search($function,$this->hooks[$command]);
                    if($k !== FALSE)
                    {
                        unset($this->hooks[$command][$k]);
                    }
                } else {
                    $k = array_search($this->user_funcs,$function);
                    if($k !== FALSE)
                    {
                        unset($this->user_funcs[$k]);
                    }
                }
            }
    
            /*! @function   loop_once
                @abstract   Runs the class's actions once.
                @discussion Should only be used if you want to run additional checks during server operation.  Otherwise, use infinite_loop()
                @param      void
                @see        infinite_loop
                @result     bool    - True
            */
            public function loop_once()
            {
                // Setup Clients Listen Socket For Reading
                $read[0] = $this->master_socket;
                for($i = 0; $i < $this->max_clients; $i++)
                {
                    if(isset($this->clients[$i]))
                    {
                        $read[$i + 1] = $this->clients[$i]->socket;
                    }
                }
    
                // Set up a blocking call to socket_select
                if(socket_select($read,$write = NULL, $except = NULL, $tv_sec = 5) < 1)
                {
                //  SocketServer::debug("Problem blocking socket_select?");
                    return true;
                }
    
                // Handle new Connections
                if(in_array($this->master_socket, $read))
                {
                    for($i = 0; $i < $this->max_clients; $i++)
                    {
                        if(empty($this->clients[$i]))
                        {
                            $temp_sock = $this->master_socket;
                            $this->clients[$i] = new SocketServerClient($this->master_socket,$i);
                            $this->trigger_hooks("CONNECT",$this->clients[$i],"");
                            break;
                        }
                        elseif($i == ($this->max_clients-1))
                        {
                            SocketServer::debug("Too many clients... :( ");
                        }
                    }
    
                }
    
                // Handle Input
                for($i = 0; $i < $this->max_clients; $i++) // for each client
                {
                    if(isset($this->clients[$i]))
                    {
                        if(in_array($this->clients[$i]->socket, $read))
                        {
                            $input = socket_read($this->clients[$i]->socket, $this->max_read);
                            if($input == null)
                            {
                                $this->disconnect($i);
                            }
                            else
                            {
                                SocketServer::debug("{$i}@{$this->clients[$i]->ip} --> {$input}");
                                $this->trigger_hooks("INPUT",$this->clients[$i],$input);
                            }
                        }
                    }
                }
                return true;
            }
    
            /*! @function   disconnect
                @abstract   Disconnects a client from the server.
                @param      int - Index of the client to disconnect.
                @param      string  - Message to send to the hooks
                @result     void
            */
            public function disconnect($client_index,$message = "")
            {
                $i = $client_index;
                SocketServer::debug("Client {$i} from {$this->clients[$i]->ip} Disconnecting");
                $this->trigger_hooks("DISCONNECT",$this->clients[$i],$message);
                $this->clients[$i]->destroy();
                unset($this->clients[$i]);          
            }
    
            /*! @function   trigger_hooks
                @abstract   Triggers Hooks for a certain command.
                @param      string  - Command who's hooks you want to trigger.
                @param      object  - The client who activated this command.
                @param      string  - The input from the client, or a message to be sent to the hooks.
                @result     void
            */
            public function trigger_hooks($command,&$client,$input)
            {
                if(isset($this->hooks[$command]))
                {
                    foreach($this->hooks[$command] as $function)
                    {
                        SocketServer::debug("Triggering Hook '{$function}' for '{$command}'");
                        $continue = call_user_func($function,&$this,&$client,$input);
                        if($continue === FALSE) { break; }
                    }
                }
            }
    
            /*! @function   infinite_loop
                @abstract   Runs the server code until the server is shut down.
                @see        loop_once
                @param      void
                @result     void
            */
            public function infinite_loop()
            {
                $test = true;
                do
                {
                    $test = $this->loop_once();
                }
                while($test);
            }
    
            /*! @function   debug
                @static
                @abstract   Outputs Text directly.
                @discussion Yeah, should probably make a way to turn this off.
                @param      string  - Text to Output
                @result     void
            */
            public static function debug($text)
            {
                echo("{$text}\r\n");
            }
    
            /*! @function   socket_write_smart
                @static
                @abstract   Writes data to the socket, including the length of the data, and ends it with a CRLF unless specified.
                @discussion It is perfectly valid for socket_write_smart to return zero which means no bytes have been written. Be sure to use the === operator to check for FALSE in case of an error. 
                @param      resource- Socket Instance
                @param      string  - Data to write to the socket.
                @param      string  - Data to end the line with.  Specify a "" if you don't want a line end sent.
                @result     mixed   - Returns the number of bytes successfully written to the socket or FALSE on failure. The error code can be retrieved with socket_last_error(). This code may be passed to socket_strerror() to get a textual explanation of the error.
            */
            public static function socket_write_smart(&$sock,$string,$crlf = "\r\n")
            {
                SocketServer::debug("<-- {$string}");
                if($crlf) { $string = "{$string}{$crlf}"; }
                return socket_write($sock,$string,strlen($string));
            }
    
            /*! @function   __get
                @abstract   Magic Method used for allowing the reading of protected variables.
                @discussion You never need to use this method, simply calling $server->variable works because of this method's existence.
                @param      string  - Variable to retrieve
                @result     mixed   - Returns the reference to the variable called.
            */
            function &__get($name)
            {
                return $this->{$name};
            }
        }
    
        /*! @class      SocketServerClient
            @author     Navarr Barnier
            @abstract   A Client Instance for use with SocketServer
         */
        class SocketServerClient
        {
            /*! @var        socket
                @abstract   resource - The client's socket resource, for sending and receiving data with.
             */
            protected $socket;
    
            /*! @var        ip
                @abstract   string - The client's IP address, as seen by the server.
             */
            protected $ip;
    
            /*! @var        hostname
                @abstract   string - The client's hostname, as seen by the server.
                @discussion This variable is only set after calling lookup_hostname, as hostname lookups can take up a decent amount of time.
                @see        lookup_hostname
             */
            protected $hostname;
    
            /*! @var        server_clients_index
                @abstract   int - The index of this client in the SocketServer's client array.
             */
            protected $server_clients_index;
    
            /*! @function   __construct
                @param      resource- The resource of the socket the client is connecting by, generally the master socket.
                @param      int - The Index in the Server's client array.
                @result     void
             */
            public function __construct(&$socket,$i)
            {
                $this->server_clients_index = $i;
                $this->socket = socket_accept($socket) or die("Failed to Accept");
                SocketServer::debug("New Client Connected");
                socket_getpeername($this->socket,$ip);
                $this->ip = $ip;
            }
    
            /*! @function   lookup_hostname
                @abstract   Searches for the user's hostname and stores the result to hostname.
                @see        hostname
                @param      void
                @result     string  - The hostname on success or the IP address on failure.
             */
            public function lookup_hostname()
            {
                $this->hostname = gethostbyaddr($this->ip);
                return $this->hostname;
            }
    
            /*! @function   destroy
                @abstract   Closes the socket.  Thats pretty much it.
                @param      void
                @result     void
             */
            public function destroy()
            {
                socket_close($this->socket);
            }
    
            function &__get($name)
            {
                return $this->{$name};
            }
    
            function __isset($name)
            {
                return isset($this->{$name});
            }
        }
    

    Source on github

    0 讨论(0)
  • 2021-02-05 13:49

    The current top answer here is wrong, you don't need multiple threads to handle multiple clients. You can use non-blocking I/O and stream_select / socket_select to process messages from clients that are actionable. I'd recommend using the stream_socket_* functions over socket_*.

    While non-blocking I/O works quite fine, you can't make any function calls with involve blocking I/O, otherwise that blocking I/O blocks the complete process and all clients hang, not just one.

    That means all I/O has to be non-blocking or guaranteed to be very fast (which isn't perfect, but might be acceptable). Because not only your sockets need to use stream_select, but you need to select on all open streams, I'd recommend a library that offers to register read and write watchers that are executed once a stream becomes readable / writable.

    There are multiple such frameworks available, the most common ones are ReactPHP and Amp. The underlying event loops are pretty similar, but Amp offers a few more features on that side.

    The main difference between the two is the approach for APIs. While ReactPHP uses callbacks everywhere, Amp tries to avoid them by using coroutines and optimizing its APIs for such a usage.

    Amp's "Getting Started" guide is basically exactly about this topic. You can read the full guide here. I'll include a working example below.

    <?php
    
    require __DIR__ . "/vendor/autoload.php";
    
    // Non-blocking server implementation based on amphp/socket.
    
    use Amp\Loop;
    use Amp\Socket\ServerSocket;
    use function Amp\asyncCall;
    
    Loop::run(function () {
        $uri = "tcp://127.0.0.1:1337";
    
        $clientHandler = function (ServerSocket $socket) {
            while (null !== $chunk = yield $socket->read()) {
                yield $socket->write($chunk);
            }
        };
    
        $server = Amp\Socket\listen($uri);
    
        while ($socket = yield $server->accept()) {
            asyncCall($clientHandler, $socket);
        }
    });
    

    Loop::run() runs the event loop and watches for timer events, signals and actionable streams, which can be registered with Loop::on*() methods. A server socket is created using Amp\Socket\listen(). Server::accept() returns a Promise which can be used to await new client connections. It executes a coroutine once a client is accepted that reads from the client and echo's the same data back to it. For more details, refer to Amp's documentation.

    0 讨论(0)
  • 2021-02-05 14:03

    Typically socket servers need to be multi-threaded if you want to handle > 1 client. You'd create a 'listen' thread and spawn a new 'answer' thread for each client request. Im not sure how PHP would handle a situation like this though. Perhaps it has a fork mechanism?

    EDIT: Doesn't appear that PHP offers threading per se (http://stackoverflow.com/questions/70855/how-can-one-use-multi-threading-in-php-applications) If you want to follow the typical paradigm for a socket server you might get away with using 'popen' to spawn a process to handle the child request. Hand off the socket id and let it close itself when the child socket closes. You'd need to keep on top of this list to avoid orphaning these processes if your server process closes.

    FWIW: here are some examples of multi-client servers: http://php.net/manual/en/function.socket-accept.php

    0 讨论(0)
提交回复
热议问题