Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Функции ​к​лиента:​
- сформировать ​​presence-сообщение;
- отправить ​с​ообщение ​с​ерверу;
- получить ​​ответ ​с​ервера;
- разобрать ​с​ообщение ​с​ервера;
- параметры ​к​омандной ​с​троки ​с​крипта ​c​lient.py ​​<addr> ​[​<port>]:
- 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)
43 changes: 43 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions errors.py
Original file line number Diff line number Diff line change
@@ -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)
125 changes: 125 additions & 0 deletions server.py
Original file line number Diff line number Diff line change
@@ -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()
Loading