Программа-мечта начинающего питоновода

в 13:47, , рубрики: python, Песочница, чат, метки: ,

Практически любой начинающий программист на 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 метров.

Автор: Pinsky


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js