PHP套接字 - 接受多个连接

18
我正在尝试创建一个简单的客户端/服务器应用程序,因此我正在使用PHP中的sockets进行实验。
现在,我有一个简单的C#客户端可以成功连接到服务器,但是我只能同时连接一个客户端到这个服务器(我在网上找到了这个代码示例并为测试目的进行了一些调整)。
有趣的是,我发现了同样的问题,基于同样的示例,链接在这里:https://stackoverflow.com/questions/10318023/php-socket-connections-cant-handle-multiple-connection 我试图理解每个部分,并且接近详细了解它的工作原理,但是由于某种原因,当我连接第二个客户端时,第一个客户端会断开连接/崩溃。
有人能给我一些疯狂的想法或指向我应该查看哪里的指针吗?
<?php
// Set time limit to indefinite execution
set_time_limit (0);
// Set the ip and port we will listen on
$address = '127.0.0.1';
$port = 9000;
$max_clients = 10;
// Array that will hold client information
$client = array();
// Create a TCP Stream socket
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// Bind the socket to an address/port
socket_bind($sock, $address, $port) or die('Could not bind to address');
// Start listening for connections
socket_listen($sock);
// Loop continuously
while (true) {
    // Setup clients listen socket for reading
    $read[0] = $sock;
    for ($i = 0; $i < $max_clients; $i++)
    {
        if (isset($client[$i]))
        if ($client[$i]['sock']  != null)
            $read[$i + 1] = $client[$i]['sock'] ;
    }
    // Set up a blocking call to socket_select()
    $ready = socket_select($read, $write = NULL, $except = NULL, $tv_sec = NULL);
    /* if a new connection is being made add it to the client array */
    if (in_array($sock, $read)) {
        for ($i = 0; $i < $max_clients; $i++)
        {
            if (!isset($client[$i])) {
                $client[$i] = array();
                $client[$i]['sock'] = socket_accept($sock);
                echo("Accepting incoming connection...\n");
                break;
            }
            elseif ($i == $max_clients - 1)
                print ("too many clients");
        }
        if (--$ready <= 0)
            continue;
    } // end if in_array

    // If a client is trying to write - handle it now
    for ($i = 0; $i < $max_clients; $i++) // for each client
    {
        if (isset($client[$i]))
        if (in_array($client[$i]['sock'] , $read))
        {
            $input = socket_read($client[$i]['sock'] , 1024);
            if ($input == null) {
                // Zero length string meaning disconnected
                echo("Client disconnected\n");
                unset($client[$i]);
            }
            $n = trim($input);
            if ($n == 'exit') {
                echo("Client requested disconnect\n");
                // requested disconnect
                socket_close($client[$i]['sock']);
            }
            if(substr($n,0,3) == 'say') {
                //broadcast
                echo("Broadcast received\n");
                for ($j = 0; $j < $max_clients; $j++) // for each client
                {
                    if (isset($client[$j]))
                    if ($client[$j]['sock']) {
                        socket_write($client[$j]['sock'], substr($n, 4, strlen($n)-4).chr(0));
                    }
                }
            } elseif ($input) {
                echo("Returning stripped input\n");
                // strip white spaces and write back to user
                $output = ereg_replace("[ \t\n\r]","",$input).chr(0);
                socket_write($client[$i]['sock'],$output);
            }
        } else {
            // Close the socket
            if (isset($client[$i]))
            echo("Client disconnected\n");
            if ($client[$i]['sock'] != null){ 
                socket_close($client[$i]['sock']); 
                unset($client[$i]); 
            }
        }
    }
} // end while
// Close the master sockets
echo("Shutting down\n");
socket_close($sock);
?>

你以前在这方面有过好运吗? - tyler
请查看:SocketServer.class.php - kenorb
5个回答

