Как научиться читать компьютерный язык
Как изучать языки программирования
Я ни в коей мере не утверждаю, что указанный мной способ лучший из возможных. Более того, я вовсе не уверен в том, что он вообще правильный. Возможно, если бы моим первым языком был не Си, а какой-либо из функциональных языков или ассемблеров, моё мнение было бы иным, и жизнь моя сложилось бы совсем иначе. Так что весь нижеизложенный материал воспринимайте со здоровым скептицизмом.
Немного личных воспоминаний
С детства я мечтал о карьере биолога, но в тринадцать лет, впервые ознакомившись с компьютером в школе, задумал стать крутым хакером. Когда у меня в пятнадцать лет появился регулярный доступ к компьютеру, первой мыслью было изучение программирования. Мой выбор пал на C++, была куплена книга из серии «Для чайников», но всё было ужасно скучным, непонятным и неинтересным. Уже тогда я понимал, что с C++ что-то не так. И сам ход изложения материала в книге начинался с краткого экскурса в функции, после чего сразу переходили к классам, причём всё объяснялось не на живых примерах, а на уровне глобальных абстракций. Авторы с увлечением рассказывали о прекрасном мире ООП, в котором нам посчастливится жить, про машины с разными функциями или животных с разным поведением. В общем, в то время изучение C++ и сама концепция ООП были заброшены.
Спустя несколько лет, во время учёбы в университете, я увидел в киоске маленькую книгу «Язык программирования Си», Брайана Кернигана и Денниса Ритчи. Купив её, я сразу же приступил к чтению. И о чудо! Всё было ясно, легко и ужасно интересно. Язык C очень маленький, простой и элегантный (я всё ещё так считаю), а сама книга была написана нормальным человеческим языком, не лишённым однако строгости. Я перечитывал её много раз, выполнял упражнения, и, таким образом, вскоре освоил этот язык и стандартную библиотеку функций.
Но на чистом Си много не напишешь, а мне хотелось писать серьёзные программы с красивой графикой. Программировал я в Slackware Linux, ибо в Windows как в среде программирования полностью разочаровался. В те времена я вообще много экспериментировал с разными системами, включая QNX и NetBSD. Но в свободных Unix системах выбор вменяемых графических библиотек был невелик: GTK+ и Qt. Причём вторая была написана на C++, с которым я не желал связываться, а в первой не было ясно вообще, как работать. И, если мне не изменила память, их редакторы интерфейсов генерировали огромный код, который ещё нужно было вставлять в свою программу, а не ресурсные файлы, что казалось мне совершенно ужасным.
Так бы я и не начал карьеру, если бы случайно не познакомился с Mac OS X, которую недавно портировали на Intel процессоры. Попросив у знакомого диск с Mac OS X 10.5 Leopard, я установил её на свой HackBook и первый раз ощутил себя на Земле Обетованной.
Помня о своём неудачном первом разе, я ещё долгое время скептически относился к объектно-ориентированному программированию. В начале даже планировал писать программы на чистом Си, используя библиотеку Carbon. И, как выяснилось – зря. Objective-C я изучил по официальному руководству компании NeXT – Object-oriented programming and the Objective-C language. В те времена изучающих новый язык ещё не считали поголовно идиотами или школьниками, поэтому книга написана нормальным языком, а авторы предполагают знание Си и общее представление о программировании. Оказалось, что ООП – это легко, интересно и очень удобно, а объекты – вовсе не абстрактные сущности, а всего лишь структуры, объединённые с функциями. На мой взгляд, именно так и нужно рассказывать основы объектно-ориентированного программирования.
В Xcode и основам Mac OS X я разобрался по замечательным урокам Алексея Борескова. После чего сразу же нырнул в Mac Developer Library, взялся за изучение официальных руководств по Cocoa и прочим библиотекам, параллельно начав писать программы.
Моим первым серьёзным проектом было портирование уроков по OpenGL с Windows на Mac OS X, которое растянулось на целый год. Лишь закончив его, я почувствовал себя достаточно уверенно, чтобы устроиться на регулярную оплачиваемую работу.
Изучая Objective-C, нельзя не услышать о Smalltalk, ибо на него очень часто ссылаются в материалах по Objective-C, видимо, извиняясь за непривычный синтаксис. Но Smalltalk был похож на Неуловимого Джо – все о нём слышали, но мало кто его видел или использовал. В конце концов и мне стало интересно, откуда есть пошла русская земля.
Узнав, что компания Cincom – лидер в сфере решений на Smalltalk – в рекламных целях предоставляет свои инструменты для некоммерческого использования, я сразу же заказал их. Менее чем несколько недель в мою дверь постучал курьер и вручил мне фирменную коробку с несколькими CD дисками и официальным руководством. Всё было совершенно бесплатно, даже за доставку не пришлось платить. На одном из дисков обнаружился VisualWorks – одна из самых мощных и быстрых коммерческих сред по разработке на Smalltalk в мире.
После я узнал про Squeak – свободную реализацию Smalltalk, основанную на оригинальной версии Smalltalk-80, – и познакомился со многими её разработчиками. Для обучения и исследований он подходит намного больше, чем прочие версии. Кстати, сообщество программистов на Smalltalk очень дружное.
Почти все серьёзные проекты являются кроссплатформенными, и их ядра написаны на C++, ибо Objective-C очень мало распространён за пределами Apple экосистемы. Главным образом из-за отсутствия удобных средств разработки. (Будем надеяться, что благодаря LLVM ситуация вскоре улучшится.) Так что мне пришлось изучить C++, несмотря на явную нелюбовь к нему. К счастью, к этому времени я уже научился отделять зёрна от плевел.
Впрочем, хакером я так и не стал.
Порядок изучения языков
C – первый язык
Это маленький (всего 32 ключевых слова в C89, 37 слов в C99 и 44 слова в C11) императивный язык, и его первичное изучение не займёт у вас много времени. В то же время C – очень мощный промышленный язык. Главным образом используется в системном программировании, написании аппаратно независимых компонент операционных систем, компиляторов и трансляторов языков, библиотек, программировании микроконтроллеров. По версии TIOBE Programming Community Index Си всё ещё лидирует по использованию, изредка вежливо уступая первенство Java.
Objective-C – второй язык
Он представляет собой очень тонкий слой поверх чистого Си. Все знания, полученные при изучении Си понадобятся вам в Objective-C. Причём объектно-ориентированная парадигма концептуально и синтаксически отделена от процедурной: в нём вы никогда не спутаете вызов функции и посылку сообщения. Основное его применение – создание пользовательских программ на устройствах компании Apple.
Smalltalk – третий язык
После Objective-C он покажется вам очень знакомым. И не зря, ведь именно его синтаксис и объектная модель были положены в основу Objective-C. Интересен прежде всего чистой реализацией объектно-ориентированной парадигмы. Все конструкции языка, включая условия, циклы, управляющие структуры и операторы, реализованы через механизм сообщений и вынесены в стандартную библиотеку классов. Несмотря на малое распространение он используется в крупных сложных проектах, где важна масштабируемость, гибкость и возможность изменений во время работы. Например, в финансовой сфере (Kapital – система оценки и управления рисками в JPMorgan), управлении производством (CIM в Texas Instruments), логистике (IRIS-2 в Orient Overseas Container Lines), страховании (Desjardins General Insurance Group) и прочих.
После изучения указанных языков, все прочие можно учить в любом порядке, в зависимости от ваших личных предпочтений. Ниже я приведу список наиболее важных и интересных языков.
Очень избыточный (число ключевых слов превалило за 100), запутанный и сложный в изучении язык, со множеством исключений и массовыми нарушениями ОО парадигмы. Но если вы хотите работать в индустрии, знать его нужно, так как альтернативы ему не предвидится. На C++ написана большая часть научных и инженерных программ, игр, фото- видео- аудиоредакторов и много чего ещё. После того, как Objective-C и Smalltalk сформировали у вас правильное представление об ООП и программировании, C++ уже не сможет повредить ваш разум. Но всё равно будьте очень осторожны. Вообще, слово «знать» в применении к C++ имеет несколько иное значение, ибо знать его в том же смысле, в каком можно знать, например, Objective-C или Java мало реально. Хорошая новость: в большинстве случаев для успешной работы вам нужно освоить лишь небольшое подмножество языка. Великолепным примером грамотного использования C++ является I/O Kit – интерфейс программирования драйверов в Mac OS X.
Assembler
Ассемблеров существует очень много, намного больше, чем различных архитектур процессоров, так как они представляет собой всего лишь мнемонические коды машинных команд и различаются в разных операционных системах и их версиях. В настоящее время на них не пишут большие программы (исключая, разве что Demo сцену). Однако они широко используются при написании компьютерных вирусов и антивирусов, аппаратно зависимых компонент операционных систем, программировании микроконтроллеров, в реверсивной инженерии и отладке программ.
JavaScript
Пожалуй, главный язык программирования Всемирной Паутины. Основное его применение – создание динамических web страниц на клиентской стороне. Каждый браузер снабжён интерпретатором и средствами для анализа и разработки на нём. Если вы хотите заниматься web программированием, JavaScript – ваш выбор. В нём реализованы по меньшей мере три парадигмы: процедурная, функциональная и объектно-ориентированная, причём последняя основана на прототипах. На первый взгляд JavaScript кажется очень лёгким, ибо позволяет писать в C-подобной манере, но в реальности он сложный. Большинство JavaScript разработчиков программируют не с использованием языка и даже не на языке, а на одной из библиотек.
Язык Ada
По крайней мере его стоит выучить, чтобы шокировать знакомых священников громким заявлением. Шучу. Благодаря правильному дизайну, многочисленным встроенным механизмам и проверкам Ada является одним из самых безопасных языков программирования. Вряд ли вы будете регулярно использовать его (если, конечно, вам не повезло работать в Министерстве Обороны США, авиационной или космической сфере), но ознакомиться с ним крайне полезно. После его изучения вы узнаете, каким должен был быть C++.
Scheme
Функциональный язык, разработанный в Массачусетском Технологическом Институте специально для обучения студентов программированию. Один из диалектов знаменитого Lisp. Интересен своей элегантностью и минимализмом. В промышленности не замечен, главным образом используется (и весьма успешно) в образовательных учреждениях. Своё знакомство с миром функционального программирования лучше начать именно с него.
Знание разных языков позволит вам выбирать нужный язык под конкретную задачу, а не наоборот.
Особое мнение
Многие авторы советуют первыми изучать скриптовые языки высокого уровня, такие, как Shell, Perl, JavaScript или Scheme. Но лично я рекомендую первым языком учить именно Си. Скриптовые языки с автоматическим управлением памятью, скрывающие всю сложную работу с указателями глубоко в недрах программы, действительно легче в освоении, но все их преимущества можно в полной мере понять и оценить лишь после изучения языка уровнем ниже.
Когда вы научились считать каждый байт, привыкли проверять границы массивов, освоили ручное управление памятью и разобрались с указателями и адресной арифметикой, переход на любой скриптовый язык покажется вам лёгкой прогулкой. Обратное же, как правило, не верно. Если вы привыкли, что ваш интерпретатор всё делает за вас, освоиться в C или C++ будет намного сложнее, особенно, в процессе смены специализации. Ещё вы будете знать, что происходит под капотом вашей программы, и сможете примерно представлять в уме, как могли бы выглядеть на Си выражения используемого языка.
По схожей причине ряд учёных предлагает изучать физику не с классической, а с упрощённой версии квантовой механики. Чтобы у студентов сразу формировалось правильное представление о мире.
Более того, поскольку практически все интерпретаторы и компиляторы языков высокого уровня, а также их модули расширений (во всяком случае самые важные их компоненты) написаны на C, его знание будет вам очень полезным, если вы захотите внести в них изменения или исправления.
Также не нужно выбирать первым объектно-ориентированный язык. Не бегите впереди паровоза. Дело в том, что временные затраты на проектирование и реализацию классов весьма существенны. И в маленьких учебных программах польза от них не очевидна. ООП является логическим расширением процедурного программирования, и изучать его лучше именно в таком ключе. После того, как вы написали несколько сложных программ на процедурном языке, естественным путём пришли к объектным моделям, вам сразу будут ясны все преимущества языков, реализующих объектно-ориентированную парадигму. И ещё, на первом языке обычно изучают основы программирования и алгоритмы, а применение объектной парадигмы, требующей немалого объёма «лишней» работы, будет отвлекать ваше внимание.
Хороший пример объектной модели, реализованной на языке C – библиотека CoreFoundation в Mac OS X.
Как учить язык?
Изучение языка программирования очень похоже на изучение обычного человеческого языка общения. Чтобы выучить новый язык вам понадобятся:
Во-первых, хороший учебник. Не «Для чайников» и не «За 21 день». Лучше, если это будет учебник именно по языку, а не руководство по созданию и выкладыванию игры в App Store (в котором процессу выкладывания посвящено больше времени, чем самому программированию). Во-вторых, стандарт языка ANSI/ISO и справочник по стандартной библиотеке. И, в-третьих, учебные и реальные примеры, которые можно найти в Сети.
Читайте учебники, выполняйте упражнения, изучайте чужие и пишите свои учебные примеры. Экспериментируйте с языком, например, попробуйте реализовать на нём несвойственные ему парадигмы. Полезное упражнение не только поможет вам лучше прочувствовать язык, его достоинства и недостатки, но и приведёт вас к правильной мысли – не существует универсальных языков. Бывают языки хорошо или плохо спроектированные.
Лучшее упражнение – это написание небольших программ, реализующих различные структуры данных и алгоритмы – деревья, списки, таблицы, графы, поиск и сортировку из книги «Алгоритмы. Построение и анализ», Томас Кормен, Чарльз Лейзерсон, Рональд Ривест, Клиффорд Штайн (Introduction to Algorithms by Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein). Несложные функции стандартной библиотеки или утилиты командной строки. Не нужно пытаться сразу писать большие и сложные программы, всё равно вы ещё плохо знаете язык.
Ещё один хороший способ лучше разобраться в языке – портировать на него учебные примеры или реальную программу с другого языка. По сравнению с разработкой программы с нуля, при портировании у вас уже есть образец перед глазами, и вы сможете всецело сфокусироваться на языке, а не на архитектуре и проектировании. В отличие от многих моих коллег лично я не считаю портирование скучным занятием.
У многих на фоне первых успехов возникает желание сразу начать писать большие серьёзные программы. Я крайне не рекомендую вам следовать их примеру. Взявшись за сложный проект, вы через месяц обнаружите, что ваш первый код был ужасен и приметесь его переписывать, вполне возможно, вам потребуется ещё несколько итераций. В результате вы потратите время и силы на то, что нужно отрабатывать на учебных или небольших программах. Так что не торопитесь – к счастью, примерно за год изучения и упражнений легко можно выйти на приемлемый уровень коммерческой разработки.
Даже если вы опытный программист, лучше вначале разберитесь в языке – как известно, программисты на Fortran (в основном касается ранних версий) могут на любом языке писать как на Fortran. Дело в том, что язык программирования не ограничивается синтаксисом и семантикой, кроме них он включает в себя набор шаблонов, приёмов, идиом и парадигм характерных именно для него. За неделю разобраться в них не представляется возможным.
Подпишитесь на каналы и новостные группы, списки рассылки, посвящённые вашему языку. Ценным источником информации и ответов на многие вопросы являются профильные сообщества в stackexchange.com и аналогичных ресурсах.
В последнее время получил широкое распространение иная стратегия – нацеленная на немедленный результат. Появилось множество книг и курсов, обещающих сделать из человека без малейшего представления о программировании высококлассного специалиста менее чем за месяц. Немало людей посещают подобные курсы (за приличные деньги), учатся писать screensaver, радостно публикуют его в Сети и получает на выходе красивый сертификат с которым бегут устраиваться в фирмы. Подобный подход опасен тем, что создаёт иллюзию знания: человек наспех выучивает одну технологию (как правило не самую лучшую) и считает её единственно возможной.
Программирование с нуля
Данная статья описывает основные конструкции в программировании и предназначена для тех, кто хочет в этом разобраться. Но статья не описывает все нюансы, потому что их слишком много. Если описывать их все, будет очень нудно и непонятно.
Использовать будем си-подобный синтаксис, то есть подобный языку си, но не будем вникать в заголовочные файлы, указатели и другие особенности относительно низкоуровневых языков, перейдём на синтаксис более высокоуровневых языков, которые сделают рутинную работу за нас. А конкретно, будем использовать синтаксис языка Java. Добро пожаловать под кат.
Двоичная система счисления
Числа в двоичной системе счисления состоят всего из двух знаков. Нуля и единицы. 00000001 – число один. 00000010 – число два. 00000100 – число 4. Как вы можете заметить, когда единица смещается влево, число увеличивается в два раза. Чтобы получилось число 3, необходимо написать 00000011. Таким образом можно составить все необходимые числа. В данном примере мы использовали двоичное число с восемью знаками, иначе говоря число восьмиразрядное. Чем больше у числа разрядов, тем большее оно может вместить значение. Например, восьмиразрядное число вмещает максимальное значение 255, если считать ноль, тогда 256, а в программировании ноль считается всегда. Если увеличить разряд на один, получится девятиразрядное число и его вместимость увеличится в два раза, то есть станет 512. Но так в программировании никогда не делается и обычно каждая следующая разрядность увеличивается вдвое. Один разряд, потом 2 разряда, потом 4 разряда, потом 8 разрядов, потом 16 разрядов, потом 32 разряда и далее.
Шестнадцатеричная система счисления
Всё аналогично двоичной, только вместо нулей и единиц участвуют цифры от 0 до 15. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, где A – 10, B – 11, C – 12, D – 13, E – 14, F – 15.
Знак минус в программировании
Буквы и знаки
Буквы, знаки, смайлики и так далее обозначаются также числами. Буква А может быть числом 00000001 или любым другим, или даже комбинацией чисел в зависимости от кодировки символов. Кодировок много.
Типы данных
В программировании есть типы данных. Числовые, такие как 233, которые разобрали выше. Называются почти везде int, от слова integer. С плавающей запятой, такие как 198,76, называются почти везде float. У букв тип char, у строк тип String. Тип bool имеет два значения – истина (true) и ложь (false). У этого типа реализация в разных языках разная, но самая простая, когда ноль — значит ложь, а любое другое число истину. Нестандартные типы данных, такие как числа с фиксированной запятой, рассматривать не будем.
Применение
Прежде чем использовать числа в программировании их нужно объявить, то есть сказать с помощью языка программирования, что они существуют.
Это стандартное объявление примитивного типа.
Сначала пишем тип, потом имя переменной, то есть нашего числа. Всегда заканчиваем наше выражение, да и любое, точкой с запятой.
Теперь мы можем использовать переменную по её имени.
Здесь мы присвоили переменной значение. В отличии от математики в программировании = значит взять значение справа и присвоить переменной слева. = — это знак/оператор присвоения.
Можно объединить объявление и присвоение, то есть сразу инициализировать переменную.
Буквы выделяются одинарными кавычками, строки выделяются двойными кавычками. Числа типа int не выделяются.
К числам с плавающей запятой одинарной точности в конце добавляется f.
К числам с плавающей запятой двойной точности ничего не добавляется.
Операторы
После того как мы записали наше выражение, например сложения,
получается значение. Но так как оно ни одной переменной не присваивается, оно исчезает. Чтобы присвоить значение переменной используется специальный оператор присвоения, который коротко описан выше.
Повторим ещё раз. Он берёт значение со своей правой стороны и присваивает его переменной в левой стороне. Это оператор =, и он не имеет ничего общего со знаком равно из математики.
Также у нас есть логические операторы, такие как (больше),
План изучения программирования сосредоточенный на проблемах новичков | Часть 0
Приветствую, примерно полтора месяца назад я написал пост о проблемах планов изучения программирования. Потом были посты о методах изучения, подходящих материалах, и т.д. и т.п., из них можно собрать примерный план, избежав каких то ошибок. Но все же, это скорее усложнит жизнь новичку, и только добавит вопросов. Поэтому, кажется хорошей идеей полностью прописать этот план.
Зачем нужен очередной план изучения программирования?
Мне не нравится вектор большинства планов, они не сосредоточены на проблемах новичков. Зачастую, это просто список учебных материалов с небольшими аннотациями. Списки эти могут разительно отличатся, проблемы у новичков на удивление похожи. Особенно паршиво, когда вовремя не обозначенная проблема, может на корню зарубить весь процесс изучения.
Может показаться, что это не задача плана изучения. Он всего лишь должен рассказать о хороших источниках, а уже задача источника правильно направить ученика, предвидеть ошибки и прочее. Чтобы разобраться в этом вопросе, копнем чуть глубже.
Почему так сложно понять проблемы новичков?
В посте, ссылка на который выше, как раз, о проблеме ‘непонимания самого процесса программирования’. Его зачастую не объясняют, поскольку это единственный способ программировать, другой еще не придумали, вот только новички не программировали и об этом не знают.
Отсюда и появляется проблема не намеренного ‘снобизма’. Вот как программисту объяснить работу циклов новичку? Она очевидна, буквально дословный перевод, показывает взаимодействия. Вот только новичок не понимает, что же делать? Объяснить внутряк, на более глубоком уровне, может так станет понятно? И вот тут появляется эта пропасть, когда простые вещи, вдруг становятся невероятно сложными, непонимание новичка может быть на другом уровне, нежели объяснение учителя.
Тоже касается и книг/курсов. Предположим программисту предложат прочитать курс по вводу в какой нибудь оояп. Как сделать этот курс максимально полезным? Ну разумеется все возможное время посвятить ООП. Курс точно так же, может быть хорош, может отлично объясняться парадигма и даваться интересные задачи, и при этом ученик не научиться программировать.
И как же это исправить?
Задача плана именно в том, чтобы выстроить эту картину. Не просто рассказать о хорошем материале, но и объяснить, что именно нужно из него получить, какую роль сыграют эти знания в общем прогрессе. Изучающий должен понимать на каком он этапе, куда движется, и главное, каким образом, ему поможет то, что он учит сейчас. Более того, объяснять как двигаться по материалу, рассматривая возможные проблемы и их решения.
Кому то покажется, что от подобного подхода теряется какая то роль открытия, как будто добавляются спойлеры, которых быть не должно. Но именно этот подход позволяет не просто идти по материалам, надеясь, что в конечном итоге все сложится и нужные навыки приобретаются(и будет очень обидно, если нет). Но и взаимодействовать с ним на другом уровне, самостоятельно видеть возможные пробелы, в определенных местах отступать от плана, к примеру если материал в источнике подан не понятно, но при этом ученик знает, что конкретно ищет, он вполне может поискать это в другом месте, не теряя общей картины. Появляется контроль.
Ко всему прочему, именно этот подход позволяет рассматривать общие проблемы, их удобно разбивать на подзадачи, и с помощью плана, постепенно закрывать, идя по материалам, а не предполагать что они закроются сами собой. Ну и как итог, все это дает возможность не просто оценить прогресс, но и сам план. Может быть подход, который в нем заложен вовсе не близок, а понять это через ряд названий незнакомых материалов, очевидно, невозможно.
Почему же, не стартануть с CS сразу? Питон через автоматизацию даст кучу проектов, набьет руку, и приучит к коду. Новые техники ложатся на отработанную основу, к примеру, изучили разные сортировки, и вместо отдаленных от практики задач, можем применять их в своих проектах. Само изучение пойдет куда легче, основы уже есть из питона, т.е. меньше распыления и сосредоточенность на конкретных проблемах.
3 этап. Может показаться, что если уже есть понимание самого процесса, наложена практика с теорией, то цель достигнута. Это так, но все же стоит это все объединить, закрыв какие то непонятные моменты. Снова возвращаемся к Python, но уже с несколько другим, более глубоким взглядом.
Как можно планировать что либо хз. Программирование это имхо такой процесс при котором ты сска всегда находишься в начале и чувствуешь себя тупым (правда до того момента когда тебе встретится программист менее опытный)
Чё сказать то хотел?
А вот когда придет понимание хоть какое-то общее, только тогда переходить на пайтон и то на библиотеки типа черепаха и решать задачи там, только уже сложнее, заодно знакомясь с синтаксисом и особенностями.
А вот когда все начнет получаться на каких-то простеньких, но уже вполне работающих задачах (ну не знаю парсеры там пописать или простенькие игрушки или еще чего не особо сложного), вот уже далее больше ориентироваться на то к чему все-таки душа лежит и подбирать уже материалы более осознанно и тщательно с прицелом к чему все-таки по итогу хотелось бы прийти.
Сугубо имхо, на какую-то единственную истину и единственно верный подход к изучению не претендую.
А надо ли учиться?
Я понимаю, что даже при покупке курса надо будет что-то почитывать дополнительно, но на сколько это реально, например, для того же, скажем, юриста со знанием английского на уровне Pre-Intermediate?
Так много вопросов, много желания начать учиться и изменить жизнь к лучшему, уйти из старой нелюбимой профессии, и столько же здорового реализма, который заставляет сомневаться в собственных возможностях и оценивает память уже не как свежую студенческую, а немного поношенную, что значительно затруднит процесс. И после работы и нервяков голова тупит уже слегонца. И здоровую прокрастинацию никто не отменял. Поделитесь опытом, пожалуйста.
Пы.Сы. Не бейте сильно, если эта тема уже обсуждалась, а я пропустила или мой вопрос кому-то покажется глуповатым.
Интересная фича
Обидно
Только плати
Ответ на пост «Как войти в IT после 30, мой путь от офисного планктона до Middle Java разработчика в Сбербанке»
На всякий случай можно ещё раз нажать
Как войти в IT после 30, мой путь от офисного планктона до Middle Java разработчика в Сбербанке
поэтому три года назад, перегорев от творческой работы по раскладыванию пасьянса «косынка», я решил что-то в жизни поменять. Начал с изучения английского. Не стану превращать пост в подробное описание этого процесса, тем более есть уже замечательный пост от @L4rever, на эту тему. Добавлю лишь, что для пополнения словарного запаса мне очень зашёл сайт\приложение memrise который помог мне дотянуть свой словарик до 5000 слов примерно за год в бесплатном режиме. Хоть изучение английского и не имело решающего значения в моей дальнейшей судьбе, но весьма помогло.
Глава 1. Избавление от обязательств и накопление финансовой подушки
Как и у большинства людей старше 25 у меня была ипотека, благо не такая большая, всего на 1,5 миллиона (хоть какие-то преимущества жизни в пригороде провинциального города).
Поэтому прежде чем резко менять свою сферу деятельности, неплохо бы было погасить обязательства. Нужно было заработать денег, с этим вопросом я вышел в интернет.
Интернет рассказал мне о фрилансеров, различных специальностей, всяких копирайтерах, smm менеджерах и так далее. С копирайтингом у меня не особо получилось, за 50 заказов и около 30 часов работы я получил 1500 рублей. Писать тексты было скучно, бесконечные поправки и конкуренция в виде армии школьников за заказ стоимостью 35 рублей, 84 копейки.
Поэтому было решено попробовать второй путь. И тут мне повезло уже на второй день поисков на зарубежной бирже я смог найти заказчика, которому нужно было вести группы в ВК и Facebook для нескольких брендов. Работы было не сильно много, всего на пару часов в день. Платили тоже не супер, 15 тысяч, однако это на 13 500 больше чем зарплата копирайтера, поэтому я приступил к работе.
Также спустя примерно месяц поиска мне удалось найти работу на полный день в службе поддержки одной фирмы электронных сигарет, где зарплата была выше в два раза, чем аналогичные позиции на рынке. В требованиях было знание английского. Разумеется моего текущего уровня знания языка не хватало для прохождения собеседования, но благодаря разговорам с носителем (олды может даже вспомнят) я мог хоть как-то поддерживать разговор. К моему удивлению этого хватило, чтобы:
1) Устроится туда самому.
2) Устроить туда жену, со знанием языка на уровне летспик фромахарт используя хитроумное устройство, в виде наушника и меня с микрофоном.
Благо звонки были только на русском и английский нужен был чтобы в рабочей программке с английским интерфейсом заполнять данные.
Но как не может долго длится волна постов на пикабу, так и эффективные менеджеры не могли долго смотреть на зарплату выше рынка. В итоге через год наш коллектив был распущен, мы с женой остались без работы, но и без ипотеки и даже с финансовой подушкой на пол года. Обдумав все варианты дальнейшей жизни я решил, что:
Глава 2. Куда пойти
Есть люди способные учиться чему-то после работы или в выходные. Они могут потратить свой отпуск на курсы или экзамены, а зимние каникулы на собственные проекты. Я по хорошему завидовал таким людям, но прекрасно осознавал, что я ленивая жопа с низкой самодисциплиной и вот это вот всё мне не подходит. Мне нужны были какие-то курсы, желательно оффлайн, но обязательно с другими людьми рядом, с этим вопросом я вышел в интернет.
UPD. Сейчас многие крупные компании также открыли свои школы, видел такие у Яндекса и ВТБ, поэтому выбор стал шире.
Здание школы в Казани
На удивление не нашёл нормальных постов про эту школу на пикабу, хотя казалось бы тема актуальная. Поэтому расскажу подробно как проходит процесс поступления и обучения.
Так как школа бесплатная и фактически никаких ограничений по возрасту и образованию к поступающим не предъявляет (единственное это возраст 18+), желающих поступить приходит больше, чем существующее количество мест. Поэтому в качестве вступительных экзаменов там есть «Бассейны». Но перед тем как попасть на них нужно пройти две игры на сайте, на память и логику\алгоритмы, после чего записать видеоинтервью с коротким рассказом о себе и ждать приглашения на испытание бассейном.
Нет, плавательные шапочки можно отложить, это не совсем то
Они представляют собой месячное, пробное обучение, где каждый день тебе даётся несколько задач, решение которых ты должен предоставить через день, однако учитывая, что задачи дают ежедневно, фактически ты ограничен одним днём. После чего выполнение таких заданий проверяют два случайным образом выбранных ученика и внутренняя система (мулиннет). Каждую неделю проходит экзамен, по темам, которые ты должен был изучить за эту неделю и финальный экзамен на 28 день обучения.
Так выглядит учебное здание, около 300 рабочих станций с шахматным расположением.
Советую по возможности найти компанию людей и вместе проходить это обучение, так намного проще, если вообще не единственный способ успешно превозмочь все трудности. В феврале 2020 я прошел этот бассейн, первый на тот момент в Казани, школу только открыли. Но потом случился коронавирус, который заморозил сроки старта обучения и до мая я просто сидел дома слегка самообучаясь в ожидании новостей.
В мае мы приступили к обучению, при чём дистанционно из-за ограничений в массовых собраниях, что было неслыханно для школы 21 (Кстати кампус в Москве уже на тот момент работал два года) и добавило сложностей к обучению, ведь я опять остался на самоконтроле и без друзей за соседними компами. Благо в сентябре нам разрешили вернутся в Кампус и надев маски, перчатки, соблюдая дистанцию метр мы начали учится сообща, что было всё же намного лучше, чем ничего. Лучшей мотивацией для меня стала работа в команде, желание не подставить свою команду, да и в целом, коллективная работа более эффективная, особенно когда сталкиваешься с проблемой, которую не можешь решить несколько часов.
Для закрытия 9 уровня мне потребовался ровно год, я это сделал в мае 2021 и начал искать место стажировки, которая кстати в 99% случаев оплачиваемая.
Глава 4. Стажировка и работа
Офис какой-то ИТ-компании
Логично было бы подумать, что раз школа финансируется Сбером, то и работать ты обязан только там. Но это не так. Никаких финансовых или трудовых обязательств у учеников нет, в интернете ходит информация о штрафе в 50к за отчисление, но она устарела, этот штраф был лишь стимулирующей мерой не забрасывать школу, как оказалось она не работает и вызывает массу негатива, поэтому была заменена на дедлайны по набору уровней, при нарушении которых тебя просто отчисляют.
Затем школа помогает тебе состряпать твоё первое резюме и проводит различные ярмарки вакансий, куда приглашаются крупные компании, яндекс, мэйл и другие. Также ты можешь сам искать вакансии в любых фирмах, либо даже организовать собственный стартап, бывало и такое, как были и случаи стажировки в кампусах гугла с переездом в другие страны.
Но естественно самым простым, в плане организации, путём является стажировка в Сбере. Так как они прекрасно знают что ты стажёр и много от тебя ожидать не стоит, не получается таких недоразумений:
Мне опять же повезло, я прошел 4 собеседования в различные команды, по итогу получил 1 предложение на стажировку и 1 сразу в штат. Так не мудрствуя лукаво, в Июне я стал Junior Java разработчиком в Сбере в городе Иннополис (недалеко от Казани). Про город кстати отдельная тема, достойная целого поста, но можно просто почитать посты @veronichka.pb, где она подробно рассказывает о городе и процессе переезда в него.
Ну а спустя пол года, неделю назад, получил повышение (сейчас тру-погроммисты будут меня ругать, так как настоящий мидл это человек с опытом 2-3 года. И честно сказать будут правы, я считаю что получил повышение авансом, но в конце концов кто мы такие чтобы спорить с начальством)))
Касательно работы именно в Сбере, я очень доволен тем, что имею. Особенно сравнивая свой текущий опыт с отношением работодателей в не-айтишном прошлом, даже не беря в расчёт зарплату, а откинуть такой весомый аргумент довольно сложно =D. Коллектив поддерживает меня, несмотря на пробелы в моих знаниях и более низкую эффективность, отпроситься с работы для каких-то дел очень просто, а при необходимости легко можно работать удаленно. Ну и весьма неплохо иметь возможность переключится на другой проект, если текущий для тебя станет скучным, а проектов здесь огромное количество.
Кстати одним из главных аргументов, почему я выбрал именно эту компанию является отсутствие постоянно горящих дедлайнов, из-за чего многие перегорают, как например @OWIII, за вхождением в IT которого я внимательно следил. именно поэтому решил написать пост об этом сейчас, уже пройдя этот путь, а не в процессе его.
В конце хотел бы сказать, что ничего нереального во вхождение в ИТ-сферу нет, да вам будет легче, если вам 20 лет, нет детей и ипотеки, но рядом со мной прошёл путь человек с двумя детьми, ипотекой и 40 годами за плечами, который тоже сейчас работает разработчиком. И это не какая-то единичная ситуация, таких людей буквально сотни, даже на пикабу @vigerf, почитав мои комментарии решился начать свой путь, надеюсь он также поможет ответить на вопросы о школе в комментариях))
Не бойтесь начинать это в 30, 40, любое другое количество лет, намного страшнее как мне кажется, так и не попробовать.
Обычная история
Естественный отбор
Английский для программирования не важен!©
Как я делал систему оптического трекинга
Дело было в далеком 2015 году. В продаже только появились очки виртуальной реальности Oculus DK2, рынок VR игр быстро набирал популярность.
Возможности игрока в таких играх были невелики. Отслеживалось всего 6 степеней свободы движений головы — вращение (инерциалкой в очках) и перемещение в маленьком объеме в зоне видимости инфракрасной камеры, закрепленной на мониторе. Процесс игры представлял собой сидение на стуле с геймпадом в руках, вращение головой в разные стороны и борьбу с тошнотой.
Звучало не очень круто, но я увидел в этом возможность сделать что-то интересное, используя свой опыт в разработке электроники и жажду новых проектов. Как можно было эту систему улучшить?
Конечно, избавиться от геймпада, от проводов, дать возможность игроку свободно перемещаться в пространстве, видеть свои руки и ноги, взаимодействовать с окружением, другими игроками и реальными интерактивными предметами.
1) Берем несколько игроков, надеваем на них VR очки, ноутбук и датчики на руки, ноги и туловище.
2) Берем помещение, состоящее из нескольких комнат, коридоров, дверей, оборудуем его системой трекинга, вешаем датчики и магнитные замки на двери, добавляем несколько интерактивных предметов и создаем игру, в которой геометрия виртуальной локации точно повторяет геометрию реального помещения.
3) Создаем игру. Игра представляет собой многопользовательский квест, в котором несколько игроков надевают на себя оборудование и оказываются в виртуальном мире. В нем они видят себя, видят друг друга, могут ходить по локации, открывать двери и совместно решать игровые задачи.
Эту идею я рассказал своему товарищу, который неожиданно воспринял ее с большим энтузиазмом и предложил взять на себя организационные вопросы. Так мы решили мутить стартап.
Для реализации заявленного функционала, нужно было создать две основные технологии:
1) Костюм, состоящий из датчиков на руках, ногах и торсе, отслеживающий положения частей тела игрока
2) Система трекинга, отслеживающая игроков и интерактивные объекты в 3D пространстве.
Про разработку второй технологии и пойдет речь в этой статье. Может быть, позже напишу и про первую.
Бюджета на все это, конечно, у нас не было, поэтому нужно было сделать все из подручных материалов. Для задачи отслеживания игроков в пространстве я решил использовать оптические камеры и светодиодные маркеры, закрепленные на VR очках. Опыта подобных разработок у меня не было, но я уже что-то слышал про OpenCV, Python, и подумал, что справлюсь.
По задумке, если система знает где расположена камера и как она ориентирована, то по положению изображения маркера на кадре можно определить прямую в 3D пространстве, на которой этот маркер находится. Пересечение двух таких прямых дает итоговое положение маркера.
Соответственно, камеры нужно было закрепить на потолке так, чтобы каждая точка пространства просматривалась минимум двумя камерами (лучше больше, чтобы избежать перекрытия обзора телами игроков). Для покрытия трекингом предполагаемого помещения площадью около 100 кв.м., требовалось около 60 камер. Я выбрал первые попавшиеся дешевые на тот момент usb вебки.
Эти вебки нужно к чему-то подключать. Эксперименты показали, что при использовании usb удлинителей (по крайней мере, дешевых), камеры начинали глючить. Поэтому решил разделить вебки на группы по 8 штук и втыкать их в системники, закрепленные на потолке. На моем домашнем компе как раз было 10 usb портов, так что пришло время начинать разработку тестового стенда.
Архитектуру я придумал следующую:
На каждые очки вешается акриловый матовый шарик от гирлянды с вклеенным внутрь RGB светодиодом. Одновременно в игре предполагалось несколько игроков, так что для идентификации решил разделять их по цвету – R, G, B, RG, RB, GB, RB. Вот так это выглядело:
Первая задача, которую нужно выполнить – написать программу поиска шарика на кадре.
Поиск шарика на кадре
Мне нужно было в каждом кадре, пришедшем с камеры, искать координаты центра шарика и его цвет для идентификации. Звучит несложно. Качаю OpenCV под Python, втыкаю камеру в usb, пишу скрипт. Для минимизации влияния лишних объектов на кадре, выставляю экспозицию и выдержку на камере в самый минимум, а яркость светодиода делаю высокой, чтобы получить яркие пятна на темном фоне. В первой версии алгоритм был следующий:
1) Переводим изображение в градации серого
2) Бинаризуем по порогу (если яркость пикселя больше порога, он становится белым, иначе – черным). При этом размытое пятно от шарика превращается в кластер белых пикселей на черном фоне
3) Находим контуры кластеров и их центры. Это и есть координаты шарика на кадре
4) Определяем усредненный цвет пикселей кластера (на исходном цветном изображении) в окрестности его центра для идентификации
Вроде, работает, но есть нюансы.
Во-первых, на дешевой камере матрица довольно шумная, что приводит к постоянным флуктуациям контуров бинаризованных кластеров и соответственно к дерганью центра. Нельзя, чтобы у игроков дергалась картинка в VR очках, поэтому нужно было эту проблему решать. Попытки применять другие виды адаптивной бинаризации с разными параметрами не давали большого эффекта.
Во-вторых, разрешение камеры всего лишь 640*480, поэтому на некотором расстоянии (не очень большом) шарик виден как пара пикселей на кадре и алгоритм поиска контуров перестает нормально работать.
Пришлось придумывать новый алгоритм. В голову пришла следующая идея:
1) Переводим изображение в градации серого
2) Размываем картинку мощным Gaussian blur –ом так, чтобы изображения светодиодов превратились в размытые пятна с градиентом яркости от центра к периферии
3) Находим самые яркие пиксели на изображении, они должны соответствовать центрам пятен
4) Так же определяем средний цвет кластера в окрестности центра
Так работает гораздо лучше, координаты центра при неподвижном шарике неподвижны, и работает даже при большом расстоянии от камеры.
Чтобы убедиться, что все это будет работать с 8-ю камерами на одном компе, нужно провести нагрузочный тест.
Подключаю 8 камер к своему десктопу, располагаю их так, чтобы каждая видела светящиеся точки и запускаю скрипт, где описанный алгоритм работает в 8-ми независимых процессах (спасибо питонской либе «multiprocessing») и обрабатывает все потоки сразу.
И… сразу натыкаюсь на фейл. Изображения с камер то появляются, то исчезают, framerate скачет от 0 до 100, кошмар. Расследование показало, что часть usb портов на моем компе подключены к одной шине через внутренний хаб, из-за чего скорость шины делится между несколькими портами и ее уже не хватает на битрейт камер. Втыкание камер в разные порты компа в разных комбинациях показало, что у меня всего 4 независимых usb шины. Пришлось найти материнку с 8-ю шинами, что было довольно непростым квестом.
Продолжаю нагрузочный тест. На этот раз все камеры подключились и выдают нормальные потоки, но сразу сталкиваюсь со следующей проблемой – низкий fps. Процессор загружен на 100% и успевает обрабатывать лишь 8-10 кадров в секунду с каждой из восьми вебок.
Похоже, нужно оптимизировать код. Узким местом оказалось Гауссово размытие (оно и не удивительно, ведь нужно на каждый пиксель кадра производить свертку с матрицей 9*9). Уменьшение ядра не спасало ситуацию. Пришлось искать другой метод нахождения центров пятен на кадрах.
Решение удалось найти внезапно во встроенной в OpenCV функции SimpleBlobDetector. Она делает прямо то, что мне нужно и очень быстро. Преимущество достигается благодаря последовательной бинаризации изображения с разными порогами и поиску контуров. Результат – максимальные 30 fps при загрузке процессора меньше 40%. Нагрузочный тест пройден!
Классификация по цвету
Следующая задача – классификация маркера по его цвету. Усредненное значение цвета по пикселям пятна дает RGB компоненты, которые очень нестабильны и сильно меняются в зависимости от расстояния до камеры и яркости светодиода. Но есть отличное решение: перевод из RGB пространства с HSV (hue, saturation, value). В таком представлении пиксель вместо «красный», «синий», «зеленый», раскладывается на компоненты «тон», «насыщенность», «яркость». В этом случае насыщенность и яркость можно просто исключить и классифицировать только по тону.
И так, на данный момент я научился находить и идентифицировать маркеры на кадрах с большого количества камер. Теперь можно перейти к следующему этапу – трекингу в пространстве.
Я использовал pinhole модель камеры, в которой все лучи падают на матрицу через точку, находящуюся на фокусном расстоянии от матрицы.
По этой модели будет происходить преобразование двухмерных координат точки на кадре в трехмерные уравнения прямой в пространстве.
Для отслеживания 3D координат маркера нужно получить минимум две скрещивающиеся прямые в пространстве от разных камер и найти точку их пересечения. Увидеть маркер двумя камерами не сложно, но для построения этих прямых нужно, чтобы система знала все о подключенных камерах: где они висят, под какими углами, фокусное расстояние каждого объектива. Проблема в том, что все это неизвестно. Для вычисления параметров требуется некая процедура калибровки.
В первом варианте решил сделать калибровку трекинга максимально примитивной.
1) Вешаю первый блок из восьми камер на потолок, подключаю их к системнику, висящему там же, направляю камеры так, чтобы ими покрывался максимальный игровой объем.
2) С помощью лазерного нивелира и дальномера измеряю XYZ координаты всех камер в единой системе координат
3) Для вычисления ориентаций и фокусных расстояний камер, измеряю координаты специальных стикеров. Стикеры вешаю следующим образом:
В интерфейсе отображения картинки с камеры рисую две точки. Одну в центре кадра, другую в 200 пикселях справа от центра:
Если смотреть на кадр, эти точки падают куда-то на стену, пол или любой другой объект внутри помещения. Вешаю в соответствующие места бумажные наклейки и рисую на них точки маркером.
Измеряю XYZ координаты этих точек с помощью тех же нивелира и дальномера. Итого для блока из восьми камер нужно измерить координаты самих камер и еще по две точки на каждую. Т.е. 24 тройки координат. А таких блоков должно быть около десяти. Получается долгая муторная работа. Но ничего, позже сделаю калибровку автоматизированной.
Запускаю процесс расчета на основе измеренных данных.
Есть две системы координат: одна глобальная, связанная с помещением, другая локальная для каждой камеры. В моем алгоритме результатом для каждой камеры должна получиться матрица 4*4, содержащая ее местоположение и ориентацию, позволяющая преобразовать координаты из локальной в глобальную систему.
1) Берем исходную матрицу с нулевыми поворотами и смещением.
2) Берем единичный вектор в локальной системе камеры, который смотрит из объектива вперед и преобразуем его в глобальные координаты по исходной матрице.
3) Берем другой вектор в глобальной системе, который из камеры смотрит на центральную точку на стене.
4) С помощью градиентного спуска поворачиваем исходную матрицу так, чтобы после преобразования эти векторы были сонаправлены. Таким образом, мы зафиксировали направление камеры. Осталось зафиксировать вращение вокруг этого направления. Для этого и измерялась вторая точка в 200 пикселях от центра кадра. Поворачиваем матрицу вокруг главной оси, пока два вектора не станут достаточно параллельны.
5) По расстоянию между этими двумя точками вычисляю фокусное расстояния в пикселях (учитывая, что расстояние между проекциями этих точек на кадре составляет 200 пикселей).
Наверняка эту задачу можно было решить аналитически, но для простоты я использовал численное решение на градиентном спуске. Это не страшно, т.к. вычисления будут проводиться один раз после монтажа камер.
Для визуализации результатов калибровки я сделал 2D интерфейс с картой, на которой скрипт рисует метки камер и направления, в которых они видят маркеры. Треугольником обозначаются ориентации камер и углы обзора.
Можно приступать к запуску визуализации, которая покажет правильно ли определились ориентации камер и правильно ли интерпретируются кадры. В идеале, линии, идущие из значков камер должны пересекаться в одной точке.
Похоже на правду, но точность явно могла быть выше. Первая причина несовершенства, которая пришла в голову – искажения в объективах камер. Значит, нужно эти искажения как-то компенсировать.
У идеальной камеры важный для меня параметр только один – фокусное расстояние. У реальной кривой камеры нужно учитывать еще дисторсии объектива и смещение центра матрицы.
Для измерения этих параметров есть стандартная процедура калибровки, в процессе которой измеряемой камерой делают набор фотографий шахматной доски, на которых распознаются углы между квадратами с субпиксельной точностью.
Результатом калибровки является матрица, содержащая фокусные расстояния по двум осям и смещение матрицы относительно оптического центра. Все это измеряется в пикселях.
А также вектор коэффициентов дисторсии, который позволяет компенсировать искажения объектива с помощью преобразований координат пикселей.
Применяя преобразования с этими коэффициентами к координатам маркера на кадре, можно привести систему к модели идеальной pinhole камеры.
Провожу новый тест трекинга:
Уже гораздо лучше! Выглядит настолько хорошо, что даже вроде будет работать.
Вычисление координат маркера
И так, я получил кучу прямых, разбросанных по пространству, на пересечениях которых должны находиться маркеры. Только вот прямые в пространстве на самом деле не пересекаются, а скрещиваются, т.е. проходят на некотором расстоянии друг от друга. Моя задача – найти точку, максимально близкую к обеим прямым. Формально говоря, нужно найти середину отрезка, являющегося перпендикуляром к обеим прямым.
Длина отрезка AB тоже пригодится, т.к. она отражает «качество» полученного результата. Чем он короче, тем ближе друг к другу прямые, тем лучше результат.
Затем я написал алгоритм трекинга, который попарно вычисляет пересечения прямых (внутри одного цвета, от камер, находящихся на достаточном расстоянии друг от друга), ищет лучшее и использует его как координаты маркера. На следующих кадрах старается использовать ту же пару камер, чтобы избежать скачка координат при переходе на трекинг другими камерами.
Параллельно, при разработке костюма с датчиками, я обнаружил странное явление. Все датчики показывали разные значения угла рысканья (направления в горизонтальной плоскости), как будто у каждого был свой север. В первую очередь полез проверять не ошибся ли я в алгоритмах фильтрации данных или в разводке платы, но ничего не нашел. Потом решил посмотреть на сырые данные магнитометра и увидел проблему.
Магнитное поле в нашем помещении было направлено ВЕРТИКАЛЬНО ВНИЗ! Видимо, это связано с железом в конструкции здания.
Но ведь в VR очках тоже используется магнитометр. Почему у них такого эффекта нет? Иду проверять. Оказалось, что в очках он тоже есть… Если сидеть неподвижно, можно заметить, как виртуальный мир медленно, но верно вращается вокруг тебя в рандомную сторону. За минут 10 он уезжает почти на 180 градусов. В нашей игре это неминуемо приведет к рассинхрону виртуальной и реальной реальностей и сломанным об стены очкам.
Похоже, что помимо координат очков, придется определять и их направление в горизонтальной плоскости. Решение напрашивается само – ставить на очки не один, а два одинаковых маркера. Оно позволит определять направление с точностью до разворота на 180 градусов, но с учетом наличия встроенных инерциальных датчиков, этого вполне достаточно.
Система в целом работала, хоть и с небольшими косяками. Но было принято решение запустить квест, который как раз был близок к завершению нашим gamedev разработчиком, присоединившимся к нашей миникоманде. Была затречена вся игровая площадь, установлены двери с датчиками и магнитными замками, изготовлено два интерактивных предмета:
Игроки надевали очки, костюмы и рюкзаки-компьютеры и заходили в игровую зону. Координаты трекинга отсылались им по wi-fi и применялись для позиционирования виртуального персонажа. Все работало достаточно неплохо, посетители довольны. Приятнее всего было наблюдать ужас и крики особо впечатлительных посетителей в моменты, когда на них из темноты нападали виртуальные призраки =)
Внезапно нам прилетел заказ на большой VR шутер на 8 игроков с автоматами в руках. А это 16 объектов, которые нужно тречить. Повезло, что сценарий предполагал возможность разделения трекинга на две зоны по 4 игрока, поэтому я решил, что проблем не будет, можно принимать заказ и ни о чем не волноваться. Протестировать систему в домашних условиях было невозможно, т.к. требовалась большая площадь и много оборудования, которое будет куплено заказчиком, поэтому до монтажа я решил потратить время на автоматизацию калибровки трекинга.
Первым делом нужно было централизовать всю систему. Вместо разделения игровой зоны на блоки по 8 камер, я сделал единый сервер, на который приходили координаты точек на кадрах всех камер сразу.
1) вешаю камеры и на глаз направляю их в игровую область
2) запускаю режим записи на сервере, в котором все приходящие с камер 2D точки сохраняются в файл
3) хожу по темной игровой локации с маркером в руках
4) останавливаю запись и запускаю расчет калибровочных данных, при котором вычисляются расположения, ориентации и фокусные расстояния всех камер.
5) в результате предыдущего пункта получается единое пространство, наполненное камерами. Т.к. это пространство не привязано к реальным координатам, оно имеет случайное смещение и поворот, которое я вычитаю вручную.
Пришлось перелопатить огромное количество материала по линейной алгебре и написать многие сотни строк питонского кода. Настолько много, что я уже почти не помню как оно работает.
Вот так выглядит напечатанная на принтере специальная палка-калибровалка.
Тестирование большого проекта
Проблемы начались во время тестирования на объекте за пару недель до запуска проекта. Идентификация 8-ми разных цветов маркеров работала ужасно, тестовые игроки постоянно телепортировались друг в друга, некоторые цвета вообще не отличались от внешних засветок в помещении торгового комплекса. Тщетные попытки что-то исправить с каждой бессонной ночью все сильнее вгоняли меня в отчаяние. Все это осложнялось нехваткой производительности сервера при расчете десятков тысяч прямых в секунду.
Когда уровень кортизола в крови превысил теоретический максимум, я решил посмотреть на проблему с другой стороны. Как можно сократить количество разноцветных точек, не сокращая количество маркеров? Сделать трекинг активным. Пускай у каждого игрока, например, левый рог всегда корит красным. А второй иногда загорается зеленым по приходу команды с сервера так, что в один момент времени он горит только у одного игрока. Получается, что зеленая лампочка будет как-будто перепрыгивать с одного игрока на другого, обновляя привязку трекинга к красной лампочке и обнуляя ошибку ориентации магнитометра.
Для этого пришлось бежать в ближайший чипидип, покупать светодиоды, провода, транзисторы, паяльник, изоленту и на соплях навешивать функционал управления светодиодами на плату костюма, которая на это рассчитана не была. Хорошо, что при разводке платы я на всякий случай повесил пару свободных ног stm-ки на контактные площадки.
Алгоритмы трекинга пришлось заметно усложнить, но в итоге все заработало! Телепортации игроков друг в друга исчезли, нагрузка на процессор упала, засветки перестали мешать.
Проект был успешно запущен, первым делом я сделал новые платы костюмов с поддержкой активного трекинга, и мы произвели обновление оборудования.
Чем все закончилось?
За 3 года мы открыли множество развлекательных точек по всему миру, но коронавирус внес свои коррективы, что дало нам возможность сменить направление работы в более общественно-полезную сторону. Теперь мы довольно успешно занимаемся разработкой медицинских симуляторов в VR. Команда у нас все еще маленькая и мы активно стремимся расширять штат. Если среди читателей есть опытные разработчики под UE4, ищущие работу, пожалуйста, напишите мне.
Традиционный забавный момент в конце статьи:
Периодически при тестах с большим количеством игроков возникал глюк, при котором игрока внезапно на короткое время телепортировало на высоту несколько метров, что вызывало соответствующую реакцию. Дело оказалось в том, что моя модель камеры предполагала пересечение матрицы с бесконечной прямой, идущей от маркера. Но она не учитывала, что у камеры есть перед и зад, так что система искала пересечение бесконечных прямых, даже если точка находится за камерой. Поэтому возникали ситуации, когда две разные камеры видели два разных маркера, но система думала, что это один маркер на высоте в несколько метров.
Система в прямом смысле работала через задницу =)
Им лучше не знать своих кумиров
Вот уже на протяжении нескольких лет Тимофей, преподаватель кафедры информатики МФТИ, выкладывает свои лекции по программированию на своём Youtube канале с открытым доступом.
Не заметил подвоха
Разработка системы заметок с нуля. Часть 2: REST API для RESTful API Service + JWT + Swagger
Продолжаем серию материалов про создание системы заметок. В этой части мы спроектируем и разработаем RESTful API Service на Go cо Swagger и авторизацией. Будет много кода, ещё больше рефакторинга и даже немного интеграционных тестов.
В первой части мы спроектировали систему и посмотрели, какие сервисы требуются для построения микросервисной архитектуры.
Подробности в видео и текстовой расшифровке под ним.
Начнём с макетов интерфейса. Нам нужно понять, какие ручки будут у нашего API и какой состав данных он должен отдавать. Макеты мы будем делать, чтобы понять, какие сущности, поля и эндпоинты нам нужны. Используем для этого онлайн-сервис NinjaMock. Он подходит, если макет надо сделать быстро и без лишних действий.
Страницу регистрации сделаем простую, с четырьмя полями: Name, Email, Password и Repeat Password. Лейблы делать не будем, обойдемся плейсходерами. Авторизацию сделаем по юзернейму и паролю.
После входа в приложение пользователь увидит список заметок, который будет выглядеть примерно так:
Интерфейс, который будет у нашего веб-приложения:
— Слева — список категорий любой вложенности.
— Справа — список заметок в виде карточек, который делится на два списка: прикреплённые и обычные карточки.
— Каждая карточка состоит из заголовка, который урезается, если он очень длинный.
— Справа указано, сколько секунд/минут/часов/дней назад была создана заметка.
— Тело заголовка — отрендеренный Markdown.
— Панель инструментов. Через неё можно изменить цвет, прикрепить или удалить заметку.
Тут важно отметить, что файлы заметки мы не отображаем и не будем запрашивать у API для списка заметок.
Полная карточка открывается по клику на заметку. Тут можно сразу отобразить полностью длинный заголовок. Высота заметки зависит от количества текста. Для файлов появляется отдельная секция. Мы их будем получать отдельным асинхронным запросом, который не помешает пользователю редактировать заметку. Файлы можно скачать по ссылке, также есть отдельная кнопка на добавление файлов.
Так будет выглядеть открытая заметка
В ходе прототипирования стало понятно, что в первой части мы забыли добавить еще один микросервис — TagsService. Он будет управлять тегами.
Для страниц авторизации и регистрации нам нужны эндпоинты аутентификации и регистрации соответственно. В качестве аутентификации и сессий пользователя мы будем использовать JWT. Что это такое и как работает, разберём чуть позднее. Пока просто запомните эти 3 буквы.
Для страницы списка заметок нам нужны эндпоинты /api/categories для получения древовидного списка категорий и /api/notes?category_id=? для получения списка заметок текущей категории. Перемещаясь по другим категориям, мы будем отдельно запрашивать заметки для выбранной категории, а на фронтенде сделаем кэш на клиенте. В ходе работы с заметками нам нужно уметь создавать новую категорию. Это будет метод POST на URL /api/categories. Также мы будем создавать новый тег при помощи метода POST на URL /api/tags.
Чтобы обновить заметку, используем метод PATCH на URL /api/notes/:uuid с измененными полями. Делаем PATCH, а не PUT, потому что PUT требует отправки всех полей сущности по спецификации HTTP, а PATCH как раз нужен для частичного обновления. Для отображения заметки нам ещё нужен эндпоинт /api/notes/:uuid/files с методами POST и GET. Также нам нужно скачивать файл, поэтому сделаем метод GET на URL /api/files/:uuid.
Структура репозитория системы
Ещё немного общей информации. Структура репозитория всей системы будет выглядеть следующим образом:
В директории app будет исходный код сервиса (если он будет). На уровне с app будут другие директории других продуктов, которые используются с этим сервисом, например, MongoDB или ELK. Продукты, которые будут использоваться на уровне всей системы, например, Consul, будут в отдельных директориях на уровне с сервисами.
Писать будем на Go
— Идём на официальный сайт.
— Копируем ссылку до архива, скачиваем, проверяем хеш-сумму.
— Распаковываем и добавляем в переменную PATH путь до бинарников Go
— Пишем небольшой тест проверки работоспособности, собираем бинарник и запускаем.
Установка завершена, всё работает
Теперь создаём проект. Структура стандартная:
— cmd — точка входа в приложение,
— internal — внутренняя бизнес-логика приложения,
— pkg — для кода, который можно переиспользовать из проекта в проект.
Я очень люблю логировать ход работы приложения, поэтому перенесу свою обёртку над логером logrus из другого проекта. Основная функция здесь Init, которая создает логер, папку logs и в ней файл all.log со всеми логами. Кроме файла логи будут выводиться в STDOUT. Также в пакете реализована поддержка логирования в разные файлы с разным уровнем логирования, но в текущем проекте мы это использовать не будем.
APIService будет работать на сокете. Создаём роутер, затем файл с сокетом и начинаем его слушать. Также мы хотим перехватывать от системы сигналы завершения работы. Например, если кто-то пошлёт приложению сигнал SIGHUP, приложение должно корректно завершиться, закрыв все текущие соединения и сессии. Хотел перехватывать все сигналы, но линтер предупреждает, что os.Kill и SIGSTOP перехватить не получится, поэтому их удаляем из этого списка.
Теперь давайте добавим сразу стандартный handler для метрик. Я его копирую в директорию pkg, далее добавляю в роутер. Все последующие роутеры будем добавлять так же.
Далее создаём точку входа в приложение. В директории cmd создаём директорию main, а в ней — файл app.go. В нём мы создаём функцию main, в которой инициализируем и создаём логер. Роутер создаём через ключевое слово defer, чтобы метод Init у роутера вызвался только тогда, когда завершится функция main. Таким образом можно выполнять очистку ресурсов, закрытие контекстов и отложенный запуск методов. Запускаем, проверяем логи и сокет, всё работает.
Но для разработки нам нужно запускать приложение на порту, а не на сокете. Поэтому давайте добавим запуск приложения на порту в наш роутер. Определять, как запускать приложение, мы будем с помощью конфига.
Создадим для приложения контекст. Сделаем его синглтоном при помощи механизма sync.Once. Пока что в нём будет только конфиг. Контекст в виде синглтона создаю исключительно в учебных целях, впоследствии он будет выпилен. В большинстве случаев синглтоны — необходимое зло, в нашем проекте они не нужны. Далее создаём конфиг. Это будет YAML-файл, который мы будем парсить в структуру.
В роутере мы вытаскиваем из контекста конфиг и на основании listen.type либо создаем сокет, либо вешаем приложение на порт. Код graceful shutdown выделяем в отдельный пакет и передаём на вход список сигналов и список интерфейсов io.Close, которые надо закрывать. Запускаем приложение и проверяем наш эндпоинт heartbeat. Всё работает. Давайте и конфиг сделаем синглтоном через механизм sync.Once, чтобы потом безболезненно удалить контекст, который создавался в учебных целях.
Теперь переходим к API. Создаём эндпоинты, полученные при анализе прототипов интерфейса. Тут важно отметить, что у нас все данные привязаны к пользователю. На первый взгляд, все ручки должны начинаться с пользователя и его идентификатора /api/users/:uuid. Но у нас будет авторизация, иначе любой пользователь сможет программно запросить заметки любого другого пользователя. Авторизацию можно сделать следующим образом: Basic Auth, Digest Auth, JSON Web Token, сессии и OAuth2. У всех способов есть свои плюсы и минусы. Для этого проекта мы возьмём JSON Web Token.
Работа с JSON Web Token
JSON Web Token (JWT) — это JSON-объект, который определён в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д., а также подписи (signature).
JWT использует преимущества подхода цифровой подписи JWS (Signature) и кодирования JWE (Encrypting). Подпись не даёт кому-то подделать токен без информации о секретном ключе, а кодирование защищает от прочтения данных третьими лицами. Давайте разберёмся, как они могут нам помочь для аутентификации и авторизации пользователя.
Аутентификация — процедура проверки подлинности. Мы проверяем, есть ли пользователь с полученной связкой логин-пароль в нашей системе.
Авторизация — предоставление пользователю прав на выполнение определённых действий, а также процесс проверки (подтверждения) данных прав при попытке выполнения этих действий.
Другими словами, аутентификация проверяет легальность пользователя. Пользователь становится авторизированным, если может выполнять разрешённые действия.
Важно понимать, что использование JWT не скрывает и не маскирует данные автоматически. Причина использования JWT — проверка, что отправленные данные были действительно отправлены авторизованным источником. Данные внутри JWT закодированы и подписаны, но не зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных.
Реализация JWT в нашем APIService:
— Создаём директории middleware и jwt, а также файл jwt.go.
— Описываем кастомные UserClaims и сам middlware.
— Получаем заголовок Authorization, оттуда берём токен.
— Берём секрет из конфига.
— Создаём верификатор HMAC.
— Парсим и проверяем токен.
— Анмаршалим полученные данные в модель UserClaims.
— Проверяем, что токен валидный на текущий момент.
При любой ошибке отдаём ответ с кодом 401 Unauthorized. Если ошибок не было, в контекст сохраняем ID пользователя в параметр user_id, чтобы во всех хендлерах его можно было получить. Теперь надо этот токен сгенерировать. Это будет делать хендлер авторизации с методом POST и эндпоинтом /api/auth. Он получает входные данные в виде полей username и password, которые мы описываем отдельной структурой user. Здесь также будет взаимодействие с UserService, нам надо там искать пользователя по полученным данным. Если такой пользователь есть, то создаём для него UserClaims, в которых указываем все нужные для нас данные. Определяем время жизни токена при помощи переменной ExpiresAt — берём текущее время и добавляем 15 секунд. Билдим токен и отдаём в виде JSON в параметре token. Клиента к UserService у нас пока нет, поэтому делаем заглушку.
Добавим в хендлер с heartbeat еще один тестовый хендлер, чтобы проверить работу аутентификации. Пишем небольшой тест. Для этого используем инструмент sketch, встроенный в IDE. Делаем POST-запрос на /api/auth, получаем токен и подставляем его в следующий запрос. Получаем ответ от эндпоинта /api/heartbeat, по истечении 5 секунд мы начнём получать ошибку с кодом 401 Unauthorized.
Наш токен действителен очень ограниченное время. Сейчас это 15 секунд, а будет минут 30. Но этого всё равно мало. Когда токен протухнет, пользователю необходимо будет заново авторизовываться в системе. Это сделано для того, чтобы защитить пользовательские данные. Если злоумышленник украдет токен авторизации, который будет действовать очень большой промежуток времени или вообще бессрочно, то это будет провал.
Чтобы этого избежать, прикрутим refresh-токен. Он позволит пересоздать основной токен доступа без запроса данных авторизации пользователя. Такие токены живут очень долго или вообще бессрочно. После того как только старый JWT истекает мы больше не можем обратиться к API. Тогда отправляем refresh-токен. Нам приходит новая пара токена доступа и refresh-токена.
Хранить refresh-токены на сервере мы будем в кэше. В качестве реализации возьмём FreeCache. Я использую свою обёртку над кэшем из другого проекта, которая позволяет заменить реализацию FreeCache на любую другую, так как отдает интерфейс Repository с методами, которые никак не связаны с библиотекой.
Пока рассуждал про кэш, решил зарефакторить существующий код, чтобы было удобней прокидывать объекты без dependency injection и синглтонов. Обернул хендлеры и роутер в структуры. В хендлерах сделал интерфейс с методом Register, которые регистрируют его в роутере. Все объекты теперь инициализируются в main, весь роутер переехал в мейн. Старт приложения выделили в отдельную функцию также в main-файле. Теперь, если хендлеру нужен какой-то объект, я его просто буду добавлять в конструктор структуры хендлера, а инициализировать в main. Плюс появилась возможность прокидывать всем хендлерам свой логер. Это будет удобно когда надо будет добавлять поле trace_id от Zipkin в строчку лога.
Вернемся к refresh_token. Теперь при создании токена доступа создадим refresh_token и отдадим его вместе с основным. Сделаем обработку метода PUT для эндпоинта /api/auth, а в теле запроса будем ожидать параметр refresh_token, чтобы сгенерировать новую пару токена доступа и refresh-токена. Refresh-токен мы кладём в кэш в качестве ключа. Значением будет user_id, чтобы по нему можно было запросить данные пользователя у UserService и сгенерировать новый токен доступа. Refresh-токен одноразовый, поэтому сразу после получения токена из кэша удаляем его.
Для описания нашего API будем использовать спецификацию OpenAPI 3.0 и Swagger — YAML-файл, который описывает все схемы данных и все эндпоинты. По нему очень легко ориентироваться, у него приятный интерфейс. Но описывать вручную всё очень муторно, поэтому лучше генерировать его кодом.
— Создаём эндпоинты /api/auth с методами POST и PUT для получения токена по юзернейму и паролю и по Refresh-токену соответственно.
— Добавляем схемы объектов Token и User.
— Создаём эндпоинты /api/users с методом POST для регистрации нового пользователя. Для него создаём схему CreateUser.
Понимаем, что забыли сделать хендлер для регистрации пользователя. Создаём метод Signup у хенлера Auth и структуру newUser со всеми полями для регистрации. Генерацию JWT выделяем в отдельный метод, чтобы можно было его вызывать как в Auth, так и в Signup-хендлерах. У нас всё еще нет UserService, поэтому проставляем TODO. Нам надо будет провалидировать полученные данные от пользователя и потом отправить их в UserService, чтобы он уже создал пользователя и ответил нам об успехе. Далее вызываем функцию создания пары токена доступа и refresh-токена и отдаём с кодом 201.
У нас есть подсказка в виде Swagger-файла. На его основе создаём все нужные хендлеры. Там, где вызов микросервисов, будем проставлять комментарий с TODO.
Создаём хендлер для категорий, определяем URL в константах. Далее создаём структуры. Опираемся на Swagger-файл, который создали ранее. Далее создаём сам хендлер и реализуем метод Register, который регистрирует его в роутере. Затем создаём методы с логикой работы и сразу пишем тест API на этот метод. Проверяем, находим ошибки в сваггере. Таким образом мы создаём все методы по работе с категориями: получение и создание.
Далее создаём таким же образом хендлер для заметок. Понимаем, что забыли методы частичного обновления и удаления как для заметок, так и для категорий. Дописываем их в Swagger и реализуем методы в коде. Также обязательно тестируем Swagger в онлайн-редакторе.
Здесь надо обратить внимание на то, что методы создания сущности возвращают код ответа 201 и заголовок Location, в котором находится URL для получения сущности. Оттуда можно вытащить идентификатор созданной сущности.
В третьей части мы познакомимся с графовой базой данных Neo4j, а также будем работать над микросервисами CategoryService и APIService.