Index.xyl 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <overlay xmlns="http://hoa-project.net/xyl/xylophone">
  3. <yield id="chapter">
  4. <p>Le protocole WebSocket permet une communication
  5. <strong>bidirectionnelle</strong> et <strong>full-duplex</strong> entre un
  6. client et un serveur. La bibliothèque <code>Hoa\Websocket</code> permet de
  7. créer des <strong>serveurs</strong> et des <strong>clients</strong>
  8. WebSocket.</p>
  9. <h2 id="Table_of_contents">Table des matières</h2>
  10. <tableofcontents id="main-toc" />
  11. <h2 id="Introduction" for="main-toc">Introduction</h2>
  12. <p>Le protocole WebSocket est <strong>standardisé</strong> dans la
  13. <a href="https://tools.ietf.org/html/rfc6455">RFC6455</a>. Il permet à un
  14. client et à un serveur de communiquer ensemble. Cette communication est
  15. <strong>bidirectionnelle</strong>, cela signifie que le client peut envoyer
  16. des messages au serveur, et <strong>inversement</strong>. Le serveur n'envoie
  17. pas uniquement des réponses il peut envoyer un message
  18. <strong>spontanément</strong>. Cela change des habitudes du Web et de son
  19. protocole HTTP. Le protocole WebSocket est également
  20. <strong>full-duplex</strong>, c'est à dire que les données sont échangées
  21. <strong>simultanément</strong> dans les <strong>deux</strong> sens : ce n'est
  22. pas parce que le serveur a envoyé une donnée qui est en cours d'acheminement
  23. que le client ne peut pas envoyer de données à son tour. Le protocole
  24. WebSocket permet alors une forte <strong>interactivité</strong> entre le
  25. client et le serveur. Le client sera très souvent un navigateur. Notons que le
  26. schéma URI (voir la <a href="https://tools.ietf.org/html/rfc3986">RFC3986</a>)
  27. du protocole WebSocket est <code>ws://</code>.</p>
  28. <p>Nous pouvons nous demander quelles sont les différences entre WebSocket et
  29. EventSource. Ces deux solutions sont en fait fondamentalement différentes :
  30. WebSocket permet une communication bidirectionnelle et full-duplex, alors que
  31. EventSource est une technologie basée sur le <strong>protocole HTTP</strong>
  32. et ne propose qu'une communication <strong>unidirectionnelle</strong>. Pour
  33. cet usage, un serveur EventSource est plus léger, plus simple et conçu pour
  34. être robuste aux déconnexions (voir
  35. <a href="@hack:chapter=Eventsource">la bibliothèque
  36. <code>Hoa\Eventsource</code></a>).</p>
  37. <p>Le protocole WebSocket commence par une phase de
  38. <em lang="en">handshake</em> afin de permettre, par la suite, les échanges de
  39. messages sous forme de <em lang="en">frames</em>.</p>
  40. <h3 id="Handshake_and_challenge" for="main-toc"><em lang="en">Handshake</em>
  41. et <em lang="en">challenge</em></h3>
  42. <p>Pour <strong>démarrer</strong> une communication avec le protocole
  43. WebSocket, le client doit envoyer une requête HTTP au serveur en lui demandant
  44. de changer de protocole. Dans cette requête, le client insère un
  45. <strong><em lang="en">challenge</em></strong>, une sorte de petite énigme, que
  46. le serveur doit résoudre. S'il l'a résolu correctement, alors la communication
  47. démarrera.</p>
  48. <p>Le fait de commencer par une requête HTTP n'est pas anodin. Cela permet au
  49. protocole WebSocket d'emprunter le même chemin que les requêtes HTTP, et
  50. ainsi, par exemple, traverser les proxys, les pare-feu etc. Cela facilite
  51. également le déploiement de ce protocole : pas besoin de lui réserver un port
  52. particulier, pas besoin d'avoir une configuration serveur particulière etc.
  53. Cela permet enfin d'utiliser une connexion <strong>sécurisée</strong>, à
  54. travers TLS. Dans ce cas, nous utilisons le schéma URI
  55. <code>wss://</code>.</p>
  56. <h3 id="Frame_and_opcode" for="main-toc"><em lang="en">Frame</em> et
  57. <em>opcode</em></h3>
  58. <p>Les messages qui sont échangés entre le client et le serveur ne se font pas
  59. verbatim. En réalité, le message est <strong>encapsulé</strong> dans une
  60. <em lang="en">frame</em> : un <strong>paquet</strong> de bits qui a une forme
  61. particulière. Dans ce cas, nous trouverons des informations concernant le
  62. type du message, sa taille, des codes de vérifications etc. Nous trouverons un
  63. schéma explicatif dans la
  64. <a href="https://tools.ietf.org/html/rfc6455#section-5.2">section 5.2,
  65. <em lang="en">Base Framing Protocol</em></a> de la spécification du protocole
  66. pour les plus curieux.</p>
  67. <p>Le <strong>type</strong> du message est appelé <em lang="en">opcode</em>.
  68. C'est l'information la plus importante. Nous retrouverons ce terme dans ce
  69. chapitre plusieurs fois. Des constantes pour chaque <em lang="en">opcode</em>
  70. existent dans la classe <code>Hoa\Websocket\Connection</code> afin de
  71. simplifier leur utilisation. C'est cette classe qui s'assure du
  72. <strong>support</strong> du protocole.</p>
  73. <h3 id="History" for="main-toc">Historique</h3>
  74. <p>Il existe deux versions du protocole WebSocket dans la nature : la version
  75. standard et la version non-standard. La version standard est celle décrite
  76. dans la RFC6455. La dernière version non-standard porte le petit nom de
  77. <em>draft-ietf-hybi-thewebsocketprotocol-00</em> (ou
  78. <em>draft-hixie-thewebsocketprotocol-76</em>), abrégé
  79. <a href="https://tools.ietf.org/wg/hybi/draft-ietf-hybi-thewebsocketprotocol/">Hybi00</a>.
  80. Cette version non-standard a plusieurs problèmes de sécurité importants mais
  81. elle est utilisée dans des langages comme Flash. Heureusement, elle disparaît
  82. de plus en plus et laisse la place à la RFC6455.</p>
  83. <p>La bibliothèque <code>Hoa\Websocket</code> supporte ces deux versions. Elle
  84. permet à des clients supportant des versions différentes du protocole de
  85. communiquer quand même.</p>
  86. <h2 id="Write_a_server" for="main-toc">Écrire un serveur</h2>
  87. <p>La classe <code>Hoa\Websocket\Server</code> permet d'écrire un serveur
  88. <strong>manipulant</strong> le protocole WebSocket. Cette classe hérite de
  89. <code>Hoa\Websocket\Connection</code>. La <strong>communication</strong>
  90. s'effectue à travers un serveur de socket. Nous utiliserons la classe
  91. <code>Hoa\Socket\Server</code> (de <a href="@hack:chapter=Socket">la
  92. bibliothèque <code>Hoa\Socket</code></a>) pour remplir ce rôle.</p>
  93. <p>Le protocole WebSocket fonctionne en TCP, ainsi nous allons démarrer un
  94. serveur WebSocket en local sur le port 8889 :</p>
  95. <pre><code class="language-php">$server = new Hoa\Websocket\Server(
  96. new Hoa\Socket\Server('tcp://127.0.0.1:8889')
  97. );</code></pre>
  98. <p>Toutefois, nous pouvons utiliser l'URI <code>ws://127.0.0.1:8889</code>
  99. directement à la place de <code>tcp://127.0.0.1:8889</code>. Cela a un
  100. avantage lorsque nous utilisons <code>wss://</code> pour une connexion
  101. sécurisée car <code>Hoa\Websocket</code> saura que la connexion devra être
  102. sécurisée et le fera à votre place. Vous n'aurez pas à manipuler TLS, activer
  103. le cryptage sur certaines connections etc. Ainsi :</p>
  104. <pre><code class="language-php">$server = new Hoa\Websocket\Server(
  105. new Hoa\Socket\Server('ws://127.0.0.1:8889')
  106. );</code></pre>
  107. <p>Maintenant, voyons comment <strong>interagir</strong> avec ce serveur.</p>
  108. <h3 id="Listeners" for="main-toc">Écouteurs</h3>
  109. <p>La classe <code>Hoa\Websocket\Connection</code> propose six écouteurs :</p>
  110. <ul>
  111. <li><code>open</code>, quand une connexion est
  112. <strong>ouverte</strong> ;</li>
  113. <li><code>message</code>, quand un <strong>message</strong> est reçu ;</li>
  114. <li><code>binary-message</code>, quand un message <strong>binaire</strong>
  115. est reçu ;</li>
  116. <li><code>ping</code>, quand un <strong>ping</strong> est reçu </li>
  117. <li><code>error</code>, quand une <strong>erreur</strong> s'est
  118. produite ;</li>
  119. <li><code>close</code>, quand une connexion se <strong>ferme</strong>.</li>
  120. </ul>
  121. <p>Pour les écouteurs <code>message</code> et <code>binary-message</code>, il
  122. n'y a qu'une seule donnée associée : <code>message</code>, qui contient sans
  123. surprise le <strong>message</strong> reçu.</p>
  124. <p>Pour l'écouteur <code>ping</code>, nous trouvons aussi la donnée
  125. <code>message</code>. Notons que le pong se fait
  126. <strong>automatiquement</strong> avant de déclencher l'écouteur.</p>
  127. <p>Pour l'écouteur <code>error</code>, nous trouvons la donnée
  128. <code>exception</code> qui contient une <strong>exception</strong> (pas
  129. nécessairement <code>Hoa\Websocket\Exception\Exception</code>, cela peut-être
  130. par exemple <code>Hoa\Socket\Exception</code>). L'écouteur est déclenché après
  131. que la connexion ait été fermée.</p>
  132. <p>L'écouteur <code>close</code> a deux données associées : <code>code</code>
  133. pour le <strong>code</strong> et <code>reason</code> qui explique la
  134. <strong>raison</strong> de cette fermeture avec un message court. Nous
  135. trouverons les codes de fermetures standards sous forme de constantes
  136. <code>CLOSE_<em>*</em></code> dans la classe
  137. <code>Hoa\Websocket\Connection</code>. Par exemple,
  138. <code>Hoa\Websocket\Connection::CLOSE_NORMAL</code> symbolise une fermeture de
  139. connexion normale, sans erreur, alors que
  140. <code>Hoa\Websocket\Connection::CLOSE_MESSAGE_ERROR</code> symbolise une
  141. fermeture de connexion suite à un message mal formé. Cet écouteur est
  142. déclenché après que la connexion ait été fermée.</p>
  143. <h3 id="Send_messages" for="main-toc">Échanges de messages</h3>
  144. <p>Complétons notre exemple pour, dans l'écouteur <code>message</code>,
  145. <strong>renvoyer</strong> au client tous les messages qu'il nous envoie de
  146. façon à créer un <strong>écho</strong>. Pour cela, nous allons utiliser la
  147. méthode <code>Hoa\Websocket\Connection::send</code>. Une fois que notre
  148. écouteur est positionné, nous pouvons démarrer le serveur à l'aide de la
  149. méthode <code>Hoa\Websocket\Connection::run</code>. Ainsi :</p>
  150. <pre data-line="5"><code class="language-php">$server->on('message', function (Hoa\Event\Bucket $bucket) {
  151. $data = $bucket->getData();
  152. echo 'message: ', $data['message'], "\n";
  153. $bucket->getSource()->send($data['message']);
  154. return;
  155. });
  156. $server->run();</code></pre>
  157. <p>Nous allons maintenant tester notre serveur en créant un client HTML très
  158. simple :</p>
  159. <pre data-line="6,39"><code class="language-markup">&amp;lt;input type="text" id="input" placeholder="Message…" />
  160. &amp;lt;hr />
  161. &amp;lt;pre id="output">&amp;lt;/pre>
  162. &amp;lt;script>
  163. var host = 'ws://127.0.0.1:8889';
  164. var socket = null;
  165. var input = document.getElementById('input');
  166. var output = document.getElementById('output');
  167. var print = function (message) {
  168. var samp = document.createElement('samp');
  169. samp.innerHTML = message + '\n';
  170. output.appendChild(samp);
  171. return;
  172. };
  173. input.addEventListener('keyup', function (evt) {
  174. if (13 === evt.keyCode) {
  175. var msg = input.value;
  176. if (!msg) {
  177. return;
  178. }
  179. try {
  180. socket.send(msg);
  181. input.value = '';
  182. input.focus();
  183. } catch (e) {
  184. console.log(e);
  185. }
  186. return;
  187. }
  188. });
  189. try {
  190. socket = new WebSocket(host);
  191. socket.onopen = function () {
  192. print('connection is opened');
  193. input.focus();
  194. return;
  195. };
  196. socket.onmessage = function (msg) {
  197. print(msg.data);
  198. return;
  199. };
  200. socket.onclose = function () {
  201. print('connection is closed');
  202. return;
  203. };
  204. } catch (e) {
  205. console.log(e);
  206. }
  207. &amp;lt;/script></code></pre>
  208. <p>À la ligne 6, nous déclarons l'adresse du serveur WebSocket en utilisant le
  209. protocole <code>ws</code>. À la ligne 45, nous utilisons
  210. l'<a href="https://developer.mozilla.org/docs/WebSockets/WebSockets_reference/WebSocket">objet
  211. <code>WebSocket</code></a>, et nous lui attachons des écouteurs, fortement
  212. semblables à ceux de <code>Hoa\Websocket\Connection</code> !</p>
  213. <p>Pour tester, il suffit de démarrer le serveur :</p>
  214. <pre><code class="language-shell">$ php Server.php</code></pre>
  215. <p>Puis, d'ouvrir le client avec son navigateur préféré. Chaque message
  216. envoyé au serveur nous revient à l'identique, nous avons bien un écho.</p>
  217. <h3 id="Broadcast_messages" for="main-toc">Diffusions de messages</h3>
  218. <p>Pour l'instant, le client parle avec le serveur et le serveur lui répond,
  219. mais ça ne reste qu'un <strong>dialogue</strong>. Le serveur a pourtant toutes
  220. les connexions en mémoire. Nous sommes donc capable de
  221. <strong>diffuser</strong> un message à tous les clients connectés. Pour cela,
  222. nous allons utiliser la méthode
  223. <code>Hoa\Websocket\Connection::broadcast</code> qui va envoyer un message à
  224. tous les autres clients connectés, ainsi :</p>
  225. <pre data-line="5"><code class="language-php">$server->on('message', function (Hoa\Event\Bucket $bucket) {
  226. $data = $bucket->getData();
  227. echo 'message: ', $data['message'], "\n";
  228. $bucket->getSource()->broadcast($data['message']);
  229. return;
  230. });</code></pre>
  231. <p>Et voilà ! C'est aussi simple que ça. Redémarrons le serveur, et ouvrons
  232. plusieurs clients. Chaque message envoyé sera diffusé à <strong>tous</strong>
  233. les autres ! Notre exemple est devenu un outil de <strong>messagerie
  234. instantannée</strong>.</p>
  235. <p>Il faut comprendre que le serveur de socket <code>Hoa\Socket\Server</code>
  236. travaille avec des <strong>nœuds</strong>, c'est à dire un objet qui
  237. représente une connexion ouverte. Dans un écouteur, pour connaître le nœud
  238. <strong>courant</strong> qui a déclenché l'appel à cet écouteur, nous devons
  239. appeler la méthode <code>Hoa\Websocket\Connection::getConnection</code> pour
  240. obtenir le serveur de socket, puis
  241. <code>Hoa\Socket\Server::getCurrentNode</code>. Similairement, nous avons la
  242. méthode <code>Hoa\Socket\Server::getNodes</code> pour obtenir tous les nœuds.
  243. La méthode <code>Hoa\Websocket\Connection::broadcast</code> vient en réalité
  244. de la bibliothèque <code>Hoa\Socket</code> et cache cette complexité. Il est
  245. préférable d'utiliser cette méthode pour des raisons de
  246. <strong>performance</strong> et de compatibilité.</p>
  247. <h3 id="Closing" for="main-toc">Fermeture</h3>
  248. <p>Pour <strong>fermer</strong> la connexion avec le client, nous utilisons la
  249. méthode <code>Hoa\Websocket\Connection::close</code>. Elle est très similaire
  250. à <code>Hoa\Websocket\Connection::send</code>. Ses arguments sont :</p>
  251. <ul>
  252. <li><code>code</code> : le <strong>code</strong> de fermeture, voir les
  253. constantes <code>Hoa\Websocket\Connection::CLOSE_<em>*</em></code>
  254. (<code>CLOSE_NORMAL</code> par défaut) ;</li>
  255. <li><code>reason</code> : un message court expliquant la
  256. <strong>raison</strong> de la fermeture (<code>null</code> par
  257. défaut) ;</li>
  258. <li><code>node</code> : le <strong>nœud</strong> qui va fermer la connexion
  259. (<code>null</code>, par défault, indique le nœud courant).</li>
  260. </ul>
  261. <p>Par exemple, quand nous recevons le message <code>I love you</code>, nous
  262. fermerons la connexion en expliquant pourquoi, sinon nous faisons un simple
  263. écho du message :</p>
  264. <pre><code class="language-php">$server->on('message', function (Hoa\Event\Bucket $bucket) {
  265. $data = $bucket->getData();
  266. if ('I love you' === $data['message']) {
  267. $bucket->getSource()->close(
  268. Hoa\Websocket\Connection::CLOSE_NORMAL,
  269. 'Thank you but my heart is already taken, bye bye!'
  270. );
  271. return;
  272. }
  273. $bucket->getSource()->send($data['message']);
  274. return;
  275. });</code></pre>
  276. <p>Nous pouvons modifier notre client pour qu'il nous affiche le code et la
  277. raison d'une fermeture :</p>
  278. <pre><code class="language-javascript"> socket.onclose = function (e) {
  279. print(
  280. 'connection is closed (' + e.code + ' ' +
  281. (e.reason || '—no reason—') + ')'
  282. );
  283. return;
  284. };</code></pre>
  285. <p>Il est préférable de <strong>toujours</strong> utiliser cette méthode pour
  286. fermer une connexion plutôt que de fermer directement la connexion TCP.</p>
  287. <h2 id="Message" for="main-toc">Message</h2>
  288. <p>Nous avons deux façons d'envoyer des messages : soit en un seul morceau si
  289. nous avons le message en <strong>entier</strong>, soit en
  290. <strong>plusieurs</strong> morceaux. Notre message peut aussi contenir autre
  291. chose que du texte, il peut contenir une donnée <strong>binaire</strong>. Dans
  292. ce cas, nous parlons de message binaire.</p>
  293. <h3 id="Fragmentation" for="main-toc">Fragmentation</h3>
  294. <p>Pour envoyer un message en un seul bloc, nous utilisons la méthode
  295. <code>Hoa\Websocket\Connection::send</code> comme nous l'avons vu dans les
  296. sections précédentes :</p>
  297. <pre><code class="language-php">$server->on('message', function (Hoa\Event\Bucket $bucket) {
  298. $bucket->getSource()->send('foobar');
  299. return;
  300. });</code></pre>
  301. <p>Cette méthode comporte en réalité quatre arguments :</p>
  302. <ul>
  303. <li><code>message</code> : le <strong>message</strong> ;</li>
  304. <li><code>node</code> : le <strong>nœud</strong> qui va envoyer le message
  305. (<code>null</code>, par défaut, indique le nœud courant) ;</li>
  306. <li><code>opcode</code> : l'<strong><em>opcode</em></strong>, c'est à dire
  307. le type de la <em lang="en">frame</em>
  308. (<code>Hoa\Websocket\Connection::OPCODE_TEXT_FRAME</code> par défaut) ;</li>
  309. <li><code>fin</code> : indique si le message est <strong>terminé</strong> ou
  310. pas (<code>true</code> par défaut).</li>
  311. </ul>
  312. <p>Nous allons utiliser tous les arguments en essayant d'envoyer un message
  313. <strong>fragmenté</strong>.</p>
  314. <p>Dans notre exemple, nous avons envoyé un message en entier, ce qui est le
  315. cas le plus courant. Si nous envoyons un très long message, nous utiliserons
  316. également cette même méthode. Toutefois, il peut arriver que nous ayons le
  317. message morceau après morceau et nous sommes alors incapable de l'envoyer en
  318. entier. Par exemple, si le message, de taille <strong>indéterminée</strong>,
  319. est lu sur un flux et que nous voulons ensuite l'envoyer au client, nous
  320. n'allons pas attendre d'avoir tout le message : nous allons envoyer chaque
  321. morceau directement au client. Dans ce cas, nous parlons de messages
  322. fragmentés.</p>
  323. <p>Nous allons utiliser les deux <em>opcodes</em> suivants :
  324. <code>OPCODE_TEXT_FRAME</code> pour le <strong>premier</strong> fragment, puis
  325. <code>OPCODE_CONTINUATION_FRAME</code> pour tous les
  326. <strong>suivants</strong>. À chaque fois, nous allons préciser que le message
  327. n'est pas <strong>terminé</strong> à l'aide de l'argument <code>fin</code> qui
  328. sera à <code>false</code>, sauf pour le dernier fragment où <code>fin</code>
  329. sera à <code>true</code>.</p>
  330. <p>L'utilisateur final derrière le client ne recevra pas des messages
  331. fragmentés, mais le message en entier une fois que le dernier fragment aura
  332. été reçu. Côté serveur, cela nous évite de surcharger la mémoire avec des
  333. données « en transit » et aussi de surcharger le réseau avec un gros message.
  334. Nous envoyons les données dès que nous les avons et c'est le client qui
  335. s'occupe de <strong>reconstituer</strong> le message. Le serveur opère de la
  336. même façon lorsqu'il reçoit un message fragmenté. Entre deux fragments, le
  337. serveur peut aussi traiter d'autres tâches. Il est donc plus intéressant
  338. d'utiliser les fragments plutôt que de temporiser le message.</p>
  339. <p>Passons à un exemple. Nous allons envoyer le message <code>foobarbaz</code>
  340. fragmenté en trois parties. Nous pouvons imaginer que nous lisons ces données
  341. sur une socket par exemple, et que les données viennent au fur et à mesure.
  342. Ainsi :</p>
  343. <pre><code class="language-php">$server->on('message', function (Hoa\Event\Bucket $bucket) {
  344. $self = $bucket->getSource();
  345. $self->send(
  346. 'foo',
  347. null,
  348. Hoa\Websocket\Connection::OPCODE_TEXT_FRAME,
  349. false // not the end…
  350. );
  351. echo 'sent foo', "\n";
  352. sleep(1);
  353. $self->send(
  354. 'bar',
  355. null,
  356. Hoa\Websocket\Connection::OPCODE_CONTINUATION_FRAME,
  357. false // not the end…
  358. );
  359. echo 'sent bar', "\n";
  360. sleep(1);
  361. $self->send(
  362. 'baz',
  363. null,
  364. Hoa\Websocket\Connection::OPCODE_CONTINUATION_FRAME,
  365. true // the end!
  366. );
  367. echo 'sent baz, over', "\n";
  368. return;
  369. });</code></pre>
  370. <p>Les instructions <a href="http://php.net/sleep"><code>sleep</code></a>
  371. permettent d'émuler une latence réseau ou quelque chose du genre. À chaque
  372. appel de la méthode <code>send</code>, les données sont
  373. <strong>effectivement</strong> envoyées au client, ce n'est pas un tampon côté
  374. serveur.</p>
  375. <h3 id="Encoding" for="main-toc">Encodage</h3>
  376. <p>Tous les messages échangés doivent être au format <strong>UTF-8</strong>
  377. (voir la <a href="https://tools.ietf.org/html/rfc3629">RFC3629</a>). Si les
  378. messages provenant du client ne sont pas conformes,
  379. <code>Hoa\Websocket\Connection</code> fermera la connexion de façon appropriée
  380. avec le code <code>Hoa\Websocket\Connection::CLOSE_MESSAGE_ERROR</code>, nous
  381. n'avons rien à faire de spécial. Par conséquent, tous les messages reçus dans
  382. nos écouteurs sont au bon encodage.</p>
  383. <p>En revanche, <code>Hoa\Websocket\Connection</code> vérifie que les messages
  384. à <strong>destination</strong> du client sont dans le bon encodage. Si
  385. l'encodage n'est pas approprié, alors une exception
  386. <code>Hoa\Websocket\Exception\InvalidMessage</code> sera levée, ce qui fermera
  387. la connexion et déclenchera l'écouteur <code>error</code> si elle n'est pas
  388. capturée à temps.</p>
  389. <h3 id="Binary" for="main-toc">Binaire</h3>
  390. <p>Il est également possible d'envoyer des données <strong>binaires</strong>,
  391. bien plus <strong>compactes</strong> que des données textuelles. Nous parlons
  392. alors de messages binaires. Nous allons toujours utiliser la méthode
  393. <code>Hoa\Websocket\Connection::send</code> mais avec l'<em>opcode</em>
  394. <code>OPCODE_BINARY_FRAME</code>. Cela n'a de sens que dans l'écouteur
  395. <code>binary-message</code>, c'est à dire dans un « échange binaire » entre
  396. le client et le serveur. Nous pouvons imaginer le client qui envoie des
  397. coordonnées et le serveur qui lui en redonne d'autres (échange fort probable
  398. pour un jeu de plateau par exemple) :</p>
  399. <pre><code class="language-php">$server->on('binary-message', function (Hoa\Event\Bucket $bucket) {
  400. $data = $bucket->getData();
  401. $message = $data['message'];
  402. $point = [];
  403. list($point['x'], $point['y']) = array_values(unpack('nx/ny', $message));
  404. // compute a next point.
  405. $bucket->getSource()->send(
  406. pack('nn', $point['x'], $point['y']),
  407. null,
  408. Hoa\Websocket\Connection::OPCODE_BINARY_FRAME
  409. );
  410. return;
  411. });</code></pre>
  412. <p>Les fonctions <a href="http://php.net/pack"><code>pack</code></a> et
  413. <a href="http://php.net/unpack"><code>unpack</code></a> seront des alliés
  414. précieux dans ce cas.</p>
  415. <p>Notons que les messages binaires peuvent également être
  416. <strong>fragmentés</strong>. Il faut utiliser l'<em>opcode</em>
  417. <code>OPCODE_BINARY_FRAME</code> à la place de <code>OPCODE_TEXT_FRAME</code>
  418. puis continuer avec <code>OPCODE_CONTINUATION_FRAME</code> comme nous l'avons
  419. appris.</p>
  420. <h2 id="Write_a_client" for="main-toc">Écrire un client</h2>
  421. <p>La classe <code>Hoa\Websocket\Client</code> permet d'écrire un client
  422. <strong>manipulant</strong> le protocole WebSocket. Cette classe hérite de
  423. <code>Hoa\Websocket\Connection</code>, tout comme
  424. <code>Hoa\Websocket\Server</code>. La <strong>communication</strong>
  425. s'effectue à travers un client de socket. Nous utiliserons la classe
  426. <code>Hoa\Socket\Client</code> (de <a href="@hack:chapter=Socket">la
  427. bibliothèque <code>Hoa\Socket</code></a>) pour remplir ce rôle.</p>
  428. <p>Autant <code>Hoa\Websocket\Server</code> est capable de traiter avec des
  429. clients supportant plusieurs versions du protocole WebSocket, autant
  430. <code>Hoa\Websocket\Client</code> utilise <strong>uniquement</strong> le
  431. protocole de la RFC6455. C'est à dire que le client n'est capable de parler
  432. qu'avec un serveur supportant la RFC6455.</p>
  433. <h3 id="Start_a_connection_with_the_server" for="main-toc">Démarrer une
  434. connexion avec le serveur</h3>
  435. <p>Comme pour le serveur, le client hérite de
  436. <code>Hoa\Websocket\Connection</code>. Nous retrouvons alors les mêmes
  437. méthodes <code>send</code>, <code>close</code>, <code>run</code> etc., ainsi
  438. que les mêmes écouteurs. Et comme pour le serveur, les écouteurs doivent être
  439. positionnés sur le client.</p>
  440. <p>Ainsi, pour démarrer un client, nous écrirons :</p>
  441. <pre><code class="language-php">$client = new Hoa\Websocket\Client(
  442. new Hoa\Socket\Client('ws://127.0.0.1:8889')
  443. );
  444. $client->on('message', function (Hoa\Event\Bucket $bucket) {
  445. $data = $bucket->getData();
  446. echo 'received message: ', $data['message'], "\n";
  447. return;
  448. });</code></pre>
  449. <p>Le client peut fonctionner en mode <em lang="en">loop</em>, comme le
  450. serveur, avec la méthode <code>run</code>. Dans ce cas, nous devrons
  451. écrire :</p>
  452. <pre><code class="language-php">$client->run();</code></pre>
  453. <p>Ou alors, pour un échange séquentiel, nous devons appeler manuellement la
  454. méthode <code>Hoa\Websocket\Client::connect</code> :</p>
  455. <pre><code class="language-php">$client->connect();</code></pre>
  456. <p>Si le serveur ne supporte pas le bon protocole, une exception
  457. <code>Hoa\Websocket\Exception\BadProtocol</code> sera levée.</p>
  458. <h3 id="Send_and_broadcast_messages" for="main-toc">Échanges et
  459. diffusions de messages</h3>
  460. <p>Pour envoyer un message, nous utiliserons la méthode
  461. <code>Hoa\Websocket\Connection::send</code>. Son fonctionnement a été décrit
  462. précédemment pour le serveur. Il est identique.</p>
  463. <p><em>A contrario</em>, pour recevoir un message, nous utiliserons la méthode
  464. <code>Hoa\Websocket\Client::receive</code>. Les messages reçus du serveur vont
  465. déclencher les écouteurs. Ainsi, nous allons envoyer un message au serveur,
  466. puis nous allons attendre une réponse, ceci deux fois de suite :</p>
  467. <pre><code class="language-php">$client->send('foobar');
  468. $client->receive();
  469. $client->send('bazqux');
  470. $client->receive();
  471. $client->close();</code></pre>
  472. <p>La méthode <code>Hoa\Websocket\Client::receive</code> n'a aucun
  473. argument.</p>
  474. <h3 id="Note_about_the_handshake_and_the_host" for="main-toc">Précision sur le
  475. <em lang="en">handshake</em> et l'hôte</h3>
  476. <p>Pour que le <em lang="en">handshake</em> soit complet, il est nécessaire
  477. d'envoyer l'en-tête HTTP <code>Host</code>, représentant le nom de l'hôte.
  478. Lorsque le client est exécuté à travers un serveur HTTP, l'hôte de ce serveur
  479. sera utilisé s'il est disponible. Sinon, s'il n'est pas disponible, ou si nous
  480. exécutons le client en ligne de commande par exemple, nous devons préciser un
  481. hôte avec la méthode <code>Hoa\Websocket\Client::setHost</code>, avant de
  482. connecter le client (avant l'appel de
  483. <code>Hoa\Websocket\Connection::run</code> ou
  484. <code>Hoa\Websocket\Client::connect</code>) ; ainsi :</p>
  485. <pre><code class="language-php">$client->setHost('hoa-project.net');
  486. $client->connect();
  487. // …</code></pre>
  488. <p>Pour savoir si l'hôte est connu, nous pouvons utiliser la méthode
  489. <code>Hoa\Websocket\Client::getHost</code>. Elle retournera <code>null</code>
  490. si le nom de l'hôte est introuvable. Ou sinon, au moment du
  491. <em lang="en">handshake</em>, une exception
  492. <code>Hoa\Websocket\Exception\Exception</code> sera levée.</p>
  493. <h2 id="Customized_node" for="main-toc">Nœud personnalisé</h2>
  494. <p>Les classes <code>Hoa\Socket\Server</code> et
  495. <code>Hoa\Socket\Client</code> travaillent avec des <strong>nœuds</strong> :
  496. des objets qui représentent une <strong>connexion</strong> ouverte. La classe
  497. de base pour représenter un nœud est <code>Hoa\Socket\Node</code>. La
  498. bibliothèque <code>Hoa\Websocket</code> propose son propre nœud :
  499. <code>Hoa\Websocket\Node</code>. Nous pouvons encore <strong>étendre</strong>
  500. cette classe pour ajouter et manipuler des <strong>informations</strong> sur
  501. une connexion.</p>
  502. <p>Par exemple, dans le cas d'une messagerie, nous pourrions stocker le pseudo
  503. du client :</p>
  504. <pre><code class="language-php">class ChatNode extends Hoa\Websocket\Node
  505. {
  506. protected $_pseudo = null;
  507. public function setPseudo ($pseudo)
  508. {
  509. $old = $this->_pseudo;
  510. $this->_pseudo = $pseudo;
  511. return $old;
  512. }
  513. public function getPseudo()
  514. {
  515. return $this->_pseudo;
  516. }
  517. }</code></pre>
  518. <p>Pour préciser au serveur ou au client de socket d'<strong>utiliser</strong>
  519. notre classe de nœud, nous devons utiliser la méthode
  520. <code>Hoa\Socket\Server::setNodeName</code> ou
  521. <code>Hoa\Socket\Client::setNodeName</code> de cette manière :</p>
  522. <pre><code class="language-php">$server = new Hoa\Websocket\Server(
  523. new Hoa\Socket\Server('ws://127.0.0.1:8889')
  524. );
  525. $server->getConnection()->setNodeName('ChatNode');</code></pre>
  526. <p>Et après, dans nos écouteurs, nous pourrons utiliser notre méthode
  527. <code>getPseudo</code> par exemple :</p>
  528. <pre><code class="language-php">$server->on('message', function (Hoa\Event\Bucket $bucket) {
  529. $node = $bucket->getSource()->getConnection()->getCurrentNode();
  530. var_dump($node->getPseudo());
  531. // …
  532. });</code></pre>
  533. <p>Si vous mettez en place un <strong>protocole</strong> en utilisant le canal
  534. des WebSockets entre vos clients et votre serveur, les nœuds personnalisés
  535. seront très utiles pour stocker quelques informations récurrentes.</p>
  536. <h2 id="Conclusion" for="main-toc">Conclusion</h2>
  537. <p>La bibliothèque <code>Hoa\Websocket</code> permet de créer des
  538. <strong>serveurs</strong> et des <strong>clients</strong> WebSocket pour plus
  539. d'<strong>interactivité</strong> dans vos applications. Le serveur est
  540. facilement extensible avec la notion de <strong>nœud</strong>, qui facilite le
  541. stockage et la manipulation de données utiles pour créer son propre
  542. protocole.</p>
  543. </yield>
  544. </overlay>