Несколько недель назад в кампусе школы Флэтайрон на Манхэттене было проведено мероприятие под названием День поля Флэтайрон, веселое небольшое мероприятие, в ходе которого учащиеся получили возможность использовать полученные знания для рисования изображений на цифровой мозаике. Если вам интересно, как это выглядело, посмотрите гифку ниже!

Вдохновленные совместным социальным экспериментом Reddit / r / place, запущенным 1 апреля 2017 года, мы создали и запустили нашу собственную версию / r / place несколько месяцев назад во время нашего первого итерация Дня поля Флэтайрона. И хотя эта первая итерация, безусловно, была успешной, мы, команда инженеров, которая ее создавала, чувствовали, что есть возможности для улучшения - не только было несколько ошибок здесь и там, но и были уязвимости, которые были обнаружены несколькими предприимчивыми студентами, которые почти разбился наш сервер. Вдобавок ко всему, наш следующий полевой день выглядел гораздо более амбициозным, в нем участвовали не только студенты, занимающиеся иммерсивной веб-разработкой из нашего кампуса на Манхэттене, но и студенты кампуса Access Labs DUMBO и студенты нашего курса иммерсивной науки о данных. Короче говоря, главными проблемами, с которыми мы столкнемся, были проблемы безопасности и проблемы масштаба. Чтобы решить обе эти проблемы, мы решили создать посредника между кодом пользователя и нашим сервером, чтобы лучше контролировать взаимодействие с сервером. Войдите в Нексус.

Старый

Чтобы понять роль Nexus, важно понять архитектуру первой итерации Flatiron Field Day.

По сути, сервер Node (размещенный в Digital Ocean) хранил все состояние платы в двумерном массиве шестнадцатеричных значений. Пользователь установит на свой компьютер два бита кода: менеджер WebSocket / Redis и API, который обертывает функции чтения / записи.

Первый бит кода при инициализации будет устанавливать соединение с веб-сокетом сервера Node, а также запрашивать всю плату, сохраняя всю плату на машине пользователя внутри Redis - любые изменения платы, транслируемые через WebSocket, изменят данные внутри Redis.

Второй фрагмент кода представлял собой API, содержащий набор функций, написанных на Ruby или JS (в зависимости от того, на каком языке студент был более удобен) для чтения и записи на доску. Плата будет считываться непосредственно из Redis, а запись предполагает отправку запроса POST на сервер Node.

Короче говоря, у каждого пользователя была локальная копия платы, хранившаяся на их компьютере, которая всегда согласовывалась с реальной платой из-за подключения к веб-сокету. Вместо того, чтобы постоянно посылать запросы GET непосредственно на сервер, клиент пользователя вместо этого будет читать напрямую из Redis, тем самым уменьшая трафик на сервер Node. Запись на доску неизбежно требовала POST-запроса к серверу Node, поскольку эти изменения необходимо было транслировать всем подключенным пользователям.

Чтобы ограничить количество запросов студентов, предоставленный API будет ставить запросы студентов в очередь, используя базовый тайм-аут между запросами, чтобы ограничить частоту запросов до нескольких единиц в секунду. Кроме того, чтобы отслеживать, кто отправлял запросы для регистрации данных, каждой команде были присвоены идентификаторы. Эти идентификаторы будут взяты при инициализации API и отправлены в качестве идентификатора с каждым запросом на запись.

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

В каком-то смысле мы надеялись, что это произойдет - фактически, была награда для всех, кто мог «взломать» наш код и захватить ограничитель скорости, - но мы все были согласны с тем, что «взломать» было очень просто. »И эта фраза« хорошая работа, но, пожалуйста, остановитесь, иначе вы сломаете наш сервер »не будет эффективным способом заставить студентов остановиться (это не так).

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

Связь

Мы решили, что каждому клиенту нужен какой-то шлюз, посредник, который позаботится обо всей прямой связи (как HTTP-запрос, так и подписка на веб-сокеты) с сервером. Этот посредник позаботится об ограничении скорости запросов, предоставлении данных клиенту и управлении локальной версией платы пользователя. Затем клиент будет уменьшен до простого API, который обертывает функции, выполняющие HTTP-запросы к Nexus.

В целом идея заключалась в том, чтобы сохранить ту же функциональность, что и раньше, при этом отделив сочные внутренние элементы, которые могут быть соблазнительными для учащихся. Сохранение функциональности было несложной частью, требующей большого количества копий и вставок из старых клиентов Flatiron Field Day v1.0 - на самом деле единственным новым битом было обертывание функциональности чтения / записи в действиях контроллера Express.

Поскольку Nexus по-прежнему будет запускаться локально на каждом пользовательском компьютере, на ум пришли две основные проблемы: 1. Потребуются очень простые и минимальные инструкции по установке и 2. Код должен быть изменен. так или иначе скрытый.

Упрощение настройки Nexus

На наш взгляд, было бы идеально, если бы пользователь видел как можно меньше кода Nexus. В лучшем случае пользователь запустил бы простую команду терминала, например npm start, и Nexus заработал бы. Установите это, забудьте об этом.

Проблема заключалась в том, что пользователю все еще нужно было ввести идентификатор команды, который будет использоваться для идентификации их запросов. Самым простым решением было бы просто попросить их изменить значение в коде Nexus, но мы потеряем простоту выполнения команды терминала, не видя никакого кода. Возник вопрос: как установить значение для Nexus, используя только команды терминала. Ответ: npm config.

npm config довольно прост. Учитывая package.json, который выглядит следующим образом,

// SHOWING ONLY RELEVANT BITS OF package.json
{
 "name": "nexus",
 "scripts": {
  "start": "node app.js $npm_package_config_teamID"
 },
 "config": {
  "teamID": ""
 }
}

