Follow me
RSS feed
My sources
My Viadeo

Créer un système de messagerie instantanée avec Ruby, MQTT et Ncurses

Greg | 12 Nov 2011

Projets Non, vous ne vous êtes pas trompé (et moi non plus) ! Ce post a presque le même titre que le précédent. En effet, je vous propose de refaire notre système de messagerie instantanée, mais en remplaçant AMQP par MQTT. Pour cela, nous utiliserons la gem ruby-mqtt et mosquitto comme broker.

MQTT est, tout comme AMQP, un protocole ouvert de messagerie inter-applicatif. Il est cependant beaucoup plus simple d'AMQP. En effet, il n'existe pas de notion d'exchange. Il n'est pas non plus possible d'envoyer des messages qui pourront être "conservés" par le broker afin d'être récupérés plus tard par des clients non connectés.

Un client MQTT va s'inscrire auprès d'une ou plusieurs queues pas la suite il pourra récupérer les messages envoyés à ces queues. De même, il pourra envoyer des messages à des queues, qu'il s'y soit inscrit ou non.

Voyons cela sur un petit exemple. En premier, le client :

 1 require 'rubygems'
 2 require 'mqtt/client'
 3 
 4 mqtt = MQTT::Client.new('localhost')
 5 mqtt.connect do |client|
 6    client.subscribe('my/queue')
 7 
 8    topic, message = client.get
 9    puts "#{topic} : #{message}"
10    client.disconnect
11 end

Nous commençons par créer un client MQTT (ligne 4) et nous établissons une connexion avec le broker (ligne 5). Nous nous inscrivons ensuite auprès d'une queue nommée my/queue (ligne 6). La méthode get (ligne 8) permet d'attendre la réception d'un message. En retour nous recevons un tableau dont le premier élément est le topique, soit le nom de la queue via laquelle nous recevons le message. Le second élément du tableau est le message lui même. Une fois le message reçu et affiché, nous quittons proprement en nous déconnectant (ligne 9).

La partie serveur est tout aussi simple :

 1 require 'rubygems'
 2 require 'mqtt/client'
 3 
 4 message = ARGV[0]
 5 
 6 mqtt = MQTT::Client.new('localhost')
 7 mqtt.connect do |client|
 8    puts "Send message #{message}"
 9    client.publish "my/queue", message
10 end

Comme pour le client, nous commençons par créer un client MQTT et nous nous connectons au broker (ligne 6 et 7). Pour envoyer un message vers une queue nous utilisons la méthode publish avec comme premier paramètre le nom de la queue et en second, le message à envoyer.

Vous savez maintenant le principal... Nous pouvons écrire notre système de chat :

  1 #!/usr/bin/env ruby
  2 
  3 require "rubygems"
  4 require "ncurses"
  5 require 'mqtt/client'
  6 
  7 class ChatGui
  8    def read_line(y, x,
  9                  window     = Ncurses.stdscr,
 10                  max_len    = (window.getmaxx - x - 1), 
 11                  string     = "", 
 12                  cursor_pos = 0)
 13       loop do
 14          window.mvaddstr(y,x,string)
 15          window.move(y,x+cursor_pos)
 16          ch = window.getch
 17          case ch
 18          when Ncurses::KEY_ENTER, ?\n.ord, ?\r.ord
 19             return string
 20          when Ncurses::KEY_BACKSPACE, 127
 21             string = string[0...([0, cursor_pos-1].max)] + string[cursor_pos..-1]
 22             cursor_pos = [0, cursor_pos-1].max
 23             window.mvaddstr(y, x+string.length, " ")
 24          when (" "[0].ord..255)
 25             if (cursor_pos < max_len)
 26                string[cursor_pos,0] = ch.chr
 27                cursor_pos += 1
 28             else
 29                Ncurses.beep
 30             end 
 31          else
 32             Ncurses.beep
 33          end 
 34       end 
 35    end
 36 
 37    def add_message(message)
 38       @messages += message.split("\n")
 39       if @messages.size > @max_messages
 40          @messages.shift
 41       end
 42 
 43       refresh_messages_window
 44    end
 45 
 46    def refresh_messages_window
 47       @messages_window.clear
 48       y = 0
 49       @messages.each do |message|
 50          @messages_window.mvaddstr(y, 0, message)
 51          y = y + 1
 52       end
 53       @messages_window.refresh
 54    end
 55 
 56    def initialize(nick)
 57       @messages = []
 58       Ncurses.initscr
 59       Ncurses.cbreak
 60       Ncurses.noecho
 61       Ncurses.keypad(Ncurses.stdscr, true)
 62 
 63       @window = Ncurses.stdscr
 64       @maxy = @window.getmaxy - 1
 65       @maxx = @window.getmaxx - 1
 66 
 67       @prompt_window = Ncurses.newwin(2, @maxx, @maxy - 2, 0)
 68       @prompt = "#{nick} >"
 69 
 70       @messages_window = Ncurses.newwin(@maxy - 2, @maxx, 0, 0)
 71       @max_messages = @messages_window.getmaxy
 72    end
 73 
 74    def run(&b)
 75       loop do
 76          # refresh_messages_window
 77 
 78          @prompt_window.mvaddstr(0, 0, "-"*@maxx)
 79          @prompt_window.mvaddstr(1, 0, @prompt)
 80          message = read_line(1, @prompt.length + 1, @prompt_window)
 81          yield message
 82          @prompt_window.clear
 83       end
 84    end
 85 
 86    def quit
 87       Ncurses.endwin
 88    end
 89 end
 90 
 91 # -- main --
 92 
 93 def help(gui)
 94    gui.add_message <<EOH
 95    /help : display this help
 96    /me <message> : send an action message
 97    /privmsg <nickname> <message> : send a private message
 98    /quit : Quit AMQP chat
 99    /who : ask who is here
100 EOH
101 end
102 
103 nickname = ARGV[0]
104 raise "Usage : #{$0} <nickname>" if nickname.nil?
105 
106 gui = ChatGui.new(nickname)
107 mqtt = MQTT::Client.new('localhost')
108 mqtt.connect do |client|
109    client.subscribe('chat/public')
110    client.subscribe('chat/system')
111    client.subscribe("chat/private/#{nickname}")
112 
113    client.publish 'chat/public', "** #{nickname} enter"
114 
115    Thread.new do 
116       gui.run do |message|
117          case message
118          when /^\/quit\s*(.*)/
119             client.publish 'chat/public', "** #{nickname} has quit (#{$1})"
120             client.disconnect
121             gui.quit
122             exit 1
123          when /^\/privmsg\s*([^\s]*)\s*(.*)/
124             client.publish "chat/private/#{$1}", ">> [#{Time.now.strftime('%H:%M:%S')}] #{nickname} : #{$2}"
125             client.publish "chat/private/#{nickname}", ">> [#{Time.now.strftime('%H:%M:%S')}] #{nickname} : #{$2}"
126          when /^\/who/
127             client.publish "chat/system", nickname
128          when /^\/help/
129             help gui
130          else
131             client.publish 'chat/public', "[#{Time.now.strftime('%H:%M:%S')}] #{nickname} : #{message}"
132          end
133       end
134    end
135 
136    loop do
137       topic,message = client.get
138       if topic =~ /chat\/system/
139          client.publish "chat/private/#{message}", "-- #{nickname} is here"
140       else
141          gui.add_message message
142       end
143    end
144 end

Comme d'habitude, vous pouvez récupérer le code ici.

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.