Эта статья только первая из цикла "прохожу тестовые задания". Подобными заметками я хочу показать другим начинающим программистам, с чем им придется столкнуться при собеседованиях на работу. Сам я изучаю питон (и не только) уже порядка 4 лет, но это только теория с практикой на своих пет-проектах, что, как оказалось, с реальным программированием не имеет ничего общего. Итак, хватит лирики.
Задача и попытки её решить
Сразу скажу, компанию и какие-либо имена я называть не буду. Задание было довольно простое, написать связку клиент сервер на сокетах с помощью pyZMQ для транслирования видео. Проблема нарисовалась сразу, я ни разу не работал с этой библиотекой. Я пошел смотреть уроки, читать документацию и смотреть примеры других людей на гитхабе с использованием этого фреймворка. Для начала решил сделать простой стрим, который посылал бы числа на клиента, а тот их выводил на экран, всё просто. Но ZMQ так не думает, сразу возникли проблемы с кодировкой. У меня банально не работала элементарная связка send_string
и recv_string
. Я даже решил создать топик на stackoverflow. И, о чудо - мне никто не ответил. Отчаявшись, я полез на гитхаб в поисках какой-либо реализации клиент-серверного приложения. Там-то я и нашел ответы на мои вопросы: нужно послать pyobject. Этим же способом можно отправлять видео. После череды багов я сделал это:
![Оно и правда работает Оно и правда работает](https://habrastorage.org/getpro/habr/upload_files/038/44e/372/03844e372c97281dafb5a9b17c7cfe38.png)
Сервер:
import zmq
import cv2
context = zmq.Context()
socket = context.socket(zmq.PUSH)
socket.bind("tcp://*:8000")
print('server started...')
try:
while True:
# получим объект видео
cap = cv2.VideoCapture('example.mp4')
while (cap.isOpened()):
# разбиваем по фреймам
ret, frame = cap.read()
if ret:
# передаём по одному фрейму, один фрейм это картинка
socket.send_pyobj(frame)
else:
# Сместим курсор на 0 фрейм
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
continue
finally:
socket.close()
Клиент:
import zmq
import cv2
context = zmq.Context()
socket = context.socket(zmq.PULL)
socket.connect("tcp://localhost:8000")
print("client started")
# Настройки для отображения видео
down_width = 400
down_height = 400
down_points = (down_width, down_height)
print('Чтобы закрыть видео нажмите q')
try:
while True:
image = socket.recv_pyobj()
image = cv2.resize(image, down_points, interpolation=cv2.INTER_LINEAR)
# показываем по одной картинке, что в итоге сложится в видео
cv2.imshow('frame', image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cv2.destroyAllWindows()
socket.close()
По сути мы разбиваем видео на фреймы и передаём их поочередно клиенту, он их по одному воспроизводит и получается, вроде как, стрим. Когда мы доходим до конца видео, то переносим курсор cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
на начальный фрейм и всё по новой. Вроде и видос транслируется, и даже поток есть, тз сделано.
Тут и начинаются проблемки, по сути тз мне дали из одного предложения, но с одной устной поправкой "есть пространство к творчеству". Многие из вас скажут: "тестовое задание и нужно для того, чтобы выпендриваться и показывать вообще все свои навыки программирования". И я скажу, что вы правы, но к сожалению, за 4 года в университете меня научили только решать простенькие алгоритмы и не более.
Поэтому, когда я скинул задание, то получил отказ и фидбэк. Огромное спасибо за это. Ведь благодаря замечаниям эта статья получит продолжение, где я попытаюсь исправить всё, на что мне указали.
Что ж тут не так?
Миша, всё х**ня, давай по новой. Ладно всё не так плохо, вот прямая цитата
"По поводу проекта тестового, если коротко, в целом задание выполнено, но в очень узком формате, хотелось бы что бы ты учел ошибки возникающие например при обрыве соединения, или при запуске нескольких серверов, также неплохо было бы добавить логирование, чтобы понимать что вообще происходит, и документирование кода тоже хотелось бы видеть"
Что ж теперь хотя бы понятен вектор того куда двигаться. Пойду изучать логирование, обрывы и многопоточность.
Я стал лучшим программистом, чем был вчера. Наверное...
С логированием всё понятно, его нужно добавить и вообще практика хорошая, лучше чем пользоваться принтами. В качестве инструмента, я не стал брать стандартную библиотеку logging, так как все говорят, что она хорошая, но недостаточно. Loguru мой выбор. Спасибо гайдерам на ютубе и хабру за пояснение теории логирования.
Окей, логи написаны, что там дальше? Обрывы соединения и несколько серверов. Непонятно зачем дублировать это, если библиотека zmq уже об этом позаботилась за нас, ну ладно, добавим обработку подобных ситуаций, чтобы программа не падала, а только оповещала нас об событии. Обернем инициализацию сокета в try except и добавим логи к этому.
Готово:
try:
context = zmq.Context()
socket = context.socket(zmq.PUSH)
logger.debug('Создание сокета')
socket.bind("tcp://*:8000")
logger.debug('Сервер запущен...')
except Exception as e:
logger.error(f'{e}')
return
Окей, теперь нельзя включить 2 сервера, до этого тоже нельзя было, так что непонятно, что я вообще сделал, ну хотя бы логируется. С разрывами соединения вообще непонятно, по сути связка Push-pull работает по принципу сервер отправляет данные и кто угодно может присоединиться и принимать их, если принимать некому, то ничего и не отправляют. Короче, надеюсь в комментариях мне подскажут, что я должен был сделать в рамках данной библиотеки.
Последние - это документирование кода, тут сложно придумать что-то новое, так как еще, когда я сдавал задание, у меня уже были комментарии в коде, а README в гитхаб выглядел так:
![Вроде достаточно чтобы запустить и оно работало Вроде достаточно чтобы запустить и оно работало](https://habrastorage.org/getpro/habr/upload_files/054/777/7a9/0547777a9ec1923e0632da8d391608a5.png)
Окей, добавлю еще комментариев в код, чтобы даже человеку, никогда не работавшему с библиотекой было плюс минус ясно, что я делаю, хотя считаю это избыточным и загрязняющим код.
Результат
Сервер:
import zmq
import cv2
from loguru import logger
# настройки логгера, а именно папка сохранения логов, в каком виде вести логоирование, через какое время удалять логи
logger.add('logs/server.log', format='{time} {level} {message}', level='INFO', rotation='1 week')
def main():
try:
# Создание сокета
context = zmq.Context()
socket = context.socket(zmq.PUSH)
# Привязка к сокету определенного хоста
logger.debug('Создание сокета')
socket.bind("tcp://*:8000")
logger.debug('Сервер запущен...')
except Exception as e:
logger.error(f'{e}')
return
try:
logger.info(f'Начало трансляции')
while True:
# получим объект видео
cap = cv2.VideoCapture('example.mp4')
while (cap.isOpened()):
# разбиваем по фреймам
ret, frame = cap.read()
if ret:
# передаём по одному фрейму, один фрейм это картинка
socket.send_pyobj(frame)
else:
# Сместим курсор на 0 фрейм
logger.debug('Видео началось заново')
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
continue
except Exception as e:
logger.error(f'{e}')
finally:
# закрываем сокет
socket.close()
if __name__ == '__main__':
main()
Клиент:
import zmq
import cv2
from loguru import logger
# настройки логгера, а именно папка сохранения логов, в каком виде вести логоирование, через какое время удалять логи
logger.add('logs/client.log', format='{time} {level} {message}', level='INFO', rotation='1 week')
def main():
# Соединение с сервером
context = zmq.Context()
socket = context.socket(zmq.PULL)
socket.connect("tcp://localhost:8000")
logger.debug("Присоеденились")
# Настройки для отображения видео
down_width = 400
down_height = 400
down_points = (down_width, down_height)
print('Чтобы закрыть видео нажмите q')
try:
while True:
# Принимаем по картинке
image = socket.recv_pyobj()
image = cv2.resize(image, down_points, interpolation=cv2.INTER_LINEAR)
# показываем по одной картинке, что в итоге сложится в видео
cv2.imshow('frame', image)
if cv2.waitKey(1) & 0xFF == ord('q'):
logger.info('Пользователь отключился')
break
except Exception as e:
logger.error(f'{Exception}')
finally:
# Закроем все окна и все сокеты
cv2.destroyAllWindows()
socket.close()
if __name__ == '__main__':
main()
Заключение
По итогу скажу, что каждый отказ делает нас более опытными, если мы пытаемся исправить ошибки. Статью я тоже не просто так пишу, а надеюсь умные люди сеньоры разработчики выскажут своё фи на мой говнокод и я смогу его исправить, если это и правда будет так, то можете увидеть еще более улучшенную версию этого кода на моём гитхабе.
А я пойду и дальше стучаться к компаниям, пытаясь устроиться на работу, выполнив тестовое задание, попутно конспектирую сиё действо сюда.