User Tools

Site Tools


multi-client_tcp_server_example

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

multi-client_tcp_server_example [2014/11/24 01:14] (current)
0.0.0.0 created
Line 1: Line 1:
 +====== 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>&​middot;</​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>​
multi-client_tcp_server_example.txt · Last modified: 2014/11/24 01:14 by 0.0.0.0