TServer 0.2.0
TServer est une librairie en Ruby permettant d’implémenter un serveur multi-thread, c’est une alternative à GServer de la librairie standard.
J’ai sorti la version 0.2.0 aujourd’hui, voici les nouveautés :
- Utilisation de la class ‘Logger’ pour le système de log (la sortie standard est utilisé par défaut, mais lors des tests un fichier de log est utilisé).
- Ajout de callbacks lors des événements.
- Ajout d’une tâche ’server’ au Rakefile afin de lancer le serveur d’exemple.
- Plurialisation du nom des méthodes ‘Tserver.listeners’ et ‘TServer.waiting_listeners’.
- Une méthode ‘reload’ a été ajouté afin de permettre de redémarrer le serveur sans coupure du service.
- ‘TServer.process’ ne prend plus d’argument et les méthodes suivantes sont maintenant disponibles : ‘TServer.connection’, ‘TServer.connection_addr’ et ‘TServer.terminate_listener?’.
- Un certain nombre de corrections ont étés faites et les tests améliorés.
Les sources et un gem sont disponibles depuis le trac du projet, j’espère pouvoir ajouter le gem à rubyforge bientôt.
Le serveur fonctionne bien, bien que je ne l’ai pas encore testé dans des conditions réels. Je pense que je devrais faire une class pour les Listener, ceci permettrais d’être plus facilement “thread safe” en plus d’un certain nombre davantage pour l’implémentation je pense. Par contre, j’ai de la peine à évaluer les incidences sur les performances. Ce changement serais important, je vais y réfléchir et décider si je prends cette option dans le but de sortir une version 1.0.
Naturellement tout commentaire est le bienvenu.
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) :
#
# A server that returns the time in seconds since 1970.
#
class TimeServer < GServer
def initialize(port=10001, *args)
super(port, *args)
end
def serve(io)
io.puts(Time.now.to_i)
end
end
# Run the server with logging enabled (it's a separate thread).
server = TimeServer.new
server.audit = true # Turn logging on.
server.start
server.join
# *** Now point your browser to http://localhost:10001 to see it working ***
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 :
# A server can return
class ExempleServer < TServer
def serve(conn)
conn.each do |line|
break if line =~ /(quit|exit|close)/
log '> ' + line.chomp
conn.puts Time.now.to_s + '> ' + line.chomp
end
end
end
# Create the server with logging enabled (server activity is displayed
# in console with received data)
server = ExempleServer.new
server.verbose = true
# Shutdown the server when script is interupted
Signal.trap('SIGINT') do
server.shutdown
end
# Start the server (joined is set to true and the line wait on server
# thread before continue, the default values of this parameter is set to
# false, you can also use 'server.join' after server.start)
server.start(true)
# Now you can open a telnet connection to 127.0.0.1:10001 (telnet 127.0.0.1 10001)
# and send text (use exit to close the connection)