Créer un serveur TCP multithread en Ruby
Suite à une discussion avec un ami, qui voulait une estimation du coût de développement d’une petite application permettant de recevoir des données par une simple connections TCP ne demandant pas d’implémenter un protocole “complexe” dans le client, j’ai effectué quelques recherches sur l’implémentation d’un serveur TCP en Ruby.
Bien sur créer un serveur TCP acceptant des connections tient en 2 lignes :
Mais un serveur comme celui-ci est un peu limité (surtout quand il doit accepter 2000 connections toutes les deux minutes), la suite est logiquement de trouver une solution multithread permettant de recevoir plusieurs connections simultanément. On trouve GServer dans la libraire standard de Ruby qui permet d’implémenter ceci très facilement (exemple tiré de la documentation) :
Si cette solution fonctionne mais j’ai tout de même rencontré quelques problèmes, pour commencer la méthode “shutdown” ne fonctionne pas correctement et ce serveur crée un nouveau Thread à chaque connexion et le détruit une fois les traitements de celle-ci terminé. Et vu que j’étais intéressé par faire quelques expériences avec les Thread de Ruby j’ai décidé de créer mon propre serveur multithread avec les spécifications suivantes :
- Thread persistants en attente de connexions.
- Gracefull shutdown.
Ce petit teste à finalement donnée naissance à une librairie complète, la classe TServer permet maintenant de créer un serveur multithread pouvant être arrêté en douceur et dont les listener (Thread traitant les connections) sont persistants. Lors de l’initialisation il est possible de configuré le nombre maximum de connections que le serveur peut traiter en même temps et le nombre de listener qui doivent toujours être à l’écoute (il est possible de configurer cette variable à 0 pour avoir un comportement identique à GServer).
L’utilisation des Thread est intéressant, mais demande de faire attention à ce qu’on fait (le contenu des variables n’est pas toujours celui auquel on s’attend) et à bien utiliser les outils de synchronisation à notre disposition (Monitor, Mutex, ConditionVariable, Queue). Faire des testes unitaires n’est pas évident puisqu’il est difficile de tester un serveur dont on ne connait pas l’état actuel. J’ai pu m’en sortir en contrôlant l’évolution du serveur avant de faire certains testes et en utilisant la classe Timeout pour m’assurer que son état change dans un temps raisonnable. Je ne suis pas complètement satisfait du résultat et j’ai probablement encore des choses à découvrir pour le faire correctement (pour l’instant je n’arrive pas a faire passer les testes sous Windows, apparemment ils attendent sur quelque chose lors d’un test et je doit tuer le process).
Mon prochain objectif est de remplacer le système de log (utilisation de la librairie logger et utilisations de diverses méthodes afin de permettre la collecte de stats plus complète comme dans GServer), mais également de permettre un reload du serveur sans interruption du service. En attendant, voici un exemple d’implémentation de TServer (tiré de la documentation) dont j’ai mis les sources et un gem à disposition ici :
Je me suis amusé à faire son petit frère en Python (non pas pour troller, mais juste pour voir le comment c’est possible de faire). C’était très instructif, merci Yann.
#!/usr/bin/env python
import sys
from time import time
from twisted.python import log
from twisted.internet import reactor
from twisted.protocols.basic import LineReceiver
from twisted.internet.protocol import ClientFactory
log.startLogging(sys.stdout)
class Time(LineReceiver):
def lineReceived(self, line):
response = “%f > %s” % (time(), line)
if “quit” in line:
reactor.stop()
else:
log.msg(response)
self.sendLine(response)
class TimeFactor(ClientFactory):
protocol = Time
reactor.listenTCP(10001, TimeFactor())
15 septembre 2007 à 23:11reactor.run()
Bon, l’indentation a pêté comme les guillemets. La foule de from twisted donne un peu la nausée, ça rend juste le code en dessous plus “clair”.
Au passage, y’a une faute d’erreur dans le titre.
15 septembre 2007 à 23:19Pour continuer mon monologue, tes exemples ne fonctionnant pas chez moi :’-/ (oui j’ai fait le code .py avant même d’essayer) tu as fait comment ? tserver a apparemment été renommé en prefork et la dernière mise à jour date de bien deux ans et demi, arrgh une page en jap. Merci.
16 septembre 2007 à 0:14Tu veux parler de gserver pour le premier exemple ?
GServer est dans la librairie standard de Ruby, il fonctionne très bien chez moi.
Si tu parle de TServer, c’est moi qui l’ai écris, mais va falloir que je change le nom car j’avais regardé que dans la base standard de gem si il existait une lib avec celui-ci… Pffff… je manque d’imagination pour ça.
Je voie que j’ai zappé le lien pour ma lib…
http://dev.sans-savoir.net/trac/tserver
le temps de changer de nom, y a un GEM mais les testes ne passe pas sous windows (le trunk du SVN passe lui).
Donc mon serveur est basé sur les Threads, comme GServer, mais il est plus bien :D
16 septembre 2007 à 0:33Bien, je viens de mettre un gem 0.1.1 dont les testes passent sous Windows et Linux.
En fait TServer a été renommé PreFork, je pourrais donc garder ce nom peut-être :D
Sinon, merci pour l’exemple Python, c’est toujours intéressant.
16 septembre 2007 à 0:47