Рубрики
Без рубрики

Давайте напишем приложение для чата на Python

Прочитайте это пошаговое руководство, чтобы узнать, как создать полнофункциональное приложение для чата на Python.

Автор оригинала: Saurabh Chaturvedi.

Всегда приятно Поболтать!

Сокеты Tkinter + менее чем в 150 строках кода.

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

Именно это и произошло со мной, поэтому я здесь, чтобы поделиться тем, как я создал простое приложение для чата с довольно сжатым кодом Python. Более того, я реализовал код без каких-либо сторонних зависимостей!

Давайте просто нырнем!

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

Использование фреймворков, таких как Twisted и Socket Server был вариантом, но это казалось мне излишним для такого простого программного обеспечения, как наше.

Сервер

Вот как мы начинаем наш серверный сценарий (для этого приложения есть только два сценария: один для сервера, а другой для клиента):

#!/usr/bin/env python3"""Server for multithreaded (asynchronous) chat application."""
from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread

Для этой цели мы будем использовать сокеты TCP, поэтому мы используем флаги AF_INET и SOCK_STREAM . Мы используем их через сокеты UDP, потому что они более телефонные — получатель должен одобрить входящее соединение до начала связи.

Сокеты UDP-это скорее почтовая рассылка (любой может отправить почту получателю, адрес которого он или она знает), поэтому на самом деле им не требуется устанавливать соединение, прежде чем может произойти связь.

Очевидно, что TCP больше подходит для нашей цели, чем сокеты UDP, поэтому мы будем использовать их. Вы можете узнать больше о сокетах здесь .

После импорта мы настроили некоторые константы для последующего использования:

clients = {}
addresses = {}
HOST = ''
PORT = 33000
BUFSIZ = 1024
ADDR = (HOST, PORT)
SERVER = socket(AF_INET, SOCK_STREAM)
SERVER.bind(ADDR)

Теперь мы разбиваем нашу задачу на прием новых соединений, передачу сообщений и обработку конкретных клиентов. Давайте начнем с принятия соединений:

def accept_incoming_connections():
    """Sets up handling for incoming clients."""
    while True:
        client, client_address = SERVER.accept()
        print("%s:%s has connected." % client_address)
        client.send(bytes("Greetings from the cave!" + "Now type your name and press enter!", "utf8"))
        addresses[client] = client_address
        Thread(target=handle_client, args=(client,)).start()

Это просто цикл, который вечно ждет входящих соединений. Как только он получает его, он регистрирует соединение (печатает некоторые сведения о соединении) и отправляет подключенному клиенту приветственное сообщение. Затем он сохраняет адрес клиента в словаре addresses , а затем запускает поток обработки для этого клиента. Конечно, мы еще не определили целевую функцию handle_client() для этого, но вот как мы это делаем:

def handle_client(client): 
    # Takes client socket as argument.
    """Handles a single client connection."""
    name = client.recv(BUFSIZ).decode("utf8")
    welcome = 'Welcome %s! If you ever want to quit, type {quit} to exit.' % name
    client.send(bytes(welcome, "utf8"))
    msg = "%s has joined the chat!" % name
    broadcast(bytes(msg, "utf8"))
    clients[client] = name
    while True:
        msg = client.recv(BUFSIZ)
        if msg != bytes("{quit}", "utf8"):
            broadcast(msg, name+": ")
        else:
            client.send(bytes("{quit}", "utf8"))
            client.close()
            del clients[client]
            broadcast(bytes("%s has left the chat." % name, "utf8"))
            break

Естественно, после того, как мы отправим новому клиенту приветственное сообщение, он ответит именем, которое он или она хочет использовать для дальнейшего общения. В функции handle_client() первой задачей, которую мы выполняем, является сохранение этого имени, а затем отправка клиенту другого сообщения о дальнейших инструкциях.

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

Если мы действительно сталкиваемся с сообщением с инструкциями по выходу (т. Е. клиент отправляет {quit} ), мы возвращаем то же самое сообщение клиенту (оно запускает действие закрытия на стороне клиента), а затем закрываем для него сокет подключения. Затем мы делаем некоторую очистку, удаляя запись для клиента, и, наконец, кричим другим подключенным людям, что этот конкретный человек покинул разговор.

Теперь идет наша функция broadcast() :

def broadcast(msg, prefix=""):
    # prefix is for name identification.
    """Broadcasts a message to all the clients."""
    for sock in clients:
        sock.send(bytes(prefix, "utf8")+msg)

