diff --git a/client.py b/client.py new file mode 100644 index 0000000..79135ec --- /dev/null +++ b/client.py @@ -0,0 +1,127 @@ +""" +Функции ​к​лиента:​ +- сформировать ​​presence-сообщение; +- отправить ​с​ообщение ​с​ерверу; +- получить ​​ответ ​с​ервера; +- разобрать ​с​ообщение ​с​ервера; +- параметры ​к​омандной ​с​троки ​с​крипта ​c​lient.py ​​ ​[​]: +- addr ​-​ ​i​p-адрес ​с​ервера; +- port ​-​ ​t​cp-порт ​​на ​с​ервере, ​​по ​у​молчанию ​​7777. +""" +import sys +import time +import threading +from socket import socket, AF_INET, SOCK_STREAM +from errors import UsernameToLongError, ResponseCodeLenError, MandatoryKeyError, ResponseCodeError +from config import * +from utils import send_message, get_message + + +def create_presence(account_name="Guest"): + """ + Сформировать ​​presence-сообщение + :param account_name: Имя пользователя + :return: Словарь сообщения + tests: + ИЗ-ЗА времени трудно написать doctest + """ + # Если имя не строка + if not isinstance(account_name, str): + # Генерируем ошибку передан неверный тип + raise TypeError + # Если длина имени пользователя больше 25 символов + if len(account_name) > 25: + # генерируем нашу ошибку имя пользователя слишком длинное + raise UsernameToLongError(account_name) + # формируем словарь сообщения + message = { + ACTION: PRESENCE, + TIME: time.time(), + USER: { + ACCOUNT_NAME: account_name + } + } + # возвращаем + return message + + +def translate_response(response): + """ + Разбор сообщения + :param response: Словарь ответа от сервера + :return: корректный словарь ответа + """ + # Передали не словарь + if not isinstance(response, dict): + raise TypeError + # Нету ключа response + if RESPONSE not in response: + # Ошибка нужен обязательный ключ + raise MandatoryKeyError(RESPONSE) + # получаем код ответа + code = response[RESPONSE] + # длина кода не 3 символа + if len(str(code)) != 3: + # Ошибка неверная длина кода ошибки + raise ResponseCodeLenError(code) + # неправильные коды символов + if code not in RESPONSE_CODES: + # ошибка неверный код ответа + raise ResponseCodeError(code) + # возвращаем ответ + return response + + +def create_message(message_to, text, account_name='Guest'): + return {ACTION: MSG, TIME: time.time(), TO: message_to, FROM: account_name, MESSAGE: text} + + +def read_messages(client): + """ + Клиент читает входящие сообщения в бесконечном цикле + :param client: сокет клиента + """ + while True: + # читаем сообщение + message = get_message(client) + print(message) + + +def write_messages(client, account_name): + """Клиент пишет сообщение в бесконечном цикле""" + while True: + # Вводим сообщение с клавиатуры + # Кому + user_name = input('user: ') + # Текст сообщения + text = input('text: ') + # Создаем jim сообщение + message = create_message(user_name, text, account_name) + # отправляем на сервер + send_message(client, message) + + +if __name__ == '__main__': + client = socket(AF_INET, SOCK_STREAM) # Создать сокет TCP + # Пытаемся получить параметры скрипта + addr = DEFAULT_HOST + port = DEFAULT_PORT + # Логин пользователя + account_name = input('Ваш login: ') + # Соединиться с сервером + client.connect((addr, port)) + # Создаем сообщение + presence = create_presence(account_name) + # Отсылаем сообщение + send_message(client, presence) + # Получаем ответ + response = get_message(client) + # Проверяем ответ + response = translate_response(response) + if response['response'] == OK: + # в одном потоке слушаем сообщения + reader = threading.Thread(target=read_messages, args=(client,)) + reader.start() + + # в главном потоке пишем сообщения + write_messages(client, account_name) diff --git a/config.py b/config.py new file mode 100644 index 0000000..4d87ad1 --- /dev/null +++ b/config.py @@ -0,0 +1,43 @@ +"""Константы для jim протокола, настройки""" +# Ключи +ACTION = 'action' +TIME = 'time' +USER = 'user' +ACCOUNT_NAME = 'account_name' +USER_ID = 'user_id' +RESPONSE = 'response' +ERROR = 'error' +ALERT = 'alert' +QUANTITY = 'quantity' + +# Значения +PRESENCE = 'presence' +MSG = 'msg' +TO = 'to' +FROM = 'from' +MESSAGE = 'message' +GET_CONTACTS = 'get_contacts' +CONTACT_LIST = 'contact_list' +ADD_CONTACT = 'add_contact' +DEL_CONTACT = 'del_contact' + +# Коды ответов (будут дополняться) +BASIC_NOTICE = 100 +OK = 200 +ACCEPTED = 202 +WRONG_REQUEST = 400 # неправильный запрос/json объект +SERVER_ERROR = 500 + +# Кортеж из кодов ответов +RESPONSE_CODES = (BASIC_NOTICE, OK, ACCEPTED, WRONG_REQUEST, SERVER_ERROR) + +USERNAME_MAX_LENGTH = 25 +MESSAGE_MAX_LENGTH = 500 + +ENCODING = 'utf-8' + +# Кортеж действий +ACTIONS = (PRESENCE, MSG, GET_CONTACTS, CONTACT_LIST, ADD_CONTACT, DEL_CONTACT) + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 7777 \ No newline at end of file diff --git a/errors.py b/errors.py new file mode 100644 index 0000000..bdf914e --- /dev/null +++ b/errors.py @@ -0,0 +1,30 @@ +"""Все ошибки""" + + +class UsernameToLongError(Exception): + def __init__(self, username): + self.username = username + + def __str__(self): + return 'Имя пользователя {} должно быть менее 26 символов'.format(self.username) + + +class ResponseCodeError(Exception): + def __init__(self, code): + self.code = code + + def __str__(self): + return 'Неверный код ответа {}'.format(self.code) + + +class ResponseCodeLenError(ResponseCodeError): + def __str__(self): + return 'Неверная длина кода {}. Длина кода должна быть 3 символа.'.format(self.code) + + +class MandatoryKeyError(Exception): + def __init__(self, key): + self.key = key + + def __str__(self): + return 'Не хватает обязательного атрибута {}'.format(self.key) \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..46d1096 --- /dev/null +++ b/server.py @@ -0,0 +1,125 @@ +import asyncio +from config import * +from utils import bytes_to_dict, dict_to_bytes +import time + + +class ChatServerProtocol(asyncio.Protocol): + def __init__(self, connections): + # Все подключения + # 1. + self.connections = connections + + def connection_made(self, transport): + # 2. сохранение "сокет" + self.transport = transport + + def connection_lost(self, exc): + if isinstance(exc, ConnectionResetError): + print('Обрыв соединения') + del self.connections[self.transport] # удаляем из соединений + else: + # почему то всегда None + print(f'Ошибка при отключении клиента: {exc}') + + def data_received(self, data): + """ + Вызов происходит если на сервер приходит сообщение + 3. + """ + if data: + # читаем данный из байтов + message = bytes_to_dict(data) + # смотрим что нам отправили + print(f'Входящее сообщение: {message}') + # обрабатываем + self.message_handle_router(message) + + def message_handle_router(self, message): + """ + Распределятор сообщений по обработчикам. + параметром принимат JIM сообщение + """ + # смотрим тим сообщения + action = message[ACTION] + if action == PRESENCE: + self.presence_handle(message) + elif action == MSG: + self.new_msg_handle(message) + elif action == GET_CONTACTS: + pass + elif action == ADD_CONTACT: + pass + elif action == DEL_CONTACT: + pass + else: + self.send_error_message('Формат сообщения не распознан сервером!. Сообщение: {}'.format(message)) + + def send_error_message(self, text): + """ + Отправляет клиенту сообщение об ошибке + """ + response = {RESPONSE: WRONG_REQUEST, ALERT: text} + # self.transport - это наш канал общения с текущим клиентом + self.transport.write(dict_to_bytes(response)) + + def presence_handle(self, message): + """ + Обработчик presence + """ + # получаем имя пользователя + account_name = message[USER][ACCOUNT_NAME] + # формируем ответ + response = {RESPONSE: OK} + # отправляем self.transport - это текущий клиент + self.transport.write(dict_to_bytes(response)) + # добавляем клиента в соединения + # добавляем клиента в текущие соединения, его как ключ и его имя значением + self.connections[self.transport] = account_name + + def is_client_online(self, account_name): + """ + Проверяем наличие клиента в сети + :param account_name: + :return: + """ + # Имя клиента должно быть в списке подключений + if account_name in self.connections.values(): + return True + else: + return False + + def new_msg_handle(self, message): + """ + Обработка сообщений пользователей + """ + # текст сообщения + body = message[MESSAGE] + # от кого + from_ = message[FROM] + to = message[TO] + + # обработка сообщений. + if self.is_client_online(to): + for transport, account_name in self.connections.items(): + # Перебираем все подключения. + # Если один и тот же клиент будет сидеть с разных клиентов, + # он получит свое сообщение на все клиенты + if account_name == to: + # Формирует сообщение для отправки с сервера + response = {ACTION: MSG, TIME: time.time(), TO: account_name, FROM: from_, MESSAGE: body} + # Отправляем + transport.write(dict_to_bytes(response)) + else: + # Отправляем что клиента нету в сети + response = {RESPONSE: BASIC_NOTICE, ALERT: 'Клиента нет в сети'} + self.transport.write(dict_to_bytes(response)) + + +if __name__ == "__main__": + server_connections = {} # клиенты + + loop = asyncio.get_event_loop() + coro = loop.create_server(lambda: ChatServerProtocol(server_connections), DEFAULT_HOST, DEFAULT_PORT) + server = loop.run_until_complete(coro) + loop.run_forever() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..ef9112e --- /dev/null +++ b/utils.py @@ -0,0 +1,74 @@ +import json + +# Кодировка +ENCODING = 'utf-8' + + +def dict_to_bytes(message_dict): + """ + Преобразование словаря в байты + :param message_dict: словарь + :return: bytes + """ + # Проверям, что пришел словарь + if isinstance(message_dict, dict) or isinstance(message_dict, list): + # Преобразуем словарь в json + jmessage = json.dumps(message_dict) + # Переводим json в байты + bmessage = jmessage.encode(ENCODING) + # Возвращаем байты + return bmessage + else: + raise TypeError + + +def bytes_to_dict(message_bytes): + """ + Получение словаря из байтов + :param message_bytes: сообщение в виде байтов + :return: словарь сообщения + """ + # Если переданы байты + #print('Пришли байты', message_bytes) + if isinstance(message_bytes, bytes): + # Декодируем + jmessage = message_bytes.decode(ENCODING) + # Из json делаем словарь + message = json.loads(jmessage) + # Если там был словарь + if isinstance(message, dict) or isinstance(message, list): + # Возвращаем сообщение + return message + else: + # Нам прислали неверный тип + raise TypeError + else: + # Передан неверный тип + raise TypeError + + +def send_message(sock, message): + """ + Отправка сообщения + :param sock: сокет + :param message: словарь сообщения + :return: None + """ + # Словарь переводим в байты + bprescence = dict_to_bytes(message) + # Отправляем + sock.send(bprescence) + + +def get_message(sock): + """ + Получение сообщения + :param sock: + :return: словарь ответа + """ + # Получаем байты + bresponse = sock.recv(1024) + # переводим байты в словарь + response = bytes_to_dict(bresponse) + # возвращаем словарь + return response \ No newline at end of file