2012-11-03 317 views
11

我正在嘗試創建一個簡單的客戶端/服務器應用程序,因此我正在嘗試使用PHP中的套接字。現在我在C#中有一個簡單的客戶端,它很好地連接到服務器,但我只能連接一個客戶端到這個服務器(我發現這個代碼示例在線,併爲測試目的調整了一下)。PHP套接字 - 接受多個連接

搞笑的是我發現了同樣的問題,基於同樣的例子在這裏: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 incomming 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); 
?> 
+0

你有沒有碰運氣? –

+0

請參閱:[SocketServer.class.php](https://gist.github.com/navarr/459321) – kenorb

回答

1

如果要處理> 1個客戶端,通常套接字服務器需要是多線程的。你會創建一個「監聽」線程,併爲每個客戶端請求產生一個新的「答案」線程。我不知道PHP如何處理這樣的情況。也許它有一個fork機制?

編輯:不會出現PHP本身提供線程(如果你想要的話)(http://stackoverflow.com/questions/70855/how-can-one-use-multi-threading-in-php-applications)如果你想遵循典型的套接字服務器範例,您可能會使用'popen'來產生一個處理子請求的流程。關閉套接字標識並在子套接字關閉時讓其自行關閉。如果您的服務器進程關閉,您需要保持在列表頂部以避免孤立這些進程。

FWIW:這裏是多客戶端服務器的一些例子:http://php.net/manual/en/function.socket-accept.php

+0

嘿,謝謝你的回覆。你真的讀過我的代碼嗎?我正在通過一系列連接來處理所有這些事情。就像您鏈接的許多示例一樣。還是謝謝! :) – JapyDooge

+0

對 - 但它看起來像阻止連接和閱讀。除非建立新的連接,否則它不會處理任何掛起的請求。 – ethrbunny

+0

啊,可能是這種情況,謝謝你:)我會看看我能如何去做! – JapyDooge

1

此腳本工作完美的我

<?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

這裏目前最大的答案是錯的,你不需要多線程處理多個客戶端。您可以使用非阻塞I/O和stream_select/socket_select來處理來自可操作客戶端的消息。我建議通過socket_*使用stream_socket_*函數。

雖然非阻塞I/O工作得很好,你不能做任何函數調用與涉及阻塞I/O,否則是阻塞I/O模塊的完整過程和所有客戶端掛起,不只是一個。

這意味着所有的I/O必須是無阻塞或保證是非常快的(這是不完美的,但可以接受)。因爲不僅是你的插座需要使用stream_select,但你需要在所有打開的流選擇,我建議,可向登記被執行一次流變成可讀/寫讀寫觀察家庫。

有多個這樣的框架可用,最常見的是ReactPHPAmp。基礎事件循環非常相似,但Amp在這方面還提供了更多功能。

兩者之間的主要區別在於API的方法。儘管ReactPHP在各處都使用回調函數,但Amp試圖通過使用協程並優化其API以達到這種用途來避免它們。

Amp's 「入門」指南基本上就是關於這個話題的。您可以閱讀完整指南here。我將在下面包含一個工作示例。

<?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的文檔。