Это в значительной степени самоочевидно-он просто отправляет msg всем подключенным клиентам и при необходимости добавляет дополнительный префикс . Мы передаем префикс | в broadcast () в нашей функции handle_client () , и мы делаем это так, чтобы люди могли точно видеть, кто является отправителем конкретного сообщения.

Это были все необходимые функции для нашего сервера. Наконец, мы ввели некоторый код для запуска нашего сервера и прослушивания входящих подключений:

if __name__ == " __main__":
    SERVER.listen(5)
    # Listens for 5 connections at max.
    print("Waiting for connection...")
    ACCEPT_THREAD = Thread(target=accept_incoming_connections)
    ACCEPT_THREAD.start()
    # Starts the infinite loop.
    ACCEPT_THREAD.join()
    SERVER.close()

Мы join () |/ACCEPT_THREAD так, чтобы основной скрипт ждал его завершения и не переходил к следующей строке, которая закрывает сервер.

Это завершает наш серверный скрипт, который представлен в следующей сути (для тех, кто читает это на смартфонах, посетите эту ссылку для полного кода сервера):

клиент

Это веселее, потому что мы будем писать графический интерфейс! Для наших целей мы используем Tkinter, инструмент построения графического интерфейса Python “батареи включены”. Давайте сначала займемся импортом:

#!/usr/bin/env python3
"""Script for Tkinter GUI chat client."""
from socket import AF_INET, socket, SOCK_STREAM
from threading import Threadimport tkinter

Теперь мы напишем функции для обработки, отправки и получения сообщений. Начнем с получения:

def receive():
    """Handles receiving of messages."""
    while True:
        try:
            msg = client_socket.recv(BUFSIZ).decode("utf8")
            msg_list.insert(tkinter.END, msg)
        except OSError:
            # Possibly client has left the chat.
            break

Почему снова бесконечный цикл? Потому что мы будем получать сообщения совершенно недетерминированно и независимо от того, как и когда мы отправляем сообщения. Мы не хотим, чтобы это было приложение для чата по рации, которое может только отправлять или получать одновременно-мы хотим получать сообщения, когда можем, и отправлять их, когда хотим.

Функциональность внутри цикла довольно проста; recv() является блокирующей частью. Он останавливает выполнение до тех пор, пока не получит сообщение, и когда это произойдет, мы продолжим и добавим сообщение в msg_list . Вскоре мы определим msg_list , который в основном является функцией Tkinter для отображения списка сообщений на экране.

Далее мы определяем функцию send() :

def send(event=None):
    # event is passed by binders.
    """Handles sending of messages."""
    msg = my_msg.get()
    my_msg.set("")
    # Clears input field.
    client_socket.send(bytes(msg, "utf8"))
    if msg == "{quit}":
        client_socket.close()
        top.quit()

Мы используем event в качестве аргумента, потому что он неявно передается Tkinter при нажатии кнопки send в графическом интерфейсе. my_msg – это поле ввода в графическом интерфейсе. Поэтому мы извлекаем сообщение для отправки с помощью msg.get() .

После этого мы очищаем поле ввода, а затем отправляем сообщение на сервер, который, как мы видели ранее, передает это сообщение всем клиентам (если это не сообщение о выходе). Если это сообщение о выходе, мы закрываем сокет, а затем приложение GUI (через top.close() )

Мы определяем еще одну функцию, которая будет вызвана, когда мы решим закрыть окно графического интерфейса. Это своего рода функция очистки перед закрытием и закроет соединение с сокетом до закрытия графического интерфейса:

def on_closing(event=None):
    """This function is to be called when the window is closed."""
    my_msg.set("{quit}")
    send()

Это устанавливает поле ввода в {quit} , а затем вызывает send () , который затем работает должным образом. Теперь мы начинаем строить графический интерфейс в основном пространстве имен (т. Е. Вне любой функции). Мы начнем с определения виджета верхнего уровня и установим его название:

top = tkinter.Tk()
top.title("Chatter")

Затем мы создаем фрейм для хранения списка сообщений. Затем мы создаем строковую переменную, в первую очередь для хранения значения, полученного из поля ввода (которое мы скоро определим). Мы устанавливаем эту переменную в "Введите свои сообщения здесь". чтобы предложить пользователю написать свое сообщение. После этого мы создаем полосу прокрутки для прокрутки этого фрейма сообщения. Вот код:

