Преобразую кофеиноникотиновую смесь в код
0,2
рейтинг
17 сентября 2012 в 17:47

Разработка → Программа-мечта начинающего питоновода из песочницы

Практически любой начинающий программист на Python патологически старается написать свой чат. А если еще и с GUI, то эта прорамма является просто пределом мечтаний.

Что-то вроде введения


Для начала введем в нашу задачу насколько условностей — пишем мы чат для локальной сети с разрешенными широковещательными UDP-пакетами. Для простоты решения задачи так же решим, что в качестве GUI будем использовать библиотеку Tkinter(обычно в дистрибьютивах Linux она идет из коробки, так же и в официальной сборке Python под Windows она является стандартной).

Запитоним сеть


Работа с сокетами в питоне не зависит от платформы(по большому счету даже под PyS60 мы получим рабочий сетевой код, по этому примеру).

Для нашего чата мы решили использовать широковещательные UDP-пакеты. Одной из причин их использования была возможность отказаться от использования сервера. Необходимо принять еще одну условность в нашей программе — номер порта, и пусть он будет равен 11719, во первых, это простое число(а ведь это уже многое). А, во вторых, этот порт, по официальной информации IANA, не занят.

Создадим слушающий сокет, который будет радовать консоль сообщениями приходящими на него:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.bind(('0.0.0.0',11719))
while 1:
	message = s.recv(128)
	print (message)


У сокета мы выставили свойства SO_REUSEADDR(позволяет нескольким приложениям «слушать» сокет) и SO_BROADCAST(указывает на то, что пакеты будут широковещательные) в значние истины. На самом деле на некоторых системах возможна работа скрипта и без этих строчек, но все же лучше явно указать эти свойства.
На прослушку мы подключаемся к адресу '0.0.0.0' — это означает, что прослушиваются вообще все интерфейсы. Можно указывать в поле адреса и просто пустые кавычки, но все же лучше явно указать адрес.

Так же создадим широковещательную «засиралку» сети(для проверки первой программы):

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
while 1:
	sock.sendto('broadcast!',('255.255.255.255',11719))


В отношении же передающей части, необходима лишь только опция SO_BROADCAST(зато теперь обязательна на всех платформах) и с помощью метода sendto() мы рассылаем по всей сети наши пакеты. Остановив с помощью CTRL+C сетевой флуд перейдем к описанию интерфейса.

Окна, окна, окна


Tkinter — наверное, одна из самых простых библиотек для организации оконного интерфейса на питоне(а еще по заявлению создателя питона она является одной из самых надежных и стабильных). Но для начинающего питоновода главный момент все же, что эта библиотека простая.

И для того, чтобы получить просто окно нужного нам размера и с указанным заголовком от нас требуется лишь несколько строчек кода:

from Tkinter import *

tk=Tk()
tk.title('MegaChat')
tk.geometry('400x300')
tk.mainloop()


Все бы ничего, но мы создаем, не галерею окон разных размеров, а чат. А значит нам нужны на окне еще и элементы — лог чата, поле, где мы укажем свой ник и текст сообщения, который мы вводим. Без кнопки отправить можно обойтись, если наше поле для сообщения будет само реагировать на нажатие клавиши Enter.

Решение проблемы проще, чем кажется. Для лога чата можно использовать виджет Text, а для двух остальных Entry. Для того, чтобы разместить элементы на форме мы используем автоматический компоновщик pack, который будет известным только ему способом расставлять элементы, придерживаясь только явноуказанных указаний. Однако, ему можно указать сторону к которой крепить виджеты, расширять ли их и в какую сторону, и еще некоторые параметры компоновки.

from Tkinter import *

tk=Tk()
tk.title('MegaChat')
tk.geometry('400x300')
log = Text(tk)
nick = Entry(tk)
msg = Entry(tk)
msg.pack(side='bottom', fill='x', expand='true')
nick.pack(side='bottom', fill='x', expand='true')
log.pack(side='top', fill='both',expand='true')
tk.mainloop()


А что же до связи интерфейса с данными программы? Или как заставить в фоне выполняться какую-нибудь процедуру? Ну Entry можно связать с StingVar'ами(указав в конструкторе виджетов свойство textvariable). а для запуска фоновой процедуры есть у Tkinter'а метод after(<время в мс>,<функция>). Если в конце исполнения этой процедуры указать переинициализацию ее, то процедура будет выполняться постоянно, пока запущена программа.
Несколько нажатий клавиш и мы получаем:

from Tkinter import *

tk=Tk()

text=StringVar()
name=StringVar()
name.set('HabrUser')
text.set('')
tk.title('MegaChat')
tk.geometry('400x300')

log = Text(tk)
nick = Entry(tk, textvariable=name)
msg = Entry(tk, textvariable=text)
msg.pack(side='bottom', fill='x', expand='true')
nick.pack(side='bottom', fill='x', expand='true')
log.pack(side='top', fill='both',expand='true')