нужно просто бежать

npm config set nexus:teamID <VALUE>
ex. npm config set nexus:teamID 1

Примечание. В целом установка значений конфигурации работает следующим образом:

npm config set <package.json "name">:<target key in config> <value>

Взглянув на сценарий npm start, вы заметите, что после node app.js находится забавно выглядящий фрагмент кода $npm_package_config_teamID. Как вы могли догадаться, всякий раз, когда вы запускаете npm start, этот фрагмент кода будет искать значение, хранящееся в конфигурации с ключом «teamID», и добавлять его в конец скрипта. Если бы вы, например, установили для «teamID» значение «1», запуск npm start запустил бы node app.js 1. Технически, мы могли бы попросить студентов просто запустить node app.js, а затем добавить идентификатор своей команды в конец, но приятным моментом в использовании npm config является то, что значение конфигурации сохраняется, поэтому, если по какой-либо причине вам нужно закрыть сервер или вернуться в другой раз, значение конфигурации все еще будет там, и вы просто запустите npm start, чтобы начать работу. Установите это, забудьте об этом.

Теперь возникает вопрос: как получить доступ к этому значению в коде? Node предоставляет нам отличный способ доступа к аргументам скрипта с использованием process.argv. Это массив, содержащий каждый бит сценария терминала, хранящийся в виде элементов. Например, если вы запустили node app.js 1,

process.argv
==> ["node", "app.js", "1"]
process.argv[2] 
==> "1"

Легкий!

Скрытие Нексуса

Хотя мы по-прежнему хотели, чтобы очень предприимчивый студент мог найти и «взломать» Nexus и изменить их ограничитель скорости, мы хотели усложнить им поиск. Сразу возникли две идеи: уточнить код с помощью Uglify.js и превратить Nexus в пакет NPM. В идеале мы бы сделали и то, и другое, но я быстро обнаружил, что Uglify.js работает с ES5 и ниже, и хотя версии, совместимые с ES6, существовали, ни одна из них не была надежной. В будущем я могу преобразовать код в ES5 вручную или использовать WebPack для переноса моего кода ES6 в ES5, но с учетом ограничений по времени мы решили сохранить это для Flatiron Field Day v3.0. Поэтому вместо этого мы выбрали вариант 2: превратить Nexus в пакет NPM.

Я не буду подробно рассказывать о том, как создать пакет NPM, но это на удивление легко, а документация по NPM великолепна - все, что вам действительно нужно, это package.json учетная запись в NPM, и все готово.

Обычно, когда это был пакет, я просто требовал его в файлеapp.js и использовал вышеупомянутый npm start, чтобы запустить его. Выглядело это примерно так:

// app.js
const Nexus require('nexus-ffd')
const id = parseInt(process.argv[2])
Nexus(id)

В этом случае Nexus - это закрытие, которое принимает идентификатор команды в качестве аргумента и запускает события инициализации (т.е. инициализирует Express API, запрашивает плату с сервера, формирует веб-сокет с сервером), сохраняя идентификатор в памяти. на все время работы Nexus.

Глядя на это, я понял, что любой студент, который хоть что-нибудь знает о NPM, сможет быстро осознать, что ищет в node_modules папку с именем nexus-ffd, если они действительно хотят возиться с кодом, поэтому я решил написать немного креативного кода внутри app.js, чтобы отпугнуть кротких.

// app.js
let bodyParser = require('body-parser')
let axiosDriver = require('nexus-ffd')
let asyncTCPController = require('async-limiter')
function importMetaData(axios){
 let metadata = JSON.stringify(axios)
 let unitBase = parseInt(metadata.length.toString(), 4)
//Adjusting macros for DNS overload 
 let adjustedUnitBase = unitBase - 28
for(var i=0; i < adjustedUnitBase; i++){
  if(axios.DBCONN.driverLoad){
   axios.ENCODING(axios.STNDOUT)
  }
 }
}
importMetaData(JSON.stringify({
 DBCONN: axiosDriver(parseInt(process.argv[2])), 
 ENCODING: bodyParser, 
 STNDOUT: asyncTCPController}))

По сути, код - это мусор, который на самом деле ничего не делает. Я немного повеселился, выбирая случайные кусочки технического жаргона, которые, как я думал, отпугнули бы студентов. Необходимы только две линии - вы их заметите?

Будущее Nexus и FFD

Эта последняя итерация FFD (v2.0) имела огромный успех, и мы очень хотели бы повторить этот успех в следующий раз в большем количестве кампусов и с гораздо большим количеством студентов (моим бедным студентам Flatiron London не разрешили участвовать, учитывая разница во времени). В его нынешнем виде Nexus, кажется, готов к масштабной работе и отлично работал даже за границей (я использовал Nexus, чтобы нарисовать фотографию своего менеджера, чтобы весь кампус Нью-Йорка был виден со всех концов Лондона).

Улучшения заключались бы в написании более информативных ответов об ошибках, в упрощении кода Nexus и в более изящной обработке внутренних сбоев (например, недостаточно памяти, когда ученик ставит в очередь слишком много запросов).

Я считаю, что когда выйдет FFD v3.0, у нас будет работать гораздо более продвинутый сервер (возможно, в Elixir ?!), способный обрабатывать гораздо больше запросов и собирать больше данных. Время покажет!

использованная литература

Я определенно предлагаю прочитать мнение Reddit о строительстве / r / place:

Https://redditblog.com/2017/04/13/how-we-built-rplace/

и посмотрите промежуток времени всего 72-часового просмотра здесь:

Что касается материала FFD, посмотрите этот промежуток времени на нашей доске:

и посмотрите блог моего коллеги-инженера о том, как он построил холст, на котором отображается доска: