Эксперт
Сергей
Сергей
Задать вопрос
Мы готовы помочь Вам.

Реализовать простой “эхо-чат” с использованием сокетов.

 

Условия:

  1. Сервер должен отправлять всем клиентам (кроме отправителя) сообщение, которое он получил от клиента-отправителя (эхо). Пример:
    Клиент1: >>> Привет.

Сервер: >>> @Клиент1: Привет.

  1. Сервер и клиент должны быть реализованы и запускаться отдельно.
  2. Клиентов может быть неограниченное количество, однако сервер не должен блокироваться одним клиентом. (Следует использовать потоки и/или асинхронный подход)

 

Решение:

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

Как было сказано в теоретической справке: socket — реализован в стандартной поставке большинства популярных языков программирования, в частности Java, Python, C#, C/С++, Go и. т. д. Поэтому представленные лаботорные задания можно выполнить на любом любимом языке программирования. В данном разборе мы будем использовать язык программирования Python, версии 3.6+.

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

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

 

Перейдем к разбору решения.

Предполагается, что Python версии 3.6 или более поздней у вас уже установлен.

Создадим python-пакет (папка с файлами __init__.py, client.py, server.py). Скриншот созданного пакета представлен на рисунке 3.

screenshot 36 9

Рис. 3 — Python-пакет с разрабатываемой программой

 

Откроем файл server.py любым предпочитаемым редактором и начнем реализацию нашего сервера. В данной лабораторной работе мы создадим классическое клиент-серверное приложение, а в следующей перейдем к реализации P2P чата и переделаем наш сервер в “сигнальный”.

Для избежание проблем сериализации и разбора данных в сетевых приложениях часто прибегают к применению специализированных протоколов. Мы в нашей реализации не станем делать исключение и реализуем простой протокол, который называется JSON Message Protocol, суть которого заключается в обмене json объектами вместо “сырых” данных.

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

import socket
# Стандартный модуль socket.
# Подробнее: https://docs.python.org/3/library/socket.html
import sys
# Стандартный модуль sys.
# Подробнее: https://docs.python.org/3/library/sys.html
import time
# Стандартный модуль time.
# Подробнее: https://docs.python.org/3/library/time.html
import json
# Стандартный модуль json.
# Подробнее: https://docs.python.org/3/library/json.html

class Message:
«»»
Класс-Сообщение. Представляет сообщения,

которые будут приходить от клиентов.
«»»

def __init__(self, status_code: str = ‘200’, **data):
# Распаковываем кортеж именованных аргументов в параметры класса.
# Паттерн Builder
for param, value in data.items():
setattr(self, param, value)
self.status_code = status_code  # код ответа сообщения
# время получения сообщения.
self.curr_time = time.strftime(«%Y-%m-%d-%H.%M.%S»,
time.localtime())

def to_json(self):
«»»
Возвращает атрибуты класса и их значения в виде json.
Использует стандартный модуль python — json.
«»»
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True,
indent=4)

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

 

class ServerDataHandler:
«»»
Класс-Обработчик с бизнес-логикой сервера.
Реализует методы обработки сообщений и их рассылки.
«»»
clients = {}  # Временное хранилище клиентов в виде словаря.
# Если хотите реализовать *продвинутое решение, можете реализовать
# взаимодействие с базой данных и сохранением пользователей.

current_connection = None  # текущее соединение

def _add_connection(self, name: str, addr: str):
«»» Добавляет новое соединения в словарь clients»»»
self.current_connection = addr  # адрес, с которого пришло сообщение
self.clients[name] = addr  # добавление клиента

def get_and_register_message(self, data: bytes, addr: str):
«»»
Сохраняет адрес запроса пользователя,
записывается в атрибут data данные из json в виде словаря,
добавляет имя пользователя и адрес в словарь чтобы у нас был
доступ к адресу по имени пользователя который обратился к серверу

:param data — полученные «сырые» данные в виде bytes
:param addr — адрес отправителя данных
:return Message(status_code=’200′, **data) — объект сообщения
«»»
data = dict(json.loads(data.decode(‘utf-8’))) # декодируем данные
self._add_connection(name=data.get(‘sender_name’,
‘Unknown’),
addr=addr) # добавляем/обновляем список клиентов
return Message(status_code=’200′, **data)

