Exécuter une requête asynchrone avec PHP et cURL

Je vous parlais de cURL et d’API il y a quelques jours. Aujourd’hui, j’enfonce le clou avec une méthode que j’expérimente pour un futur projet : Comment exécuter une requête asynchrone avec PHP et cURL.

L’origine du problème

Pour rappel, PHP est un langage de programmation séquentiel, impératif ou encore synchrone. C’est à dire qu’à l’exécution d’un script, il va exécuter chaque instruction séquentiellement, l’une après l’autre.
Avant de passer à la suivante, il attendra obligatoirement le résultat de la dernière instruction.

De plus, le protocole HTTP est également synchrone. Cela signifie qu’à chaque requête, le navigateur attend une réponse qu’il va ensuite afficher.

Dans la plupart des cas, ce fonctionnement suffit. L’utilisateur souhaite accéder à une ressource ou la mettre à jour : Pas de problème, la réponse est envoyée dans la foulée. Mais ce n’est pas toujours suffisant.

Quel problème ?

Je l’ai dit : Généralement le fonctionnement séquentiel est suffisant pour nos besoins. Mais il y a des tas de raisons pour vouloir faire de l’asynchrone avec PHP : Écriture de logs, de nombreux UPDATE à la DB, lancement d’une requête cURL à une API, envoie d’un pouet sur Mastodon lorsque j’écris une news, … Ces opérations peuvent être longues et donc donner la sensation aux visiteurs que votre serveur est en rade, ou que votre script est à chier.

Comprenez bien que si vous, en tant que visiteur, ne voulez pas attendre qu’une page s’affiche, il en est de même pour les usagers qui viendront sur votre site.

La requête asynchrone

La solution à cela est en effet la requête asynchrone. Pendant que votre script enregistre les logs ou se connecte à une API, il est inutile de faire attendre l’utilisateur.
On va donc chercher à faire travailler PHP en fond sur ces tâches lourdes, et continuer nos instructions plus légères jusqu’à l’envoi de la réponse au navigateur.

Plusieurs méthodes possibles

Il existe en fait d’autres approches que celle que je vais vous présenter :

  • fsockopen : ou stream_socket_client. Permet d’ouvrir une connexion par socket, afin d’enregistrer par exemple des données avec fwrite.
  • exec : Cette fonction permet de lancer une instruction, comme si vous la tapiez dans un terminal. En ajoutant un  &  à la fin de cette ligne de commande, l’exécution sera réalisée en tâche de fond et PHP n’attendra pas le résultat avant de continuer le script.
  • pg_send_query : Celle-ci supporte une ou plusieurs requêtes asynchrones à une connexion PostgreSQL. A ma connaissance, il n’existe pas d’équivalent pour MySQL.
  • curl_multi_init : Enfin, cette dernière autorise l’exécution de multiples gestionnaires cURL de façon asynchrone.

Pour ma part, voilà comment je procède.

Une requête asynchrone avec PHP et cURL

Le principe est simple si vous connaissez un peu la librairie cURL. Il suffit de lancer une requête cURL à laquelle on définit un timeout, afin que PHP n’attende pas la réponse et passe à la suite :

Oui, c’est tout.
L’option intéressante est CURLOPT_TIMEOUT_MS , qui permet de définir un temps maximum d’exécution de la fonction cURL, en millisecondes.

Attention : Évidemment, cette ressource ne doit être fait que sur le même site, sans quoi le timeout défini ne permet pas de garantir qu’un autre serveur distant ait le temps de répondre.

A la seconde près

J’ai mis 1000, soit une seconde. Cela permet de ne pas trop gêner le visiteur mais c’est aussi le délai minimum pour la résolution du hostname sur les systèmes Unix/Linux, sans quoi une erreur 28 (timeout) sera envoyée à PHP :

En faisant mes recherches, j’ai trouvé quelques articles qui mettaient une valeur de 1ms. Je ne sais pas sur quel type de serveur ils tournent, mais ça ne fonctionne pas, vous pouvez essayer.

Outrepasser cette limite

Mais on peut le faire !
Malheureusement ces articles n’en parlent pas, alors j’ai cherché pour vous 🙂

En effet, avec l’option CURLOPT_NOSIGNAL, on peut ignorer les fonctions qui causent l’envoi d’un signal au processus PHP :

Un cas concret

A présent, voyons comment cela peut nous être utile.

