Scriptorama.nl

Header image showing a keyboard, mouse, laptop and books on design patterns

Timeouts en PHP streams

In "POST request maken zonder CURL" beschreef ik hoe je vanuit een PHP script een formulier kon posten zonder dat je daarvoor de CURL extensie voor nodig had. Daarbij had ik een ding nog niet besproken en dat is het feit dat zo'n website ook down of slecht bereikbaar kan zijn.

Je hebt het vast wel eens aan de hand gehad: je communiceert met een andere server voor SMS of betalingen en op een dag, om wat voor reden dan ook, blijft jouw site ineens hangen. Uiteindelijk kom je er achter dat de andere server niet te bereiken was, maar je script bleef het toch proberen. In dit artikel leer je hoe je met timeouts kunt werken wanneer je met andere servers communiceert.

De connectie timeout

Er zijn verschillende plekken waarin de communicatie met een server kan mislopen. De eerste plek is bij het leggen van de connectie met de andere server. De tweede plek is bij het versturen en ontvangen van gegevens. We bekijken eerst de connectie.

PHP:
  1. $fp = fopen('http://192.168.1.230', 'r');
  2.  
  3. if (!$fp) {
  4.     die("Kon geen verbinding leggen");
  5. }
  6.  
  7. die("Verbinding geslaagd");

Bij het leggen van een connectie kunnen er allerlei dingen mislopen. Het kan zijn dat de DNS server die jouw server gebruikt nog niet de meest recente gegevens heeft, de server kan overbelast zijn en geen nieuwe connecties meer accepteren, enzovoorts. Het probleem is dat dit soort problemen niet altijd meteen een foutmelding oplevert.

De bovenstaande code kan (afhankelijk van je netwerk configuratie) geen verbinding maken met de opgegeven server, maar omdat deze in z'n geheel niet antwoord, en dus ook niet aangeeft dat er geen connectie gemaakt kan worden, geeft het script het niet zomaar op en blijft deze het proberen tot dat PHP er ongeveer na 60 seconden de brui aan geeft:

PHP Warning: fopen(http://192.168.1.230): failed to open stream: Operation timed out in /Users/mathieu/Documents/Scriptorama/Timeouts met streams/test1.php on line 11

Jouw gebruiker is al deze 60 seconden aan het wachten en het lijkt voor de gebruiker inmiddels of jouw site ook down is. Het zou fijner zijn als fopen() het al na een seconde of 2 / 3 zou opgeven zodat je direct een melding aan jouw gebruiker kunt geven.

Opmerking: Deze 60 seconden is voor alle socket-based streams te wijzigen met de default_socket_timeout configuratie optie.

Het probleem met fopen() is dat deze functie voornamelijk bedoeld is voor bestandsoperaties ipv. netwerk operaties en heeft daarom geen timeout argument zoals de functie fsockopen() heeft. Maar, zoals we al in "POST request maken zonder CURL" bespraken hebben we wel de mogelijkheid tot het configureren van de stream via een zogenaamde stream context.

Via een stream context, die we maken met de functie stream_context_create(), kunnen we aangeven dat we voor het http protocol een timeout van 5 seconden willen hebben:

PHP:
  1. $stream_context = stream_context_create (
  2.     array (
  3.         'http' => array (
  4.             'timeout' => 5
  5.         )
  6.     )
  7. );
  8.  
  9. $fp = @fopen('http://192.168.1.230', 'r', FALSE, $stream_context);
  10.  
  11. if (!$fp) {
  12.     die("Verbinding mislukt");
  13. }
  14.  
  15. die ("Verbinding geslaagd.");

Wanneer we deze code nu uitvoeren (en 192.168.1.230 is nog steeds niet bereikbaar) krijgen we nu netjes na 5 seconden de melding dat de verbinding mislukt is.

De data timeout

Een ander probleem dat kan ontstaan is dat de connectie in de eerste plaats wel gelukt is, maar dat er iets fout gaat op het moment dat we data versturen of proberen te ontvangen. De server kan bijvoorbeeld niet goed geconfigureerd zijn na een update, het ineens toch te druk krijgen of de verbinding valt ineens weg. Ook dan zal PHP ongeveer 30 seconden proberen om data te blijven ontvangen en dat is wederom vervelend voor jouw gebruiker.

Om een timeout op dit niveau af te vangen kunnen we geen gebruik maken van de stream context maar maken we gebruik van de functies stream_set_timeout() en stream_get_meta_data().

stream_set_timeout() geeft de mogelijkheid om inderdaad een timeout in te stellen en stream_get_meta_data() levert de informatie aan waarmee jij kunt bepalen of er inderdaad een timeout is verstreken:

timeout.php:

PHP:
  1. $stream_context = stream_context_create (
  2.     array (
  3.         'http' => array (
  4.             'timeout' => 5
  5.         )
  6.     )
  7. );
  8.  
  9. $fp = @fopen('http://localhost/~mathieu/sleep.php', 'r', FALSE, $stream_context);
  10.  
  11. if (!$fp) {
  12.     die("Verbinding mislukt");
  13. }
  14.  
  15. $meta_data = stream_get_meta_data($fp);
  16. $data = '';
  17.  
  18. while (!feof($fp) && (isset($meta_data['timed_out']) && !$meta_data['timed_out']))
  19. {
  20.     $data .= fread($fp, 2048);
  21.    
  22.     // Update de meta data zodat de while loop z'n ding kan doen
  23.     $meta_data = stream_get_meta_data($fp);
  24. }
  25.  
  26. fclose($fp);
  27.  
  28. if ($meta_data['timed_out'])
  29. {
  30.     echo "Lees actie timeout.<br />";
  31. } else {
  32.     echo "Lees actie geslaagd<br />";
  33. }

Het principe is vrij simpel. Nadat we de timeout hebben ingesteld met stream_set_timeout() wordt na iedere poging om gegevens te lezen of te schrijven bepaald of er een timeout is verstreken. Deze gegevens halen we na iedere lees actie op met stream_get_meta_data() en wanneer blijkt dat we een timeout hebben, springen we uit de while()-loop.

Om dit te testen kunnen we gebruik maken van een ander PHP script dat simpelweg de functie sleep() aan roept:

PHP:
  1. <?php
  2.  
  3. /* Controleer of output buffering aan staat en schakel het uit. Dit is om
  4. * er voor te zorgen dat de aanroep naar echo ook direct bij de client
  5. * aan komt. fopen() verwacht op z'n minst een valide HTTP antwoord voordat
  6. * hij de verbinding terug geeft. */
  7. if (ini_get('output_buffering'))
  8. {
  9.     ob_end_clean();
  10. }
  11.  
  12. echo "Hoi, ik ga 20 seconden niks doen"; flush();
  13.  
  14. sleep(20);
  15.  
  16. ?>

Plaats dit bestand ergens op een bereikbare server en zorg dat de URL in de aanroep naar fopen() van ons testscript 'timeout.php' er naar wijst. Wanneer je nu 'timeout.php' uitvoert krijg je na ongeveer 5 seconden de melding dat er een timeout is opgetreden in een leesactie.

Conclusie

We hebben gezien dat een timeout bij een connectie met een server op 2 momenten kan plaatsvinden: zowel tijdens de connectie als tijdens het daadwerkelijk communiceren met de server en hoe je dit kunt afvangen.

Reageer ook!

Goed artikel!
Weer iets moois geleerd :)

Nice voorbeeld Mathieu :)

Inderdaad zeer helder en informatief. Dank!

Leave a comment
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>