def send_message(self, sock, message_obj: Message):
«»»
Отправляет сообщение по всем адресам в словаре
кроме адреса отправившего запрос (эхо)
:param sock — серверный сокет
:param message_obj — объект сообщения
«»»
data = message_obj.to_json() # закодированное в json сообщение
# Отправляем сообщение всем клиентам, кроме текущего:
for client in self.clients.values():
if self.current_connection != client:
sock.sendto(data.encode(‘utf-8’), client)

Наконец, реализуем “точку входа” нашего приложения, в котором непосредственно создадим сокет и будем ждать сообщений и подключений. В нашем разборе мы будем запускать сервер и клиентов локально на нашей же машине (localhost), однако, вам ничего не мешает изменить ip-адрес на ip-адрес вашего устройства в локальной или глобальной сети и использовать разработанное приложение как полноценный чат.

if __name__ == «__main__»:
# Создаем объект серверного сокета.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# host и port на котором будет запущен сервер
host = ‘localhost’
port = 8888
# Устанавливаем опцию для текущего адреса сокета,
# чтобы его можно было переиспользовать в последующих перезапуска:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Регистрируем сокет
s.bind((host, port))
# Создаем обработчик бизнес-логики
data_handler = ServerDataHandler()
# Флаг для остановки работы сервера
quit_server = False
print(«Server started»)

# Основной цикл работы сервера.
while not quit_server:
try:
# Получаем данные из буфера сокета
recv_data, recv_addr = s.recvfrom(1024)
# Логируем информацию в консоль
sys.stdout.write(recv_data.decode(‘utf-8’))

# Регистрируем сообщение
message = data_handler.get_and_register_message(recv_data,
recv_addr)
# Посылаем сообщение в чат (эхо)
data_handler.send_message(s, message)

except Exception as ex:
# Если произошла ошибка, останавливаем работу сервера.
print(f»Server stopped, because {ex}»)
quit_server = True
# Закрываем серверное соединение.
s.close()

 

С разработкой серверной логики мы закончили, можно переходить к клиентской. Напишем обработчики отправки и получения сообщений для клиента. Начнем как и в случае с клиентом с импорта необходимых библиотек и модулей и объявления класса Сообщения (Message).

 

import socket
# Стандартный модуль socket.
# Подробнее: https://docs.python.org/3/library/socket.html
import sys
# Стандартный модуль sys.
# Подробнее: https://docs.python.org/3/library/sys.html
import time
# Стандартный модуль time.
# Подробнее: https://docs.python.org/3/library/time.html
import json
# Стандартный модуль json.
# Подробнее: https://docs.python.org/3/library/json.html

import threading
# Стандартный модуль threading.
# Подробнее: https://docs.python.org/3/library/threading.html

# Глобальная переменная, отвечающая за остановку клиента.
shutdown = False

class Message:
«»»
Класс-Сообщение. Представляет сообщения,
которые будут приходить от клиентов.
«»»

def __init__(self, **data):
# Устанавливаем дополнительные атрибуты сообщения.
self.status = ‘online’
if ‘join’ not in data:
self.join = False

# Распаковываем кортеж именованных аргументов в параметры класса.
# Паттерн Builder
for param, value in data.items():
setattr(self, param, value)

# время получения сообщения:
self.curr_time = time.strftime(«%Y-%m-%d-%H.%M.%S»,
time.localtime())

def to_json(self):
«»»
Возвращает атрибуты класса и их значения в виде json.
Использует стандартный модуль python — json.
«»»
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True,
indent=4)

 

Напишем класс Обработчик (ClientHandler), который будет отвечать за получение  и отправку клиентских сообщений.

class ClientHandler:
«»»
Класс-Обработчик с бизнес-логикой клиента.
Реализует методы получения и отображения сообщений
«»»

