Receita: Monitorando Sockets com Tkinter
Seguindo a mesma idéia da outra receita, podemos usar o loop também da Tk para monitorar a entrada ou saída de dados em sockets e arquivos. Infelizmente, a Tk não tem uma interface de alto nível, como a que existe em GTK, o que exige um pouco mais de trabalho.
Para isso, eu criei uma classe, mix-in, que fornece a interface, com a API idêntica à da GTK, permitindo obter os mesmos resultados. Basta adicionar esta classe às classes base usadas pelo seu aplicativo. Segue abaixo a classe e um exemplo:
Código
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3
4 import select
5 import socket
6 import itertools
7
8 # Algumas constantes usadas
9
10 INPUT_READ = 0
11 INPUT_WRITE = 1
12 INPUT_EXCEPTION = 2
13
14 _read = {}
15 _write = {}
16 _error = {}
17
18 _modes = [_read, _write, _error]
19 _select = select.select
20
21 _tags = itertools.count()
22
23 class _Wrapper(object):
24 # Classe wrapper para os sockets ou arquivos usados
25
26 def __init__(self, source, callback):
27 self.source = source
28 self.fileno = source.fileno
29 self.close = source.close
30 self.callback = callback
31
32
33 class TkSelectMixIn:
34 # Classe wrapper para select.select().
35
36 def input_add(self, source, mode, callback):
37 # Insere um objeto na pilha e retorna um identificador
38 #
39 # source é o objeto, normalmente um socket
40 #
41 # mode é uma das constantes INPUT_READ, INPUT_WRITE ou
42 # INPUT_EXCEPTION
43 #
44 # callback é o método a ser chamado quando a condição
45 # monitorada for satisfeita
46
47 if mode not in (0, 1, 2):
48 raise ValueError("modo inválido")
49
50 tag = _tags.next()
51 _modes[mode][tag] = _Wrapper(source, callback)
52
53 return tag
54
55 def input_remove(self, tag):
56 # Remove um objeto da pilha. key é o identificador retornado
57 # quando o objeto foi inserido
58 # Note que o socket NÃO É FECHADO, apenas removido
59
60 for mode in _modes:
61 if tag in mode:
62 mode.pop(tag)
63 break
64
65 def _check(self):
66 # Verifica todos os sockets sendo usados e remove quaisquer
67 # que estejam com problemas
68
69 for mode in _modes:
70 for tag, source in mode.iteritems():
71 try:
72 _select([source], [source], [source], 0)
73 except: # encontramos o vilão
74 mode.remove(tag)
75 source.close()
76
77 def _select(self,
78 # Essa declaração estranha tem uma finalidade. Armazenar
79 # as globais no namespace local, acelerando a consulta
80 # de nomes, permitindo que este método seja executado o
81 # mais rápido possível.
82 _read=_read,
83 _write=_write,
84 _error=_error,
85 _select=_select):
86
87
88 while 1:
89
90 # tentamos o select até não ocorrer erros
91 try:
92 ready = _select(_read.values(), _write.values(),
93 _error.values(), 0)
94 break
95 except ValueError:
96 # Um socket inválido foi passado...
97 self._check()
98 except TypeError:
99 # Algo que não era um socket foi passado...
100 self._check()
101 except socket.error, reason:
102 # Algum dos sockets está com problemas...
103 code, msg = reason.args
104 if code == EINTR:
105 # consulte man 2 select
106 return
107 if code == EBADF:
108 # Socket com problemas...
109 self._check()
110 else:
111 # Se chegamos aqui, realmente não sei o que ocorreu
112 raise
113
114 for condition, mode in enumerate(ready):
115 for source in mode:
116 source.callback(source.source, condition)
117
118 self.after(100, self._select)
119
120
121 def start(self):
122 self.after(100, self._select)
123
124
125
126 if __name__ == '__main__':
127
128 import Tkinter
129 from ScrolledText import ScrolledText
130 from Tkconstants import *
131
132 class MainWindow(Tkinter.Tk, TkSelectMixIn):
133 def __init__(self):
134 Tkinter.Tk.__init__(self)
135
136 self.textbox = ScrolledText(self, bg='white')
137 self.textbox.pack(fill=BOTH, expand=1)
138 self.server()
139 self.start()
140
141 def server(self):
142 # inicializa o servidor
143
144 self.sock = socket.socket(socket.AF_INET, \
145 socket.SOCK_STREAM)
146 self.sock.bind(('localhost', 8000))
147 self.sock.listen(1)
148
149 # a chamada para input_read para o socket do servidor é
150 # um pouco diferente, tendo como callback o método
151 # self.accept
152 self.server_tag = self.input_add(self.sock, \
153 INPUT_READ, self.accept)
154 # mantemos uma lista dos clientes conectados
155 self.clients = {}
156
157 def accept(self, source, condition):
158 # método chamado quando o servidor tem um cliente
159 # esperando para ser aceito
160
161 conn, addr = source.accept()
162 self.insert("%s:%s conectado\n" % addr)
163
164 # insere o cliente na lista e registra o método self.write
165 # como callback para quando existirem dados esperando para
166 # serem lidos.
167
168 self.clients[addr] = (conn, self.input_add(conn,
169 INPUT_READ, self.write))
170 def write(self, source, condition):
171 # método chamado quando um cliente envia dados
172
173 data = source.recv(1024)
174 if not data.strip() or data.strip() == 'bye':
175 # se o cliente enviar um "bye" ou uma linha em branco,
176 # desconecte-o
177 source.close()
178
179 for addr, (conn, tag) in self.clients.iteritems():
180 if source is conn:
181 self.input_remove(tag)
182 self.insert('%s:%s desconectado\n' % addr)
183 del self.clients[addr]
184 break
185 else:
186 for (addr, port), (conn, tag) in \
187 self.clients.iteritems():
188 if source is conn:
189 self.insert('%s:%s>>>%s\n'%(addr, port, \
190 data.strip()))
191 break
192
193 def insert(self, data):
194 self.textbox.insert(END, data)
195 self.textbox.see(END)
196
197 def quit(self):
198 self.input_remove(self.server_tag)
199 for add, (conn, tag) in self.clients.iteritems():
200 self.input_remove(tag)
201 conn.close()
202 self.sock.close()
203 Tkinter.Tk.destroy(self)
204
205 if __name__ == "__main__":
206 root = MainWindow()
207 Tkinter.mainloop()
Nota: Hoje (23/11/2003) descobri meio por acaso que a Tkinter tem sim uma interface, aliás idêntica à GTk. No entanto, ela parece ser tão pouco usada que não é documentada em lugar nenhum. Eu uso Tkinter há mais de três anos e nunca tinha visto. As funções são Tkinter.tkinter.createfilehandler() e Tkinter.tkinter.deletefilehandler. Argumentos e retorno são idênticas as mesmas funções em PyGTK ou na API descrita aqui.