Table of Contents

В этой статье я бы хотел показать как написать простое приложение мессенджер менее чем в 150 строк.

Серверная часть

Начнём с сервера(наше приложение будет состоять из скриптов сервера и клиента), через который можно получать входящие запросы от клиентов, желающих общаться. Традиционно указываем путь до интерпретатора и импортируем необходимые модули. Конкретно socket и threading. Первый отвечает непосредственно за “общение” процесссов между собой, второй за многопоточность. О этих модулях подробно можно почитать например здесь - socket , threading.

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

#!/usr/bin/env python3
from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread

Давайте обозначим константы, отвечающие например за адрес порта и размер буфера.

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

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

def accept_incoming_connections():
    """Настраивает обработку для входящих клиентов."""
    while True:
        client, client_address = SERVER.accept()
        print("%s:%s присоединился к переписке" % client_address)
        client.send(bytes("Привет!"+
                          "Введи своё имя и нажми Enter", "utf8"))
        addresses[client] = client_address
        Thread(target=handle_client, args=(client,)).start()

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

def handle_client(client):
     name = client.recv(BUFSIZ).decode("utf8")
    welcome = 'Добро пожаловать %s! если желаете покинуть чат то, нажмите {quit} чтобы выйти.' % name
    client.send(bytes(welcome, "utf8"))
    msg = "%s Теперь в переписке" % 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 покинул переписку." % name, "utf8"))
            break

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

Теперь пропишем функцию broadcast ():

def broadcast(msg, prefix=""):
     for sock in clients:
        sock.send(bytes(prefix, "utf8")+msg)

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

if __name__ == "__main__":
    SERVER.listen(5)
    print("Ожидание соединения")
    ACCEPT_THREAD = Thread(target=accept_incoming_connections)
    ACCEPT_THREAD.start()  # Бесконечный цикл.
    ACCEPT_THREAD.join()
    SERVER.close()

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

В итоге получаем вот такой код для серверной части:

#!/usr/bin/env python3
from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread

def accept_incoming_connections():
    while True:
        client, client_address = SERVER.accept()
        print("%s:%s соединено" % client_address)
        client.send(bytes("Добро пожаловать , введите своё имя и нажмите Enter", "utf8"))
        addresses[client] = client_address
        Thread(target=handle_client, args=(client,)).start()


def handle_client(client):  
    name = client.recv(BUFSIZ).decode("utf8")
    welcome = 'Добро пожаловать %s! Если желаете выйти,то нажмите {quit} чтобы выйти.' % name
    client.send(bytes(welcome, "utf8"))
    msg = "%s вступил в переписку" % 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 покинул переписку" % name, "utf8"))
            break


def broadcast(msg, prefix=""):

    for sock in clients:
        sock.send(bytes(prefix, "utf8")+msg)

        
clients = {}
addresses = {}

HOST = ''
PORT = 33000
BUFSIZ = 1024
ADDR = (HOST, PORT)

SERVER = socket(AF_INET, SOCK_STREAM)
SERVER.bind(ADDR)

if __name__ == "__main__":
    SERVER.listen(5)
    print("ожидание соединения")
    ACCEPT_THREAD = Thread(target=accept_incoming_connections)
    ACCEPT_THREAD.start()
    ACCEPT_THREAD.join()
    SERVER.close()

Клиентская часть###

Теперь приступим к наиболее интересной части нашего приложения - к клиенту. В качестве gui будем использовать tkinter, т.к в нём довольно легко построить несложное приложение. Традиционно импортируем модуль tkinter, а также модули использовавшиеся ранее при написании серверной части программы.

#!/usr/bin/env python3

from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread
import tkinter

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

def receive():
    """обработка получения сообщений"""
    while True:
        try:
            msg = client_socket.recv(BUFSIZ).decode("utf8")# декодируем,чтобы не получить кракозябры
            msg_list.insert(tkinter.END, msg)
        except OSError:
            break

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

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

def send(event=None):
    """обработка отправленных сообщений"""
    msg = my_msg.get()
    my_msg.set("")  # очищаем поле.
    client_socket.send(bytes(msg, "utf8"))
    if msg == "{quit}":
        client_socket.close()
        top.quit()

my_msg - это поле ввода в графическом интерфейсе, и поэтому мы извлекаем сообщение для отправки с помощью msg = my_msg.get (). После этого мы очищаем поле ввода и затем отправляем сообщение на сервер, который, как мы видели ранее, передает это сообщение всем клиентам (если это не сообщение о выходе). Если это сообщение о выходе, мы закрываем сокет, а затем приложение с графическим интерфейсом (через top.close ())

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

def on_closing(event=None):
    """Эта функция вызывается когда закрывается окно"""
    my_msg.set("{quit}")
    send()

Это устанавливает в поле ввода значение {quit}, а затем вызывает send (). Начнем с определения виджета верхнего уровня и установки его заголовка, как и в любой другой программе на tkinter:

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

Затем создаём фрейм со списком сообщений, поле для ввода сообщений и скроллбар для перемещения по истории переписки

messages_frame = tkinter.Frame(top)
my_msg = tkinter.StringVar()
my_msg.set("Введите ваше сообщение здесь.")
scrollbar = tkinter.Scrollbar(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 (), которая должна вызываться, когда пользователь хочет закрыть окно GUI:

entry_field = tkinter.Entry(top, textvariable=my_msg)
entry_field.bind("<Return>", send)
entry_field.pack()
send_button = tkinter.Button(top, text="отправить", command=send)
send_button.pack()
top.protocol("WM_DELETE_WINDOW", on_closing)

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

HOST = input('Введите хост: ')
PORT = input('Введите порт: ')
if not PORT:
    PORT = 33000  # Стандартный порт
else:
    PORT = int(PORT)
BUFSIZ = 1024
ADDR = (HOST, PORT)
client_socket = socket(AF_INET, SOCK_STREAM)
client_socket.connect(ADDR)

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

chat_app_gif

receive_thread = Thread(target=receive)
receive_thread.start()
tkinter.mainloop()

Вот и всё! Теперь наш скрипт клиентской части выглядит вот так:

#!/usr/bin/env python3
from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread
import tkinter


def receive():
    while True:
        try:
            msg = client_socket.recv(BUFSIZ).decode("utf8")
            msg_list.insert(tkinter.END, msg)
        except OSError:
            break


def send(event=None):
    msg = my_msg.get()
    my_msg.set("")
    client_socket.send(bytes(msg, "utf8"))
    if msg == "{quit}":
        client_socket.close()
        top.quit()


def on_closing(event=None):
    my_msg.set("{quit}")
    send()

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

messages_frame = tkinter.Frame(top)
my_msg = tkinter.StringVar()
my_msg.set("Введите ваше сообщение здесь")
scrollbar = tkinter.Scrollbar(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()

entry_field = tkinter.Entry(top, textvariable=my_msg)
entry_field.bind("<Return>", send)
entry_field.pack()
send_button = tkinter.Button(top, text="отправить", command=send)
send_button.pack()

top.protocol("WM_DELETE_WINDOW", on_closing)


HOST = input('Введите хост: ')
PORT = input('Введите порт: ')
if not PORT:
    PORT = 33000
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()

Да, наше приложение не может тягаться с такими гигантами как: telegram, viber, клиентами xmpp/jabber; однако нам удалось создать простой чат, который каждый может развить в что-то своё: сделать уклон в безопасность(например шифруя передаваемые пакеты) или в хороший ux/ui. Получилась своего рода база для чего-то большего и это круто. Спасибо за прочтение, буду рад любым замечаниям и пожеланиям. Традиционно исходный код программы доступен в моём репозитории на github.