def loopproc():
	print ('Hello '+ name.get() + '!')
	tk.after(1000,loopproc)

tk.after(1000,loopproc)
tk.mainloop()


В консоли забегало приветствие. Меняя ник мы можем убедится, что связь между полем Entry и нашей переменной работает идеально. Самое время реализовать возможность передачи пользователем своего сообщения в программу по нажатию на Enter. Реализуется это еще проще, с помощью метода bind(<действие>, <функция>) у виджетов. Единственное, что нам нужно учесть, что функция должна принимать параметр event. За одно перенесем с консоли действие в поле лога. Получаем:

from Tkinter import *

tk=Tk()

text=StringVar()
name=StringVar()
name.set('HabrUser')
text.set('')
tk.title('MegaChat')
tk.geometry('400x300')

log = Text(tk)
nick = Entry(tk, textvariable=name)
msg = Entry(tk, textvariable=text)
msg.pack(side='bottom', fill='x', expand='true')
nick.pack(side='bottom', fill='x', expand='true')
log.pack(side='top', fill='both',expand='true')

def loopproc():
	log.insert (END,'Hello '+ name.get() + '!\n')
	tk.after(1000,loopproc)

def sendproc(event):
	log.insert (END,name.get()+':'+text.get()+'\n')
	text.set('')

msg.bind('<Return>',sendproc)
tk.after(1000,loopproc)
tk.mainloop()


Почти готово...


И кажется, что осталось объединить программы из первой и второй частей статьи и мы получим готовый чат. Что ж, попробуем. Однако сразу скажу, что специально выводить сообщения, которые мы отправляем не будем — так как при прослушивании широковещательного трафика мы их и так получим.

import socket
from Tkinter import *

tk=Tk()

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.bind(('0.0.0.0',11719))

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)

text=StringVar()
name=StringVar()
name.set('HabrUser')
text.set('')
tk.title('MegaChat')
tk.geometry('400x300')

log = Text(tk)
nick = Entry(tk, textvariable=name)
msg = Entry(tk, textvariable=text)
msg.pack(side='bottom', fill='x', expand='true')
nick.pack(side='bottom', fill='x', expand='true')
log.pack(side='top', fill='both',expand='true')

def loopproc():
	message = s.recv(128)
	log.insert(END,message)
	tk.after(1,loopproc)

def sendproc(event):
	sock.sendto (name.get()+':'+text.get(),('255.255.255.255',11719))
	text.set('')

msg.bind('<Return>',sendproc)
tk.after(1,loopproc)
tk.mainloop()


Однако, чуда не произошло. Программа намертво встала в ожидании входящего пакета. Фоновый поток оказался не таким уж и фоновым и для продолжения выполнения остальной программы он должен иногда завершаться. Чтобы это происходило слушающий сокет необходимо перевести в неблокирующий режим и для того, чтобы мы не получали сообщение об ошибке когда в сокете пусто оградим этот кусок кода try'ем.

import socket
from Tkinter import *

tk=Tk()

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.bind(('0.0.0.0',11719))

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)

text=StringVar()
name=StringVar()
name.set('HabrUser')
text.set('')
tk.title('MegaChat')
tk.geometry('400x300')

log = Text(tk)
nick = Entry(tk, textvariable=name)
msg = Entry(tk, textvariable=text)
msg.pack(side='bottom', fill='x', expand='true')
nick.pack(side='bottom', fill='x', expand='true')
log.pack(side='top', fill='both',expand='true')

def loopproc():
	s.setblocking(False)
	try:
		message = s.recv(128)
		log.insert(END,message+'\n')
	except:
		tk.after(1,loopproc)
		return
	tk.after(1,loopproc)
	return

def sendproc(event):
	sock.sendto (name.get()+':'+text.get(),('255.255.255.255',11719))
	text.set('')

msg.bind('<Return>',sendproc)
tk.after(1,loopproc)
tk.mainloop()


«А после доработать напильником...»


Чисто теоретически полученный код работоспособен, но обладает достаточно весомыми недостатками — поле ввода сообщения сразу не выбирается по умолчанию, возможны проблемы с кирилицей и лог сам не прокручивается вниз. Решение всех этих проблем можно увидеть из следующего листинга(место, где решается вопрос кириллицы выделил, остальное надеюсь и так очевидно):

# -*- coding: utf-8 -*- 

import socket
from Tkinter import *

#Решаем вопрос с кирилицей
reload(sys)
sys.setdefaultencoding('utf-8')
#-----------------------------

tk=Tk()

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.bind(('0.0.0.0',11719))

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)

text=StringVar()
name=StringVar()
name.set('HabrUser')
text.set('')
tk.title('MegaChat')
tk.geometry('400x300')

log = Text(tk)
nick = Entry(tk, textvariable=name)
msg = Entry(tk, textvariable=text)
msg.pack(side='bottom', fill='x', expand='true')
nick.pack(side='bottom', fill='x', expand='true')
log.pack(side='top', fill='both',expand='true')