def __init__(self, server_addr=(‘localhost’, 8888),
client_addr=(‘localhost’, 0)):
global shutdown
# Флаг сигнализирующий об успешном подключении
join = False
# Пытаемся создать соединение, если его еще нет или клиент не остановлен
while not shutdown and not join:
try:
# Имя клиента в чате:
self.name = input(«Name: «).strip()
# Адрес сервера (ip, port) к которому происходит подключение:
self.server_addr = server_addr
# Создание сокета:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Подключение сокета:
self.socket.connect(client_addr)
join = True
# Отправка сообщения о подключении:
connect_message = Message(
join=join,
message=f’User @{self.name} has joint to chat\n’,
sender_name=self.name
)
connect_message_data = connect_message.to_json()
self.socket.sendto(connect_message_data.encode(‘utf-8’),
self.server_addr)
except Exception as ex:
print(f»ClientHandler.__init__: Что-то пошло не так: {ex}»)
shutdown = True

@staticmethod
def show_message(message_obj: Message):
«»»
Выводит полученное сообщение в стандартный поток вывода (консоль)
«»»
if message_obj.join:
# Если сообщение о подключении, то выводим только его:
sys.stdout.write(message_obj.message)
else:
# Иначе, добавляем имя отправителя в вывод:
sys.stdout.write(f’@{message_obj.sender_name}: ‘
f'{message_obj.message}\n’)

def receive(self):
«»»
Получает сообщение из сокета и передает его в обработчик
«»»
global shutdown
# Пока клиент не остановлен
while not shutdown:
try:
# Получаем данные и адрес отправителя
data, addr = self.socket.recvfrom(1024)
data = dict(json.loads(data.decode(‘utf-8’)))
# Создаем объект сообщения из полученных данных:
message = Message(**data)
# Вызываем обработчик показа сообщения:
self.show_message(message)
time.sleep(0.2)
except Exception as ex:
print(f»ClientHandler.receive: Что-то пошло не так: {ex}»)
shutdown = True

def send(self):
«»»
Принимает сообщение из потока ввода консоли,
отправляет его в обработчик и посылает на сервер.
«»»
global shutdown
# Пока клиент не остановлен
while not shutdown:
try:
# Ожидаем ввод данных
input_data = input(«»).strip()
if input_data:
# Создаем объект сообщения из введенных данных:
message = Message(message=input_data,
sender_name=self.name)
# Отправляем данные на сервер:
data = message.to_json()
self.socket.sendto(data.encode(‘utf-8’), self.server_addr)
time.sleep(0.2)
except Exception as ex:
print(f»ClientHandler.send: Что-то пошло не так: {ex}»)
shutdown = True

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

if __name__ == ‘__main__’:
# Создаем обработчик клиента
handler = ClientHandler(server_addr=(‘localhost’, 8888),
client_addr=(‘localhost’, 0))
# В отдельном потоке вызываем обработку получения сообщений:
recv_thread = threading.Thread(target=handler.receive)
recv_thread.start()
# В главном потоке вызываем обработку отправки сообщений:
handler.send()
# Прикрепляем поток с обработкой получения сообщений к главному потоку:
recv_thread.join()

 

Запустим наш сервер и двух клиентов, чтобы протестировать работоспособность программы. Для запуска вы можете использовать любой терминал в случае с Unix, или “командную строку” в случае с Windows. На рисунках 4-6 представлен пример работы с использованием терминала, встроенного в IDE PyCharm и ОС Windows 10.

screenshot 37 10

Рис. 4 — Результат работы сервера

screenshot 38 9

Рис. 5 — Результат работы первого тестового клиента

screenshot 39 9

Рис. 6 — Результат работы второго тестового клиента

 

Поздравляем! Вот вы и создали собственный простой чат с использованием сокетов. Однако, разобранный пример — традиционное клиент-серверное приложение. Для реализации P2P соединения необходимо будет внести в него некоторые изменения, однако, принцип межсетевого взаимодействия останется таким же. Полностью готовое решение вы можете посмотреть в репозиторий.

Была ли полезна данная статья?
Да
60.89%
Нет
39.11%
Проголосовало: 1097

или напишите нам прямо сейчас:

⚠️ Пожалуйста, пишите в MAX или заполните форму выше.
В России Telegram и WhatsApp блокируют - сообщения могут не дойти.
Написать в MAXНаписать в TelegramНаписать в WhatsApp