Fork me on GitHub
… ou une occasion d’apprendre

Créer un serveur TCP multithread en Ruby

Posté le 15 septembre 2007 à 11:00

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 :

server = TCPServer.new(’127.0.0.1′, 10001)
socket = server.accept

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) :

require ‘gserver’

#
# 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 :

require ‘tserver’

# 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)

Articles relatifs





5 Responses to “Créer un serveur TCP multithread en Ruby”

  1. Yoan Blanc

    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())
    reactor.run()

  2. Yoan Blanc

    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.

  3. Yoan Blanc

    Pour 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.

  4. Yann Lugrin

    Tu 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

  5. Yann Lugrin

    Bien, 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.

Envoyer une réponse

Ce site supporte OpenID, vous pouvez vous authentifier en entrant votre identité dans le champ adéquat mais ceci n'est pas obligatoire.

Si vous ne vous authentifiez pas avec OpenID le nom et l'email sont requis. Si vous vous authentifiez pour la première fois, les renseigner permet de vous assurer des informations qui seront associées à votre profile (si il existe déjà elles seront mises à jour).