def loopproc():
	log.see(END)
	s.setblocking(False)
	try:
		message = s.recv(128)
		log.insert(END,message+'\n')
	except:
		tk.after(1,loopproc)
		return
	tk.after(1,loopproc)
	return

def sendproc(event):
	sock.sendto (name.get()+':'+text.get(),('255.255.255.255',11719))
	text.set('')

msg.bind('<Return>',sendproc)

msg.focus_set()

tk.after(1,loopproc)
tk.mainloop()


P.S. И не забывайте — длина питона может достигать 9-10 метров.
Alexander Sharihin @Pinsky
карма
25,7
рейтинг 0,2
Преобразую кофеиноникотиновую смесь в код
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (34)

  • +28
    Никогда не хотел писать чат.
    • 0
      На самом деле, да, я несколько преувеличил на счет практически любого, но юнных питоноводов, что мечтают написать чат не мало.
      Но плюс тем, кто не хотел писать чаты — они почувствуют себя уникальными.
      • +1
        У вас патологическая любовь к букве «н» :) «Юнный», «длинна».
        • 0
          Да я и сам заметил.
          Извиняюсь перед теми, кому устроил «рак глаз» от этого.
      • 0
        На самом деле почти угадали, но чат (или многопользовательский текстовый редкатор а-ля Google Drive) хочется писать в основном при виде какого-нибудь соблазнительного риалтаймового веб-фреймворка.

        А программы с GUI хочется писать при виде красивого IDE с интерфейс билдером :-)
    • +4
      И GUI тоже та ещё радость, особенно когда есть заказчик. Лучше писать программы для серверов чем для людей…
    • 0
      Ну не расстраивайтесь так. Есть же еще тетрис, XML парсер и судоку!
    • 0
      Я тоже. Я хотел написать SVN-клиент для Гнома.
  • +1
    name.get()+':'+text.get() # '%s:%s' % (name.get(), text.get())
    


    Так «питоничнее» ;)
    • +1
      Да да, думал сразу так писать, но цель была сделать как можно более понятно для начинающих. Моя конструкция для них как бы читаемее.
      • +2
        Но менее понимаемый, это им нужно еще почитать про форматирование строк.
    • +4
      Питоничнее ':'.join([name.get(),text.get()])
    • 0
      Хм.
      PEP, в котором рекомендуется так делать подскажете?
      • 0
        Вряд ли есть такой PEP. Просто "%" читается гораздо лучше чем каша из кавычек и плюсов.
        • 0
          А, прошу прощения. Мне показалось, что ваш текст не в комментарии.
          Вопрос снимается )
  • +2
    В одном уральском университете, математики 2 и 3 курса будут рады разжеванному заданию
  • +1
    Эх, а я лет 5 назад на делфи такой писал :) Но да, действительно был чат. С гуем.
    • –1
      А я на плюсах на первом курсе… Хорошее было время (=
  • НЛО прилетело и опубликовало эту надпись здесь
    • +6
      Таки удав :)
  • 0
    Так всетаки. Питоновода или потонщика?
    • 0
      питонист
      • 0
        Это только если музыкант
  • +1
    моя первая программа определеляла айпи адрес под которым работает домашний рутер через парсинг страницы в интернете и отсылала его на почту, если адрес менялся (статического адреса нет, ddns не использую, делал ради фана) :)
  • 0
    Пример почему то не работает (Ubuntu 12.04).
    Насколько я понял, в чате должны появляться собственные сообщения.
    После отправки сообщения, поле лога так и остается пустым.
    Пробовал открывать порт 11719, — не помогло.
    Хотел с адроид планшета запустить, но в SL4A нет tkinter :(
    • 0
      Проверь файрвол.
      Подобная проблема была на Fedora 17. Вылечилась разрешением UDP трафика на этом порту.
      Ведь если посмотришь tcpdump -np — то пакеты будут прыгать в интерфейс
  • +1
    И Вы правда хотите сказать что эта каша из кода легче и краше чем wxPyhon?!

    p.s. придираюсь исключительно к tkinter, ни к автору, ни к коду.
    • 0
      Могу ошибаться. но из коробки wxPython в дистре под винду не идет.
      А Tkinter очень многие воспринимают уже давно как архаизм.
      • 0
        Архаизм, если мне не изменяет память — нечто очень старое, вышедшее из общего пользование.

        Из коробки wxPython естественно не идет вместо с Python, а зачем скриптовому (серверному, технологичному и т.д.) с собой таскать GUI библиотеку весом в 20-40 мегабайт?!
  • 0
    Практически любой начинающий программист на Python патологически старается написать свой чат.
    Ни одного такого не знаю, а в «Яндексе» перевидал начинающих пайтонистов кучу.
  • 0
    лучше писать:
    while 2:
    
    • 0
      while True:
  • 0
    У tornado есть пример чата. Тоже советую взглянуть.
    • 0
      Не используйте торнадо, если не трогаете ручки ядра ОС (пример LXC). Замучаетесь и потратие огромную кучу времени впустую.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.