Imaginons un réseau social décentralisé (parait que c’est la mode), où chaque utilisateur peut suivre n’importe qui sur n’importe quelle instance. Le B.A.-BA en somme.
Lorsqu’un utilisateur, suivi par 100 personnes, poste un nouveau contenu, comment géreriez-vous cela ? Comment informer les 100 autres utilisateurs, possiblement sur 100 instances différentes, que notre bon utilisateur a posté quelque chose ? Et surtout, sans gêner l’auteur du post.
Avec cURL et notre pseudo méthode asynchrone, c’est possible.

Il suffit d’appeler une page sur le même domaine/serveur, qui traitera ces 100 requêtes cURL par exemple, en tâche de fond, à la manière d’une tâche CRON.
Pour l’utilisateur, rien ne transparaît, et le message peut se répliquer tranquillement sur chaque instance sans craindre un timeout execution.

18 Commentaires

Ajouter un commentaire

  1. On pourrait aussi appeler une autre partie de code sur la même instance ?
    ex : http:///monCodeBienLong.php

  2. Bonjour,
    J’ai une petite correction et une petite suggestion.

    Petite correction : « De plus, le protocole HTTP est également synchrone… », là je dis non: le HTTP définit un protocole dont l’implémentation peut très bien être synchrone comme asynchrone, et si php a une implémentation synchrone s’est le problème de php pas du http.
    Et les navigateur supporte les requête http synchrone comme asynchrone : si à chaque requête le navigateur attend une réponse, il n’est pas obligatoirement bloqué dans l’attente, il peut faire autre chose et même d’autre requêtes dont le traitement s’effectuera dans l’ordre d’arrivé et pas dans l’ordre d’émission (le « A » de AJAX est pour Asynchronous). D’ailleurs si on fait des requêtes javascript synchrone et que l’on ouvre la console de debugger (sous Firefox) on voit un bel avertissement : « L’utilisation d’XMLHttpRequest de façon synchrone sur le fil d’exécution principal est obsolète à cause de son impact négatif sur la navigation de l’utilisateur final. Consulter http://xhr.spec.whatwg.org/ pour plus d’informations. » Donc les requêtes synchrones sont à proscrire car elles sont bloquantes (OK ça c’est pour Javascript, mais pour php le principe est le même).

    Petite suggestion: pour le problème ici, le plus propre ne serait-il pas d’utiliser la base de données comme état de stockage et d’avoir un autre programme qui va périodiquement vérifier qu’il y a de nouvelle chose à poster. On peut par la même occasion utiliser un langage plus adapté aux requêtes asynchrones (je penses à Go pour faire ce programme) et on peut donc faire les 100 (ou x) requêtes asynchrone et effacer en BDD les requêtes réussi, ainsi les échec seront recommencé plus tard. Car en HTTP il faut toujours prévoir le scénario de l’échec d’aboutissement de la requête.
    La méthode1 proposé (timeout à 1s) risque fortement d’échouer si on envoie une requête sur un serveur à l’autre bout de la planète ou derrière plein de routeurs ou un serveur sous grosse charge.
    Et la méthode2 (curlnosignal) ne risque elle pas de faire tourner indéfiniment un processus php en cas d’échec de liaison avec le serveur distant ?

    • @tranche, tu prends quelques raccourcis et tu confonds certaines notion :

      Synchro – protocole HTTP :

      Ce n’est pas PHP qui autorise ou non ce détail du protocole mais le serveur qui reçoit les requête.

      Même si la RFC de HTTP n’interdit pas formellement aux implémentation de ne pas retourner de réponses (https://tools.ietf.org/html/rfc7230#section-2.1), les implémentations serveur (apache/httpd, glassfish, nginx, IIS, etc.) et client (navigateurs, divers client selon les langages – ex : Guzzle en PHP) du protocole ne fonctionnent qu’en synchrone.

      3 notions : connexion, requête, réponse

      Le client établis une connexion avec le serveur (via TCP en général), envoie sa requête si la connexion est acceptée (avec un timeout en général). Si le serveur n’a pas répondu après le timeout, on reprend l’exécution côté client. Pour ce qui est du maintien de la connexion, ça dépend de la stratégie du serveur et des headers des requêtes client.

      Un exemple simple côté navigateur avec Google Chrome qui nous lâche un ERR_CONNECTION_TIMED_OUT après le timeout sur une requête utilisateur.

      Synchro – client :
      Là effectivement, au niveau du client, on peut mettre en place de l’asynchrone. C’est-à-dire choisir une implémentation non impérative qui permet de continuer l’exécution en attendant la réponse du serveur « en arrière plan ».

      Implem’ JS : AJAX, effectivement, qui utilise la queue d’exécution du navigateur
      Exemple d’implem’ utilisant les threads en Java : http://stackoverflow.com/a/3143189

      • Non je me suis mal exprimé peut être mais je ne suis pas d’accord non plus avec ce qui est dit : être asynchrone signifie ne pas être bloquant. HTTP étant un protocole stateless (donc le résultat de chaque requête ne dépend pas des précédente) il n’as aucune bonne raison d’être implémenté de manière synchrone (bloquante).
        Exemple :
        Le navigateur fait 2 requêtes à un serveur Apache + php, requête1 et requête2 dans cette ordre : requête1 est très lente à être traité par le serveur et requête2 est très rapide.
        Dans une implémentation synchrone le traitement de requête1 bloquerai le traitement de requête2 et le serveur ne répondrai à requête2 que lorsque requête1 aurai abouti. Et ce n’est pas ce qui se passe car si php est synchrone, apache lui lance plusieurs fil d’exécution de php et donc chaque requête à son fil d’exécution de php et n’est pas bloqué par l’autre, le navigateur peut recevoir la réponse à requête2 avant celle de requête1, le traitement est donc asynchrone.

        Ici le problème est que dans un fil php l’auteur souhaite faire d’autres exécutions (des requêtes http) et ce n’est pas simple de faire ça en asynchrone en php.
        Sinon nodeJS et tout les serveurs moderne (nginx, lighthttpd) se basent sur une event loop non bloquante et sont donc événementiel par design ce qui rend l’implémentation bien asynchrone. Et pour go, les goroutines rendent l’implémentation serveur asynchrone. D’ailleurs tous les serveur s’attaquant au problème des C10k sont tous asynchrone et heureusement.

    • Bonjour tranche.

      Je pense également que tu confonds protocole et serveur.
      Le protocole HTTP n’est en gros qu’une connexion, requête et réponse. A chaque requête, le client attend une réponse.
      Le fait qu’un client puisse envoyer 2 requêtes simultanément n’est pas du fait de HTTP, mais du serveur.

      Concernant ta suggestion, c’est en effet une des solutions plus propres que de lancer 100 requêtes cURL.
      On peut également utiliser un fichier où l’on stocke les actions à lancer, qui seront exécutées par une tâche CRON par exemple.

      Enfin, je l’ai mis en rouge mais ça ne semble toujours pas assez visible : Cette méthode pour simuler un fonctionnement asynchrone ne peut se faire qu’en appelant le même serveur que celui où est appelée la requête.
      J’aurai dû mettre http://locahost ou 127.0.0.1, ça aurait été peut-être plus clair.

      Merci de tes commentaires.

      • Arf, je vois on ne parlais pas exactement de la même chose, au niveau d’une seule connexion TCP/IP le schéma requête->réponse du HTTP peut paraître synchrone effectivement, sauf que ce n’est pas (toujours) le cas non plus car depuis le HTTP1.1 le pipelining de requête est supporté ( https://fr.wikipedia.org/wiki/Pipelining_HTTP ). Dire que HTTP est un protocole synchrone n’as donc pas de sens.
        Et sinon le HTTP niveau serveur c’est plutôt plusieurs requête avec plusieurs connexion TCP/IP : les navigateurs utilisent un pool de 6 connexions et donc pour le coup au niveau serveur le flux de requêtes est asynchrone.
        Sinon pour le text en rouge c’est bête mais du coup je l’ai pas lu, je dois être habitué à filtré les pub, du coup tout ce qui n’est pas formaté comme un paragraphe je le zappe. 🙂 … Du coup effectivement y a pas trop de problème de latence réseau. Mais si on ne peut pas mettre 10ms c’est que le temps d’exécution du php et de curl rentre quand même en jeux. Donc si 1s est valable pour un PC correcte, ça ne le sera peut être pas pour RaspberryPi v1. Et le Curloptnosignal semble (j’ai pas creuser à fond) couper la communication entre php et le processus curl sous-jacent, ça relève beaucoup du hack quand même.

        • En effet, on ne parle pas de la même chose 😉

          Pour le nosignal, on trouve peu de ressources dessus, et même la doc sur php.net n’est pas très claire. Mais c’est clairement un hack et pas du tout une utilisation ‘standard’ de la lib.

  3. Bonjour,

    si vous cherchez une solution asynchrone, on peut regarder du côté des websocket, on est en full duplex, et pas besoin de framework
    À ce jour j’ai pu faire transiter à peu près n’importe quoi ( chat, flux video/audio, fichiers, CRON distant, etc … ) en liaison avec des BDD si besoin est.

    il y un exemple pour débuter sur le php.net : http://php.net/manual/en/function.socket-select.php
    attention : pour la fonction socket_select() pour le paramètre tv_sec il ne faut pas mettre 0 ni NULL mais une valeur élevé ( par exemple 150ms ) sinon c’est le deamon qui va fumer 😉

    bisou

    • Bonjour,

      Je ne connais que trop peu les websocket. Je n’en ai jamais eu le besoin, ou alors j’ai fait sans, mais il faudrait que je m’y mette un jour.
      On trouve peu de doc francophone à ce sujet, c’est peut-être l’occasion 😉

      Merci.

  4. Je trouve ça sympa de s’intéresser aux notions bas niveau de PHP. Sur la forme, c’est intéressant mais sur le fond, je ne suis pas fan.

    M’est avis que faire de l’asynchrone server-side via HTTP ne soit pas la meilleure des solutions.

    Lorsque le besoin de communiquer en asynchrone avec d’autres services (http://monsite.com/async/update dans ton exemple) se fait ressentir, on s’oriente vers une architecture de microservice. On est pas obligé d’adhérer à l’architecture microservices à 100% mais ça s’y prête bien.

    Il existe un protocole nommé AMQP qui est utilisé par des services qu’on appelle message broker (mon favori est RabbitMQ).

    Quand tu as besoin de notifier un service d’un évènement (event), tu vas publier cet event via le message broker dans une ou plusieurs queues. Le(s) service(s) qui écoute(nt) la/les queue(s) va/vont consommer ton évènement et le traiter.

    Exemple :

    – Je viens de publier un article dans mon CMS maison
    – Le service HTTP qui a reçu la requête de publication redirige vers la home qui renvoie une 200 au client et envoie cet event : ‘ArticlePublished(id: 32, author_id: 2)’ dans la queue ‘articles’
    – Un service d’envoi de mail qui écoute la queue ‘articles’ va consommer ton event et envoyer un mail à tous tes abonnés (calculer le tps de lecture, faire un digest de tes derniers articles, etc.)
    – Un service d’envoi de notification interne qui écoute aussi la queue ‘articles’ va consommer ton event et envoyer un message Slack dans le channel ‘published-articles’ pour prévenir tes collègues

    Plein d’autres notions sont à garder en tête lorsque l’on parle d’asynchrone : data consistency, failover, serialization, etc. mais le sujet est super intéressant.

    • Bonjour fhuitelec,

      PHP étant assez haut niveau, c’est vite difficile et chiant de faire du bas niveau avec. Et ça ressemble plus à de la bidouille pour en contourner les limites que de la vraie utilisation. Mais c’est intéressant.

      Ce langage n’est en effet pas prévu pour faire de l’asynchrone. Mais si le besoin est ponctuel est assez limité, il m’est facile de penser qu’une telle méthode pour répondre au besoin peut faire le job, si le reste est bien fait.
      Perso, si j’avais à balancer une seule requête asynchrone, je ne m’embêterais pas à installer un node.js ou consorts.
      De plus, cette méthode a pour avantage de fonctionner même avec un petit mutualisé, pratique pour des petites applis.

      J’étais déjà tombé sur le protocole AMQP et la librairie amqplib pour PHP, mais je n’avais pas fouillé, n’ayant pas ce besoin.
      Cela nécessite encore un déploiement coté serveur, que j’essaye d’éviter tant que je peux.

      Mais sûr que c’est intéressant, et bien plus adapté pour ce genre de services.

      Merci.

  5. Lorsqu’on a plusieurs options autant utiliser curl_setopt_array. Sinon très bon article 😉

  6. « Lorsqu’un utilisateur, suivi par 100 personnes, poste un nouveau contenu, comment géreriez-vous cela ? Comment informer les 100 autres utilisateurs, possiblement sur 100 instances différentes, que notre bon utilisateur a posté quelque chose ? Et surtout, sans gêner l’auteur du post. »

    Pourquoi ne pas faire du queuing ? 🙂
    RabbitMQ et co ?

  7. Ah les amateurs de PHP avec leurs bricolages des années 90 😉

    • C’est vrai que ça s’apparente à du bricolage, PHP n’étant pas nativement fait pour ce genre de méthodes.
      90 tout de même, tu bouches le bouchon Maurice Bob 😉

Laisser un commentaire

Votre adresse mail ne sera pas publiée.

*

© 2017 Max-Koder — Propulsé par WordPress

Theme par Anders NorenHaut ↑