7
当前最高的答案是错误的,你不需要多线程来处理多个客户端。你可以使用非阻塞 I/O 和 stream_select / socket_select 来处理来自可操作客户端的消息。我建议使用 stream_socket_* 函数而非 socket_*
虽然非阻塞 I/O 运行得很好,但你不能调用任何涉及阻塞 I/O 的函数,否则该阻塞 I/O 将阻塞整个进程,所有客户端都会挂起,而不仅仅是一个。
这意味着所有 I/O 都必须是非阻塞的或者保证非常快(这并不完美,但可能是可以接受的)。因为不仅你的套接字需要使用 stream_select,而且你需要在 所有 打开的流上进行选择,我建议使用一种库,该库提供注册读取和写入观察器以便在流变为可读/可写时执行它们。
有多个这样的框架可用,最常见的是 ReactPHPAmp。底层事件循环非常相似,但 Amp 在此方面提供了更多功能。
两者之间的主要区别是 API 的方法。虽然 ReactPHP 在各处都使用回调,但 Amp 通过使用协程并优化其 API 来避免使用它们。
Amp 的 "起步" 指南基本上就是关于这个话题的。你可以在此处阅读完整指南。我将在下面包含一个可工作的示例。
<?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()方法运行事件循环并监听定时器事件、信号和可操作流,这些可以通过Loop::on*()方法进行注册。使用Amp\Socket\listen()创建服务器套接字。Server::accept()返回一个Promise,可以用来等待新的客户端连接。它在接受客户端后执行一个协程,读取来自该客户端的数据并将相同的数据回显给客户端。有关更多详细信息,请参阅Amp文档。

5
这个脚本对我来说完美运行。
<?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});
        }
    }

GitHub上的源代码


1
这个脚本没有太多的文档,你能否概述一下如何初始化客户端和服务器连接?谢谢。 - Suresh KUMAR Mukhiya

1
我在网上找到了这个代码,但是我想在这里分享它,因为我没有在其他地方找到过。
<?php

  // port number
  $port = 5000;
  // IP address 
  $address = '127.0.0.1';
  // Maximum client number
  $max_clients_number = 10;
  // Create master stream sockets.
  $master_stream_socket = socket_create(AF_INET, SOCK_STREAM, 0);
  // Bind the socket to IP address and Port number.
  socket_bind($master_stream_socket, $address, $port);
  // Start to listen for the client.
  socket_listen($master_stream_socket);

  // This variable will hold client informations. 
  $clients = [$master_stream_socket];

  while(true){
    $read = $clients;
    
    if( socket_select($read, $write = null, $exp = null, null) ){
      if( in_array( $master_stream_socket, $read ) ){
        $c_socket = socket_accept($master_stream_socket);
        $clients[] = $c_socket;

        $key = array_search($master_stream_socket, $read);
        unset( $read[ $key ] );
      }

      if( count($read) > 0 ) {
        foreach( $read as $current_socket ) {
          $content = socket_read($current_socket, 2048);
          foreach( $clients as $client ) {
            if( $client != $master_stream_socket && $client != $current_socket ){
              socket_write($client, $content, strlen($content));
            }
          }
        }
      }
    } else {
      continue;
    }
  }

  // Close master sockets. 
  socket_close($master_stream_socket);

?>

1

通常情况下,如果您想处理> 1个客户端,则需要将套接字服务器设置为多线程。您可以创建一个“监听”线程,并为每个客户端请求生成一个新的“回答”线程。但我不确定PHP如何处理这种情况。也许它有一个分叉机制?

编辑:似乎PHP并没有提供多线程(https://dev59.com/xnVD5IYBdhLWcg3wJIEK)。如果您想遵循套接字服务器的典型范例,您可以使用“popen”来生成一个进程来处理子请求。将套接字ID移交给该进程,并在子套接字关闭时自行关闭。您需要及时跟踪此列表,以避免在服务器进程关闭时留下这些进程。

顺便说一句,这里有一些多客户端服务器的示例:http://php.net/manual/en/function.socket-accept.php


1
嘿,谢谢你的回复。你真的读了我的代码吗?我正在遍历一个连接数组来处理所有这些。就像你链接的许多示例一样。还是谢谢! :) - JapyDooge
没错 - 但是看起来你在连接和读取时都被阻塞了。除非建立新的连接,否则它将无法处理任何挂起的请求。 - ethrbunny
啊,那可能是这样的,谢谢 :) 我会看看如何让它工作的! - JapyDooge
你好,能否为我解释一下这段代码 if (in_array($sock, $read)) 是如何判断是否有新连接的呢?请帮帮我,我无法理解。 - Mohamad Haidar
嘿,你能解释一下如何在C#中为PHP套接字服务器创建客户端吗? - Prithvi Raj Nandiwal
在PHP中,你可以使用fork()函数创建进程,从而实现多线程。为了避免阻塞,你可以使用socket_select()函数。 - undefined

-4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接