| |
| — | multi-client_tcp_server_example [2014/11/24 01:14] (current) – created 0.0.0.0 |
|---|
| | ====== Multi-Client_TCP_Server_Example ====== |
| |
| | <source lang="php"><?php |
| | |
| | /***************************************************************************** |
| | * PHP Multi-Client TCP Socket Example * |
| | ***************************************************************************** |
| | * Will listen on a given socket for TCP connections, echoing whatever data * |
| | * is sent to that socket. * |
| | * * |
| | * Original script by KnoB in a comment in the PHP documentation: * |
| | * http://www.php.net/manual/en/ref.sockets.php#43155 * |
| | * * |
| | * Heavily modified and commented by Andrew Gillard * |
| | *****************************************************************************/ |
| | |
| | <nowiki>//</nowiki>What address are we listening on? This will have to be the same as in |
| | <nowiki>//</nowiki> the client. You probably just want '127.0.0.1' for both. |
| | $address = '127.0.0.1'; |
| | |
| | <nowiki>//</nowiki>What port to use? Again, the client will need to know this, too |
| | $port = 10000; |
| | |
| | ?><!DOCTYPE html PUBLIC "-<nowiki>//</nowiki>W3C<nowiki>//</nowiki>DTD XHTML 1.0 Strict<nowiki>//</nowiki>EN" |
| | "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
| | <html xmlns="http://www.w3.org/1999/xhtml"> |
| | <head> |
| | <title>PHP Multi-Client Server Example</title> |
| | <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> |
| | <style type="text/css"> |
| | div.error { |
| | padding: 0px 10px 5px 10px; |
| | background-color: #f88; |
| | border: 1px solid #f00; |
| | } |
| | div.error span { |
| | font-family: monospace; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | |
| | <?php |
| | |
| | <nowiki>//</nowiki>Disable PHP's script execution time limit |
| | set_time_limit(0); |
| | |
| | <nowiki>//</nowiki>Ensure that every time we call "echo", the data is sent to the browser |
| | <nowiki>//</nowiki> IMMEDIATELY, rather than when PHP feels like it |
| | ob_implicit_flush(); |
| | |
| | <nowiki>//</nowiki>Normally when the user clicks the "Stop" button in their browser, the |
| | <nowiki>//</nowiki> script is terminated. This line stops that happening, so that we can |
| | <nowiki>//</nowiki> detect the Stop button ourselves and properly close our sockets (to |
| | <nowiki>//</nowiki> prevent the listening socket remaining open and stealing the port) |
| | ignore_user_abort(true); |
| | |
| | <nowiki>//</nowiki>Define a function that we can call when any of our socket function calls |
| | <nowiki>//</nowiki> fail. This allows us to consolidate our error message XHTML and avoid |
| | <nowiki>//</nowiki> code repetition. If $die is set to true, the script will terminate |
| | function socketError($errorFunction, $die=false) { |
| | $errMsg = socket_strerror(socket_last_error()); |
| | |
| | <nowiki>//</nowiki>This odd construct (known as a heredoc) just echos all of the text |
| | <nowiki>//</nowiki> between "<<<EOHTML" and "EOHTML;". It's just a neater and easier to read |
| | <nowiki>//</nowiki> format than using standard quoted strings. If you want to use one |
| | <nowiki>//</nowiki> yourself, bear in mind that the structure is VERY strict: the opening |
| | <nowiki>//</nowiki> line must be just "<<<" followed by the ending identifier, and the last |
| | <nowiki>//</nowiki> line must contain NOTHING except the identifier ("EOHTML" in this case). |
| | <nowiki>//</nowiki> The semi-colon after the closing identifier is optional, but it is |
| | <nowiki>//</nowiki> important to realise that there cannot even be whitespace (tabs or |
| | <nowiki>//</nowiki> spaces) before the EOHTML; at the end!! |
| | echo <<<EOHTML |
| | <div class="error"> |
| | <h1>$errorFunction() failed!</h1> |
| | <p> |
| | <strong>Error Message:</strong> |
| | <span>$errMsg</span> |
| | </p> |
| | <p>Note that if you have recently pressed your browser's Stop or |
| | Refresh/Reload button on this server script, you may have to wait a few |
| | seconds for the old server to release its listening port. As such, wait |
| | and try again in a few seconds. |
| | </p> |
| | </div> |
| | EOHTML; |
| | |
| | if ($die) { |
| | <nowiki>//</nowiki>Close the BODY and HTML tags as well as terminating script |
| | <nowiki>//</nowiki>execution because the die() call prevents us ever getting to the last |
| | <nowiki>//</nowiki> lines of this script |
| | die('</body></html>'); |
| | } |
| | } |
| | |
| | <nowiki>//</nowiki>Attempt to create our socket. The "@" hides PHP's standard error reporting, |
| | <nowiki>//</nowiki> so that we can output our own error message if it fails |
| | if (!($server = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP))) { |
| | socketError('socket_create', true); |
| | } |
| | |
| | <nowiki>//</nowiki>Set the "Reuse Address" socket option to enabled |
| | socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1); |
| | |
| | <nowiki>//</nowiki>Attempt to bind our socket to the address and port that we're listening on. |
| | <nowiki>//</nowiki> Again, we suppress PHP's error reporting in favour of our own |
| | if (!@socket_bind($server, $address, $port)) { |
| | socketError('socket_bind', true); |
| | } |
| | |
| | <nowiki>//</nowiki>Start listening on the address and port that we bound our socket to above, |
| | <nowiki>//</nowiki> using our own error reporting code as before |
| | if (!@socket_listen($server)) { |
| | socketError('socket_listen', true); |
| | } |
| | |
| | <nowiki>//</nowiki>Create an array to store our sockets in. We use this so that we can |
| | <nowiki>//</nowiki> determine which socket has new incoming data with the "socket_select()" |
| | <nowiki>//</nowiki> function, and to properly close each socket when the script finishes |
| | $allSockets = array($server); |
| | |
| | <nowiki>//</nowiki>Start looping indefinitely. On each iteration we will make sure the browser's |
| | <nowiki>//</nowiki> "Stop" button hasn't been pressed and, if not, see if we have any incoming |
| | <nowiki>//</nowiki> client connection requests or any incoming data on existing clients |
| | while (true) { |
| | <nowiki>//</nowiki>We have to echo something to the browser or PHP won't know if the Stop |
| | <nowiki>//</nowiki> button has been pressed |
| | echo ' '; |
| | if (connection_aborted()) { |
| | <nowiki>//</nowiki>The Stop button has been pressed, so close all our sockets and exit |
| | foreach ($allSockets as $socket) { |
| | socket_close($socket); |
| | } |
| | |
| | <nowiki>//</nowiki>Now break out of this while() loop! |
| | break; |
| | } |
| | |
| | <nowiki>//</nowiki>socket_select() is slightly strange. You have to make a copy of the array |
| | <nowiki>//</nowiki> of sockets you pass to it, because it changes that array when it returns |
| | <nowiki>//</nowiki> and the resulting array will only contain sockets with waiting data on |
| | <nowiki>//</nowiki> them. $write and $except are set to NULL because we aren't interested in |
| | <nowiki>//</nowiki> them. The last parameter indicates that socket_select will return after |
| | <nowiki>//</nowiki> that many seconds if no data is receiveed in that time; this prevents the |
| | <nowiki>//</nowiki> script hanging forever at this point (remember, we might want to accept a |
| | <nowiki>//</nowiki> new connection or even exit entirely) and also pauses the script briefly |
| | <nowiki>//</nowiki> to prevent this tight while() loop using a lot of processor time |
| | $changedSockets = $allSockets; |
| | socket_select($changedSockets, $write = NULL, $except = NULL, 5); |
| | |
| | <nowiki>//</nowiki>Now we loop over each of the sockets that socket_select() says have new |
| | <nowiki>//</nowiki> data on them |
| | foreach($changedSockets as $socket) { |
| | if ($socket == $server) { |
| | <nowiki>//</nowiki>socket_select() will include our server socket in the |
| | <nowiki>//</nowiki> $changedSockets array if there is an incoming connection attempt |
| | <nowiki>//</nowiki> on it. This will only accept one incoming connection per while() |
| | <nowiki>//</nowiki> loop iteration, but that shouldn't be a problem given the |
| | <nowiki>//</nowiki> frequency that we're iterating |
| | if (!($client = @socket_accept($server))) { |
| | <nowiki>//</nowiki>socket_accept() failed for some reason (again, we hid PHP's |
| | <nowiki>//</nowiki> standard error message), so let's say what happened... |
| | socketError('socket_accept', false); |
| | } else { |
| | <nowiki>//</nowiki>We've accepted the incoming connection, so add the new client |
| | <nowiki>//</nowiki> socket to our array of sockets |
| | $allSockets[[]] = $client; |
| | } |
| | } else { |
| | <nowiki>//</nowiki>Attempt to read data from this socket |
| | $data = socket_read($socket, 2048); |
| | if ($data === false || $data === //) { |
| | <nowiki>//</nowiki>socket_read() returned FALSE, meaning that the client has |
| | <nowiki>//</nowiki> closed the connection. Therefore we need to remove this |
| | <nowiki>//</nowiki> socket from our client sockets array and close the socket |
| | <nowiki>//</nowiki>A potential bug in PHP means that socket_read() will return |
| | <nowiki>//</nowiki> an empty string instead of FALSE when the connection has |
| | <nowiki>//</nowiki> been closed, contrary to what the documentation states. As |
| | <nowiki>//</nowiki> such, we look for FALSE or an empty string (an empty string |
| | <nowiki>//</nowiki> for the current, buggy, behaviour, and FALSE in case it ends |
| | <nowiki>//</nowiki> up getting fixed at some point) |
| | unset($allSockets[[array_search($socket,|$allSockets)]]); |
| | socket_close($socket); |
| | } else { |
| | <nowiki>//</nowiki>We got useful data from socket_read(), so let's echo it. |
| | <nowiki>//</nowiki> "$socket" will be output as "Resource id #n", where n is |
| | <nowiki>//</nowiki> the internal ID of the socket, e.g. "Resource id #3" |
| | <nowiki>//</nowiki>Note also that $data can be an empty string, so we check |
| | <nowiki>//</nowiki> for that in our "elseif ($data)" line |
| | echo "\r\n<p><strong>·</strong> $socket wrote: $data</p>"; |
| | } |
| | } |
| | } |
| | } |
| | |
| | <nowiki>//</nowiki>Once we get here, the sockets have been closed, so just echo our XHTML footer |
| | |
| | ?> |
| | |
| | </body> |
| | </html></source> |