messages_frame = tkinter.Frame(top)
my_msg = tkinter.StringVar() # For the messages to be sent.
my_msg.set("Type your messages here.")
scrollbar = tkinter.Scrollbar(messages_frame) # To navigate through past messages.

Теперь мы определяем список сообщений, который будет храниться в messages_frame , а затем упаковываем (в соответствующих местах) все материалы, которые мы создали до сих пор:

msg_list = tkinter.Listbox(messages_frame, height=15, width=50, yscrollcommand=scrollbar.set)
scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
msg_list.pack(side=tkinter.LEFT, fill=tkinter.BOTH)
msg_list.pack()
messages_frame.pack()

После этого мы создаем поле ввода для ввода пользователем своего сообщения и привязываем его к строковой переменной, определенной выше. Мы также привязываем его к функции send () , чтобы всякий раз, когда пользователь нажимает return, сообщение отправлялось на сервер.

Затем мы создаем кнопку “Отправить”, если пользователь хочет отправить свои сообщения, нажав на нее. Опять же, мы привязываем нажатие этой кнопки к функции send () . И да, мы также упаковываем все эти вещи, которые мы только что создали. Кроме того, не забудьте использовать функцию очистки on_closing() , которая должна вызываться, когда пользователь хочет закрыть окно графического интерфейса. Мы делаем это с помощью протокола метода top . Вот код для всего этого:

entry_field = tkinter.Entry(top, textvariable=my_msg)
entry_field.bind("", send)
entry_field.pack()
send_button = tkinter.Button(top, text="Send", command=send)
send_button.pack()
top.protocol("WM_DELETE_WINDOW", on_closing)

(Почти) готово. Мы еще не написали код для подключения к серверу. Для этого мы должны запросить у пользователя адрес сервера. Я сделал это , просто используя input () , поэтому пользователь получает приглашение командной строки с запросом адреса хоста перед началом работы графического интерфейса. Это может быть немного неудобно, и вы можете добавить графический интерфейс для этого, но я оставляю это вам в качестве домашнего задания 🙂 . Вот мой код:

HOST = input('Enter host: ')
PORT = input('Enter port: ')
if not PORT: PORT = 33000 # Default value.
else: PORT = int(PORT)
BUFSIZ = 1024
ADDR = (HOST, PORT)
client_socket = socket(AF_INET, SOCK_STREAM)
client_socket.connect(ADDR)

Как только мы получим адрес и создадим сокет для подключения к нему, мы запустим поток для получения сообщений, а затем основной цикл для нашего графического приложения:

receive_thread = Thread(target=receive)
receive_thread.start()
tkinter.mainloop() # Starts GUI execution.

Вот и все! Мы закодировали наше приложение для чата. Опять же, полный клиентский сценарий приведен в следующей сути:

Демонстрация

Это отлично подходит для тестирования на нескольких компьютерах. Вы можете, конечно, запустить сервер и клиент на одной машине для тестирования (используя 127.0.0.1 для ХОСТА в вашем клиенте), но видеть, как общение происходит в реальном времени между разными компьютерами, кажется потрясающим. Серверный скрипт будет регистрировать, какие IP-адреса обращаются к нему, а клиентский скрипт сгенерирует графический интерфейс (после запроса адреса хоста), аналогичный следующим скриншотам:

Графический интерфейс клиента

Другой Клиент, подключенный к тому же Серверу

Честно говоря, графический интерфейс выглядит хорошо, учитывая количество строк кода Python за ним, но не очень! Я оставляю это на ваше усмотрение, чтобы это выглядело лучше (и более интуитивно понятным), возможно, сделав интерфейс чата слева направо, как в мессенджере Facebook. Вы даже можете использовать сторонние библиотеки, такие как Kivy для большей красоты и кросс-платформенной переносимости, или вместо этого использовать веб-интерфейс – публикуйте свои идеи в комментариях. Наконец, спасибо, что терпели меня и читали до последнего символа! Я аплодирую вашему терпению”.

P. S: Для других моих проектов (некоторые меньше, а другие намного больше) посетите мой профиль на GitHub .

Кроме того, я новичок в блогах, поэтому конструктивная критика не только нужна, но и очень нужна! Я открыт для лучших стилей письма, методов и педагогики — не стесняйтесь упоминать их в комментариях.

Этот пост первоначально опубликован автором на Запуск . Эта версия была отредактирована для ясности и может отличаться от исходного сообщения.