Créer un système de messagerie instantanée avec Ruby, MQTT et Ncurses
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
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.