2012-11-03 177 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的文档。