...

суббота, 20 декабря 2014 г.

6 игр за 6 недель — игра четвертая


сегодня в 20:33




Зачем жить, если не смеяться над собой и не подшучивать над приятелями






Игра четвертая — Чапай. Я решил полностью переделать свое древнее приложение и поменял в игре все, что смог — картинки, цели, физику, код. Но не о коде я хочу рассказать. Не о моделировании неровностей шахматной доски. Не об алгоритме соударения разновесных шашек. И даже не о численном решении задачи столкновения трех тел. Нет.


У меня давний должок к стране, которую я люблю и к людям, которых я уважаю. Когда-то я жил в СССР и был примерным гражданином. Мою страну разделили — я стерпел. На обиды я могу ответить лишь острым словом и дерзкой шуткой. В качестве протеста я написал ироничную игру Морской Бой, где две республики дрались за Черное море.

Мою поделку показали в программе Время, а меня назвали ястребом, разжигающим войну.


Прошло 20 лет с тех пор. Я привык к потере СССР, юности и языка ФОРТРАН. И вдруг. Вновь случилась несправедливость. И я выпустил игру, где несправедливость наказывается, и добро побеждает зло.


А заодно и вернул должок за ту старую неудачную программу-шутку.


Парни, воюйте и побеждайте в виртуальных мирах. А миру оставим мир.




Кто поставит минус — тот червяк.

И читайте тэги.






1935


11





Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


IPv6, miredo, dynamic DNS AAAA


Захотелось странного — чтоб мои IPv6-enabled (miredo) хосты еще и динамически обновляемую DNS запись имели. Поизучав вопрос выяснил, что многие распространённые dyndns сервисы или не предоставляют возможность регистрации AAAA (IPv6 эквивалент записи типа A для IPv4), или не предоставляют её бесплатно, или имеют мутные настройки динамического обновления неизвестного уровня безопасности (или вовсе http/plaintext). Перепробовал с десяток сервисов и решил остановиться на freedns.afraid.org

Плюсы:



  • Человеко-понятная админка (без всяких «купить AAAA за $0 USD»)

  • Бесплатно дают AAAA

  • Безопасное (https) обновление

  • URL-based обновление (не приходится испытывать сомнений о конфиге для агентов типа ddclient)




Из особенностей — однострочный скрипт для обновления AAAA пришлось написать самому. Получилось такое:


cat /etc/cron.d/freedns-watcher



PATH=/sbin:/usr/sbin:/bin:/usr/bin

* * * * * root ipv6=$(ip a |grep -s -i -o '2001\:[a-f0-9\.:]*') && [ "$(nslookup -query=AAAA myhost.mooo.com ns1.afraid.org |grep -s -i -o '2001\:[a-f0-9\.:]*')" != "$ipv6" ] && curl -m 30 http://ift.tt/1zMekPa 2>/dev/null |grep Updated && date >> /var/log/freedns.log &> /dev/null




Скрипт хоть и однострочный но всё же немного длинноватый получился, поэтому прокомментирую что тут зачем:

* * * * *


5 звёздочек — информация для cron «запускать это каждую минуту»

root


сами решайте под какой учёткой запускать

ipv6=$(ip a |grep -s -i -o '2001\:[a-f0-9\.:]*')


Достаем из выхлопа ip teredo-адрес, работает примерно так:


ip a |grep -s -i -o '2001\:[a-f0-9\.:]*'
2001:0:52ab:53b:2ab4:555e:23d0:1dc9


Если адрес найден — помещаем его в переменную $ipv6, если не найден — присвоение фейлится и дальнейших трудов по обновлению записи AAAA не производится (ведь типичная причина отсутствия teredo адреса — отсутствие подключения (IPv4) к Интернету, да и обновлять собственно особо не на что)

[ "$(nslookup -query=AAAA myhost.mooo.com ns1.afraid.org |grep -s -i -o '2001\:[a-f0-9\.:]*')" != "$ipv6" ]


Опрашиваем сервер ns1.afraid.org на предмет «какой там нынче IP адрес у вас записан для моего AAAA?» и сравниваем с тем, что нам сконфигурил на данный момент miredo. Если совпадает — ни чего делать не надо, выполнение скрипта прерывается. Почему указан их DNS сервер вместо системного? Что бы минимизировать задержку нотификации нашего скрипта об изменение записи. На других DNS серверах изменение будет с большой задержкой. Можно было бы ни чего не проверять а тупо долбить ежеминутно по указанному URL, но по-моему это хамство. С другой стороны отправка DNS запросов о вашем AAAA это в каком-то смысле палево, поэтому данная часть скрипта может считаться опциональной.

curl -m 30 http://ift.tt/1zMekPa 2>/dev/null |grep Updated



date >> /var/log/freedns.log


пишем в лог дату успешного обновления AAAA

&> /dev/null


cron, не беспокойся пожалуйста и не присылай нам письмо каждую минуту по поводу запуска этого скрипта

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


Дайджест интересных материалов о создании контента, маркетинге и Growth Hacking #6


сегодня в 12:06





930


13






Похожие публикации



Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


Новогодний розыгрыш



Понимаете, каждый год 31 декабря мы с друзьями разыгрываем подарки. Это у нас такая традиция…

Не всегда заранее известно, кто придет на новогоднюю вечеринку, сколько будет гостей и кем они будут. В кругу моих друзей эта ситуация повторялась из года в год.


Подарков на всех может не хватить, поэтому, чтобы никому не было обидно, однажды мы решили, что каждый захватит по одному «общественному» подарку. И на месте решим, кто кому и что дарит.


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


Каждый раз я садился и писал на коленке скрипт для подбора случайных пар дарителей и получателей. И каждый раз он, скрипт, терялся на следующий год. Но при этом появлялся новый, более совершенный. Он становился сложнее, появлялись обои, снежинки и прочая красота.


И вот однажды дошли руки увековечить накопленный опыт в области «новогоднего розыгрыша» в виде html страницы, которой я и хочу с вами поделиться: http://ift.tt/1zJAqSv.


В самой странице нет почти ничего необычного, html + javascript. Есть английская версия. Создавалась она пару вечеров, поэтому не ругайте особо. Для нахождения пар используется рандомизированный поиск в глубину. Есть проблема со временем выполнения поиска при большом количестве ограничений. Если у кого-то найдутся добрые руки, буду рад коммиту.


Чтобы страница завелась, просто скачайте репозиторий и запустите index.html.


Совсем немного времени осталось до Нового Года, католическое Рождество наступает, православное разогревается, а старый новый год забивает трубку. Надеюсь, что кому-то данный новогодний сайт пригодится.


Всех с наступающим!



This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


Несколько интересных особенностей MySQL

В не очень далеком прошлом мне пришлось покопаться немного в исходном коде MySQL, и разобраться в некоторых аспектах его работы. В ходе работы лопаткой, и эксперимнтов, я наткнулся на несколько очень интересных особенностей, часть из которых просто забавна, а в случае некоторых бывает очень интересно понять, чем руководствовался программист, который принимал решение сделать именно так.

Начнем с такого интересного типа, как ENUM.



mysql> CREATE TABLE enums(a ENUM('c', 'a', 'b'), b INT, KEY(a));
Query OK, 0 rows affected (0.36 sec)

mysql> INSERT INTO enums VALUES('a', 1), ('b', 1), ('c', 1);
Query OK, 3 rows affected (0.05 sec)
Records: 3 Duplicates: 0 Warnings: 0


Итак, у нас есть таблица, в ней есть два столбца. У первого, a , тип ENUM, у второго, b , INT. В таблице три строки, у всех трех значение b равно 1. Интересно, чему равны минимальный и максимальный элементы в столбце a ?



mysql> SELECT MIN(a), MAX(a) FROM enums;
+--------+--------+
| MIN(a) | MAX(a) |
+--------+--------+
| c | b |
+--------+--------+
1 row in set (0.00 sec)


Кажется странным, было бы разумно, если бы самым маленьким был 'a', а самым большим — 'c'.

А что если выбрать минимум и максимум только среди тех строк, где b = 1? То есть, среди всех строк?



mysql> SELECT MIN(a), MAX(a) FROM enums WHERE b = 1;
+--------+--------+
| MIN(a) | MAX(a) |
+--------+--------+
| a | c |
+--------+--------+
1 row in set (0.00 sec)


Вот так мы заставили MySQL поменять свое мнение о том, как сравнивать поля в ENUM, просто добавив предикат.

Разгадка такого поведения заключается в том, что в первом случае MySQL использует индекс, а во втором нет. Это, конечно, не объясняет, почему MySQL сравнивает ENUMы по разному для сортировки в индексе, и при обычном сравнении.


Второй пример проще и лаконичнее:



mysql> (SELECT * FROM moo LIMIT 1) LIMIT 2;
+------+
| a |
+------+
| 1 |
| 2 |
+------+
2 rows in set (0.00 sec)


Когда я показал этот запрос своему коллеге, который занимается разработкой парсера SQL, его вопрос был не «почему этот запрос возвращает две строки», а «как надо написать SQL парсер так, чтобы такой запрос был валидным, без того, чтобы написать правило, специально разрешающее такой запрос».


Интересно, что далеко не любой SELECT в скобках сработает, в частности, UNION в скобках — это синтаксическая ошибка:



mysql> (SELECT * FROM moo UNION ALL SELECT * FROM hru) LIMIT 2;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'UNION ALL SELECT * FROM hru) LIMIT 2' at line 1


Еще несколько интересных примеров под катом


Вообще, с UNION и LIMIT далеко за примером странного поведения ходить не надо:



mysql>
-> SELECT 1 FROM moo LIMIT 1
-> UNION ALL
-> SELECT 1 FROM hru LIMIT 1;
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)


Внезапно, вернулась только одна строка, хотя обе таблицы не пусты. Потому что второй LIMIT принадлежит всему запросу, а не только правой части UNION.


Тут надо рассказать о такой вещи, как shift-reduce conflict. В современных базах данных с открытым кодом парсер очень часто написан на bison. Такой парсер является так называемым L1-парсером, что значит, что парсер должен понять предназначение очередного токена, посмотрев не далее чем на один токен вперед. Например, в запросе выше смотря на слово LIMIT парсер не может понять, принадлежит этот LIMIT к второму запросу, или ко всему UNION. Когда правила написаны так, что возможны ситуации, при которых понять назначение токена посмотрев только на следующий токен нельзя, это называется shift-reduce conflict. В этом случае парсер будет выбирать решение базируясь на определенном наборе правил. Это очень плохо, потому что это приводит к тому, что вполне нормальные запросы приводят к ошибкам. Что, если я хочу в предыдущем запросе сделать LIMIT и второму SELECT, и UNION?



mysql> SELECT 1 FROM moo
-> UNION ALL
-> SELECT 1 FROM hru LIMIT 1
-> LIMIT 2;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'LIMIT 2' at line 4


Так сделать нельзя, из-за shift-reduce конфликта. Смотря на первый LIMIT парсер еще не знает, что впереди будет второй, и ошибочно полагает, что первый лимит относится ко всему запросу.

В PostgreSQL в парсере shift-reduce conflicts нет совсем. Конкретно эта ситуация там разрешена за счет того, что у UNION не может быть LIMIT.

В MySQL таких конфликтов больше чем 160. Это поражает воображение, потому что это значит, что есть 160 мест, где парсер может не правильно понять, что от него хотят.


Хороший пример такого конфликта — это соединения. Как известно, в MySQL поддерживаются CROSS JOINs, у которых нет предиката, и INNER JOINs, у которых предикат есть. Вообще говоря, CROSS JOIN и INNER JOIN — это разные вещи, но в MySQL это синонимы. То есть у INNER JOIN может не быть предиката, а у CROSS JOIN он может быть. В частности, это приводит к интересной ошибке:



mysql> SELECT * FROM
-> moo
-> INNER JOIN
-> hru
-> INNER JOIN
-> baa
-> ON hru.a = baa.a
-> ON moo.a = hru.a
-> ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'ON moo.a = hru.a' at line 8


В момент, когда парсер видит первое ON, он еще не знает, что впереди его ждет второе, и сталкивается с выбором: либо это ON для hru и baa, либо hru и baa соединяются без предиката, а текущий ON — это ON для moo и результата соединения hru и baa. Парсер ошибочно выбирает второе, что приводит к совершенно не нужной в этой ситуации ошибке. Если INNER JOIN заменить на LEFT JOIN, для которого варианта без предиката не сущестует, то запрос выполнится:



mysql> SELECT * FROM
-> moo
-> LEFT JOIN
-> hru
-> LEFT JOIN
-> baa
-> ON hru.a = baa.a
-> ON moo.a = hru.a
-> ;
+------+------+------+------+
| a | a | b | a |
+------+------+------+------+
| 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 2 |
+------+------+------+------+
2 rows in set (0.00 sec)


Тут самое интересное, это то, что в Bison надо руками указать прямо в коде количество shift-reduce conflicts, иначе код не скомпилируется. То есть в какой-то момент времени один из программистов в MySQL сделал CROSS JOIN и INNER JOIN синонимами, что уже само по себе не имеет смысла, после чего попытался собрать код, и он не собрался с ошибкой компиляции, предупреждающей, что парсер теперь не сможет распарсить определенные запросы. На что тот программист, вместо того, чтобы сделать все правильно, нашел константу, указывающую на количество ошибок в парсере, и увеличил ее.


Хотя если говорить о том, какие интересные решения иногда программисты в MySQL принимают, то лучше всего вспомнить вот эту историю:

http://ift.tt/1sIJ9Ot

В ней один из программистов сознательно сделал в collation по умолчанию для utf8 букву 's' равной символу 'ß'. Это очень иронично, потому что единственный язык, в котором это хотя бы отдаленно могло бы иметь смысл — это немецкий, но именно это изменение делает этот collation совершенно не применимым к немецкому языку, потому что теперь строки, которые совершенно не равны друг другу, становятся равны.

Это изменение было не только бесполезным, оно еще и сделало процесс перехода с 5.0 на 5.1 для баз данных с utf8 строками на немецком очень болезненным, потому что уникальные индексы внезапно начали содержать повторяющиеся элементы.


Говоря о collations, я еще очень люблю вот такой пример:


Пусть у нас есть таблица с тремя строками с разными collations:



CREATE TABLE strings(
swedish VARCHAR(100) COLLATE utf8_swedish_ci,
spanish VARCHAR(100) COLLATE utf8_spanish_ci,
bin VARCHAR(100) COLLATE utf8_bin
);


Выполним такой запрос:



mysql> SELECT * FROM strings WHERE swedish > bin AND swedish < spanish;
ERROR 1267 (HY000): Illegal mix of collations (utf8_swedish_ci,IMPLICIT) and (utf8_spanish_ci,IMPLICIT) for operation '<'


MySQL разумно жалуется, что сравнивать swedish и spanish нельзя, потому что непонятно, как их сравнивать.

Давайте напишем совершенно идентичный запрос:



mysql> SELECT * FROM strings WHERE swedish BETWEEN bin AND spanish;
Empty set (0.00 sec)


Внезапно, запрос стал валидным, хотя он по прежднему должен сравнивать swedish и spanish строку. А если я хочу наоборот?



mysql> SELECT * FROM strings WHERE swedish BETWEEN spanish AND bin;
ERROR 1270 (HY000): Illegal mix of collations (utf8_swedish_ci,IMPLICIT), (utf8_spanish_ci,IMPLICIT), (utf8_bin,IMPLICIT) for operation 'between'


А наоборот нельзя.

Если покопаться в коде, то можно понять, что в MySQL BETWEEN реализован совершенно странным образом: если первый или второй парамерт имеют бинарный collation, то все строки будут сравниваться как бинарные, и collation будет проигнорирован. Но если бинарный collation у третьего аргумента, то такая же логика не применяется.


Говоря о том, как странно работают функции в MySQL, завершим эту статью самым красивым примером.



mysql> SELECT LEAST(9, 11);
+--------------+
| LEAST(9, 11) |
+--------------+
| 9 |
+--------------+
1 row in set (0.00 sec)


Тут никаких сюрпризов



mysql> SELECT LEAST("9", "11");
+------------------+
| LEAST("9", "11") |
+------------------+
| 11 |
+------------------+
1 row in set (0.00 sec)


Это тоже разумно, строка 11 меньше чем 9. А что будет, если 11 прибавить к 11?



mysql> SELECT LEAST("9", "11") + LEAST("9", "11");
+-------------------------------------+
| LEAST("9", "11") + LEAST("9", "11") |
+-------------------------------------+
| 18 |
+-------------------------------------+
1 row in set (0.00 sec)


Конечно, 18. Получается, функция возвращает разное значение в зависимости от контекста! А можно ли заставить один и тот же LEAST вернуть три разных значения в зависимости от контекста? Оказывается, да



mysql> SELECT LEAST("9e1", "110");
+---------------------+
| LEAST("9e1", "110") |
+---------------------+
| 110 |
+---------------------+
1 row in set (0.00 sec)

mysql> SELECT LEAST("9e1", "110") + 0;
+-------------------------+
| LEAST("9e1", "110") + 0 |
+-------------------------+
| 90 |
+-------------------------+
1 row in set (0.00 sec)

mysql> SELECT LEAST("9e1", "110") & -1;
+--------------------------+
| LEAST("9e1", "110") & -1 |
+--------------------------+
| 9 |
+--------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS;
+---------+------+------------------------------------------+
| Level | Code | Message |
+---------+------+------------------------------------------+
| Warning | 1292 | Truncated incorrect INTEGER value: '9e1' |
+---------+------+------------------------------------------+
1 row in set (0.00 sec)


Хотя тут надо сказать, что в одном случае мы встретили предупреждение. Но у нас все же получилось заставить один и тот же оператор с одними и теми же аргументами вернуть три разных значения.


Чтобы сделать еще более удивительное открытие, надо познакомиться с функцией NULLIF. Эта функция принимает два аргумента, и возвращает NULL, если они равны, или значение первого аргумента, если они не равны. Отложив в сторону вопрос о том, зачем такая функция вообще существует, давайте посмотрим на результат следующих двух запросов:



mysql> SELECT NULLIF(LEAST("9", "11"), "11") + 0;
+------------------------------------+
| NULLIF(LEAST("9", "11"), "11") + 0 |
+------------------------------------+
| NULL |
+------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT NULLIF(LEAST("9", "11"), "12") + 0;
+------------------------------------+
| NULLIF(LEAST("9", "11"), "12") + 0 |
+------------------------------------+
| 9 |
+------------------------------------+
1 row in set (0.00 sec)


В первом случае мы получили NULL, что говорит о том, что LEAST действительно равен строке «11». Во втором случае в таком же запросе, с такими же типами аргументов, но с другой константой в NULLIF мы получили значение 9! То есть при совершенно одинаковых типах параметров в первом случае LEAST вернул «11», а во втором — 9.

Но можно сделать еще лучше:



mysql> SELECT NULLIF(LEAST("9", "11"), "9") + 0;
+-----------------------------------+
| NULLIF(LEAST("9", "11"), "9") + 0 |
+-----------------------------------+
| 9 |
+-----------------------------------+
1 row in set (0.00 sec)


В этом запросе LEAST вернул что-то отличное от строки «9» (иначе бы NULLIF вернул NULL), однако он в тоже самое время вернул строку «9»!

Если посмотреть в код, то это действительно то, что происходит. LEAST выполняется дважды, первый раз сравнивая параметры, как строки, а второй раз — как целые числа.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


[Перевод] Выразительный JavaScript: Проект: Веб-сайт по обмену опытом

Содержание


На встречах по обмену опытом люди с общими интересами встречаются и делают небольшие неформальные презентации на тему своих знаний. На встрече по обмену опытом среди фермеров кто-нибудь может рассказать о выращивании сельдерея. На встрече программистов вы можете выступить с рассказом про Node.js


Такие встречи – отличный способ расширить свой кругозор, узнать о новинках области, или просто пообщаться с людьми со схожими интересами. Во многих городах есть встречи любителей JavaScript. Обычно их посещение бесплатное, и я нашёл те, которые посещал, дружелюбными и гостеприимными.



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



Встречи моноциклистов




Как и в предыдущей главе, код написан для Node.js и запустить его в браузере не получится. Полный код доступен по ссылке.

Дизайн




У проекта есть серверная часть, написанная для Node.js, и клиентская, написанная для браузера. Серверная хранит системные данные и передаёт их клиенту. Также она отдаёт файлы HTML и JavaScript, которые создают систему на стороне клиента.

На сервере есть список тем для следующего собрания, и клиент их показывает. У каждой темы есть имя выступающего, название, описание и список комментариев. Клиент позволяет предлагать новые темы (добавлять их в список), удалять темы и комментировать существующие. Когда пользователь вносит это изменение, клиент делает HTTP-запрос, чтобы сообщить об этом серверу.



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


Общепринятым решением проблемы являются длинные запросы (long polling), которые послужили одной из мотиваций к разработке Node.


Длинные запросы




Чтобы мгновенно оповестить клиента об изменениях, нам нужно соединение с клиентом. Браузеры традиционно не принимают запросов на соединения, и клиенты всё равно скрыты за устройствами, которые эти соединения не приняли бы, поэтому начинать соединение с сервера смысла не имеет.

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


Но запрос HTTP разрешает только простой обмен информацией – клиент отправляет запрос, сервер возвращает ответ, и всё. Есть технология под названием web sockets, которая поддерживается современными браузерами, позволяющая открывать соединения для обмена произвольными данными. Но их довольно сложно использовать.


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


Пока клиент постоянно держит открытый запрос, он будет получать информацию с сервера немедленно. К примеру, если у Алисы в браузере открыто приложение для обмена опытом, браузер сделает запрос на обновления и будет ожидать ответа. Когда Боб из своего браузера отправит тему «Экстремальный спуск на моноцикле с горы», сервер заметит, что Алиса ждёт обновлений, и отправит информацию по новой теме в ответ на её ждущий запрос. Браузер Алисы получит данные и обновит страницу, показав новую тему.


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


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


Интерфейс HTTP




Перед тем, как мы начнём делать сервер или клиент, подумаем об их точке соприкосновения: интерфейсе HTTP, через который они взаимодействуют.

Интерфейс будет основан на JSON, и, как и в файловом сервере в главе 20, мы будем с выгодой использовать методы HTTP. Интерфейс сосредоточен вокруг пути /talks. Пути, которые не начинаются с /talks, будут использоваться для отдачи статичных файлов – HTML и JavaScript, определяющих клиентскую часть.


Запрос GET к /talks возвращает документ JSON типа этого:



{"serverTime": 1405438911833,
"talks": [{"title": "Unituning ",
"presenter": "Васисуалий",
"summary": "Украшаем свой моноцикл",
"comment": []}]}


Поле serverTime используется для надёжности длинных запросов. Вернёмся к нему позже.


Создание новой темы происходит через запрос PUT к URL вида /talks/Unituning, где часть после второго слеша – название темы. Тело запрос PUT должно содержать объект JSON, в котором описаны свойства presenter и summary.


Поскольку заголовки тем могут содержать пробелы и другие символы, которые нельзя вставлять в URL, при создании URL их надо закодировать при помощи функции encodeURIComponent.



console.log("/talks/" + encodeURIComponent("How to Idle"));
// → /talks/How%20to%20Idle


Запрос на создание темы может выглядеть так:


PUT /talks/How%20to%20Idle HTTP/1.1

Content-Type: application/json

Content-Length: 92


{«presenter»: «Даша»,

«summary»: «Неподвижно стоим на моноцикле»}


Такие URL поддерживают запросы GET для получения JSON-представления темы и DELETE для удаления темы.


Добавление комментария происходит через POST запрос к URL вида /talks/Unituning/comments, с объектом JSON, содержащим свойства author и message в теле запроса.


POST /talks/Unituning/comments HTTP/1.1

Content-Type: application/json

Content-Length: 72


{«author»: «Alice»,

«message»: «Will you talk about raising a cycle?»}


Для поддержки длинных запросов, запросы GET к /talks могут включать параметр под именем changesSince, показывающий, что клиенту нужны обновления, случившиеся после заданной точки во времени. Когда обновления появляются, они сразу же возвращаются. Когда их нет, запрос задерживается, пока что-нибудь не случится, или пока не пройдёт заданный период времени (мы зададим 90 секунд).


Время используется в формате количества миллисекунд с начала 1970 года, в том же формате, что возвращает Date.now(). Чтобы удостовериться, что клиент получает все обновления, и не получает одно и то же обновление дважды, клиент должен передать время, в которое он в последний раз получил информацию с сервера. Часы сервера могут не совпадать с клиентом, и даже если б они совпадали, клиент не мог бы знать точное время, в которое сервер отправлял ответ, потому что передача данных по сети занимает время.


Поэтому в ответах на запросы GET к /talks и существует свойство serverTime. Оно сообщает клиенту точное время по часам сервера, когда были созданы передаваемые данные. Клиент просто сохраняет время и передаёт его вместе со следующим запросом, чтобы убедиться, что он получает только те обновления, которых ещё не получал.


GET /talks?changesSince=1405438911833 HTTP/1.1


(прошло время)


HTTP/1.1 200 OK

Content-Type: application/json

Content-Length: 95


{«serverTime»: 1405438913401,

«talks»: [{«title»: «Unituning»,

«deleted»: true}]}


Когда тема меняется, создаётся или комментируется, в ответ на следующий запрос включается полная информация о теме. Когда тема удаляется, включаются только название и свойство deleted. Клиент может добавлять темы с заголовками, которые он ещё не видел, к странице, обновлять темы, которые он уже показывает, и удалять темы, которые были удалены.


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


Простым решением было бы разместить систему за обратным прокси – это HTTP-сервер, которая принимает соединения снаружи системы и перенаправляет их на сервера HTTP, работающие локально. Такой proxy можно настроить, чтобы он спрашивал имя и пароль пользователя, и вы могли бы устроить так, чтобы пароль был только у членов вашей группы.


Сервер




Начнём с написания серверной части программы. Код работает на Node.js

Роутинг




Для запуска сервера будет использоваться http.createServer. В функции, обрабатывающей новый запрос, мы должны различать запросы (определяемые методом и путём), которые мы поддерживаем. Это можно сделать через длинную цепочку if / else, но можно и красивее.

Роутер – компонент, помогающий распределить запрос к функции, которая может его обработать. Можно сказать роутеру, что запросы PUT с путём, совпадающим с регуляркой /^\/talks\/([^\/]+)$/ (что совпадает с /talks/, за которым идёт название темы), могут быть обработаны заданной функцией. Кроме того, он может помочь извлечь осмысленные части пути, в нашем случае – название темы, заключённое в кавычки, и передать их вспомогательной функции.


В NPM есть много хороших модулей роутинга, но тут мы сами себе такой напишем, чтобы продемонстрировать принцип его работы.


Вот файл router.js, который будет запрашиваться через require из модуля сервера:



var Router = module.exports = function() {
this.routes = [];
};

Router.prototype.add = function(method, url, handler) {
this.routes.push({method: method,
url: url,
handler: handler});
};

Router.prototype.resolve = function(request, response) {
var path = require("url").parse(request.url).pathname;

return this.routes.some(function(route) {
var match = route.url.exec(path);
if (!match || route.method != request.method)
return false;

var urlParts = match.slice(1).map(decodeURIComponent);
route.handler.apply(null, [request, response]
.concat(urlParts));
return true;
});
};


Модуль экспортирует конструктор Router. Объект router позволяет регистрировать новые обработчики с методом add, и распределять запросы методом resolve.


Последний вернёт булевское значение, показывающее, был ли найден обработчик. Метод some массива путей будет пробовать их по очереди (в порядке, в каком они были заданы), и остановится с возвратом true, если путь найден.


Функции обработчиков вызываются с объектами request и response. Когда регулярка, проверяющая URL, возвращает группы, то представляющие их строки передаются в обработчик в качестве дополнительных аргументов. Эти строчки надо декодировать из URL-стиля %20.


Выдача файлов




Когда тип запроса не совпадает ни с одним из типов, которые обрабатывает роутер, сервер должен интерпретировать его как запрос файла из общей директории. Можно было бы использовать файловый сервер из главы 20 для выдачи этих файлов, но нам не нужна поддержка PUT и DELETE, зато нам нужны дополнительные функции типа поддержки кеширования. Поэтому, давайте использовать проверенный и протестированный файловый сервер из NPM.

Я выбрал ecstatic. Это не единственный сервер на NPM, но он хорошо работает и удовлетворяет нашим требованиям. Модуль ecstatic экспортирует функцию, которую можно вызвать с объектом конфигурации, чтобы она выдала функцию обработчика. Мы используем опцию root, чтобы сообщить серверу, где нужно искать файлы. Обработчик принимает параметры request и response, и его можно передать напрямую в createServer, чтобы создать сервер, который отдаёт только файлы. Но сначала нам нужно проверить те запросы, которые мы обрабатываем особо – поэтому мы обёртываем его в ещё одну функцию.



var http = require("http");
var Router = require("./router");
var ecstatic = require("ecstatic");

var fileServer = ecstatic({root: "./public"});
var router = new Router();

http.createServer(function(request, response) {
if (!router.resolve(request, response))
fileServer(request, response);
}).listen(8000);

Функции respond и respondJSON используются в коде сервера, чтобы можно было отправлять ответы одним вызовом функции.

function respond(response, status, data, type) {
response.writeHead(status, {
"Content-Type": type || "text/plain"
});
response.end(data);
}

function respondJSON(response, status, data) {
respond(response, status, JSON.stringify(data),
"application/json");
}


Темы как ресурсы




Сервер хранит предложенные темы в объекте talks, у которого именами свойств являются названия тем. Они будут выглядеть как ресурсы HTTP по адресу /talks/[title], поэтому нам нужно добавить в роутер обработчиков, реализующих различные методы, которые клиенты могут использовать для работы с ними.

Обработчик для запросов GET одной темы должен найти её и либо вернуть данные в JSON, либо выдать ошибку 404.



var talks = Object.create(null);

router.add("GET", /^\/talks\/([^\/]+)$/,
function(request, response, title) {
if (title in talks)
respondJSON(response, 200, talks[title]);
else
respond(response, 404, "No talk '" + title + "' found");
});

Удаление темы делается удалением из объекта talks.

router.add("DELETE", /^\/talks\/([^\/]+)$/,
function(request, response, title) {
if (title in talks) {
delete talks[title];
registerChange(title);
}
respond(response, 204, null);
});


Функция registerChange, которую мы определим позже, уведомляет длинные запросы об изменениях.


Чтобы было просто получать контент тел запросов, закодированных при помощи JSON, мы определяем функцию readStreamAsJSON, которая читает всё содержимое потока, разбирает его по правилам JSON и затем делает обратный вызов.



function readStreamAsJSON(stream, callback) {
var data = "";
stream.on("data", function(chunk) {
data += chunk;
});
stream.on("end", function() {
var result, error;
try { result = JSON.parse(data); }
catch (e) { error = e; }
callback(error, result);
});
stream.on("error", function(error) {
callback(error);
});
}


Один из обработчиков, которому нужно читать ответы в JSON – это обработчик PUT, который используется для создания новых тем. Он должен проверить, есть ли у данных свойства presenter и summary, которые должны быть строками. Данные, приходящие снаружи, всегда могут оказаться мусором, и мы не хотим, чтобы из-за плохого запроса была сломана наша система.


Если данные выглядят приемлемо, обработчик сохраняет объект, представляющий новую тему, в объекте talks, при этом, возможно, перезаписывая существующую тему с таким же заголовком, и опять вызывает registerChange.



router.add("PUT", /^\/talks\/([^\/]+)$/,
function(request, response, title) {
readStreamAsJSON(request, function(error, talk) {
if (error) {
respond(response, 400, error.toString());
} else if (!talk ||
typeof talk.presenter != "string" ||
typeof talk.summary != "string") {
respond(response, 400, "Bad talk data");
} else {
talks[title] = {title: title,
presenter: talk.presenter,
summary: talk.summary,
comments: []};
registerChange(title);
respond(response, 204, null);
}
});
});


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



router.add("POST", /^\/talks\/([^\/]+)\/comments$/,
function(request, response, title) {
readStreamAsJSON(request, function(error, comment) {
if (error) {
respond(response, 400, error.toString());
} else if (!comment ||
typeof comment.author != "string" ||
typeof comment.message != "string") {
respond(response, 400, "Bad comment data");
} else if (title in talks) {
talks[title].comments.push(comment);
registerChange(title);
respond(response, 204, null);
} else {
respond(response, 404, "No talk '" + title + "' found");
}
});
});


Попытка добавить комментарий к несуществующей теме должна возвращать ошибку 404.


Поддержка длинных запросов




Самый интересный аспект сервера – часть, которая поддерживает длинные запросы. Когда на адрес /talks поступает запрос GET, это может быть простой запрос всех тем, или запрос на обновления с параметром changesSince.

Есть много различных ситуаций, в которых нам нужно отправить клиенту список тем, поэтому мы сначала определим вспомогательную функцию, присоединяющую поле serverTime к таким ответам.



function sendTalks(talks, response) {
respondJSON(response, 200, {
serverTime: Date.now(),
talks: talks
});
}


Обработчик должен посмотреть на все параметры запроса в его URL, чтобы проверить, не задан ли параметр changesSince. Если дать функции parse модуля “url” второй аргумент значения true, он также распарсит вторую часть URL – query, часть запроса. У возвращаемого объекта будет свойство query, в котором будет ещё один объект, с именами и значениями параметров.



router.add("GET", /^\/talks$/, function(request, response) {
var query = require("url").parse(request.url, true).query;
if (query.changesSince == null) {
var list = [];
for (var title in talks)
list.push(talks[title]);
sendTalks(list, response);
} else {
var since = Number(query.changesSince);
if (isNaN(since)) {
respond(response, 400, "Invalid parameter");
} else {
var changed = getChangedTalks(since);
if (changed.length > 0)
sendTalks(changed, response);
else
waitForChanges(since, response);
}
}
});


При отсутствии параметра changesSince обработчик просто строит список всех тем и возвращает его.


Иначе, сперва надо проверить параметр changeSince на предмет того, что это число. Функция getChangedTalks, которую мы вскоре определим, возвращает массив изменённых тем с некоего заданного времени. Если она возвращает пустой массив, то серверу нечего возвращать клиенту, так что он сохраняет объект response (при помощи waitForChanges), чтобы ответить попозже.



var waiting = [];

function waitForChanges(since, response) {
var waiter = {since: since, response: response};
waiting.push(waiter);
setTimeout(function() {
var found = waiting.indexOf(waiter);
if (found > -1) {
waiting.splice(found, 1);
sendTalks([], response);
}
}, 90 * 1000);
}


Метод splice используется для вырезания куска массива. Ему задаётся индекс и количество элементов, и он изменяет массив, удаляя это количество элементов после заданного индекса. В этом случае мы удаляем один элемент – объект, ждущий ответ, чей индекс мы узнали через indexOf. Если вы передадите дополнительные аргументы в splice, их значения будут вставлены в массив на заданной позиции, и заместят удалённые элементы.


Когда объект response сохранён в массиве waiting, задаётся таймаут. После 90 секунд он проверяет, ждёт ли ещё запрос, и если да – отправляет пустой ответ и удаляет его из массива waiting.


Чтобы найти именно те темы, которые сменились после заданного времени, нам надо отслеживать историю изменений. Регистрация изменения при помощи registerChange запомнит это изменение, вместе с текущим временем, в массиве changes. Когда случается изменение, это значит – есть новые данные, поэтому всем ждущим запросам можно немедленно ответить.



var changes = [];

function registerChange(title) {
changes.push({title: title, time: Date.now()});
waiting.forEach(function(waiter) {
sendTalks(getChangedTalks(waiter.since), waiter.response);
});
waiting = [];
}


Наконец, getChangedTalks использует массив changes, чтобы построить массив изменившихся тем, включая объекты со свойством deleted для тем, которых уже не существует. При построении массива getChangedTalks должна убедиться, что одна и та же тема не включается дважды, так как тема могла измениться несколько раз с заданного момента времени.



function getChangedTalks(since) {
var found = [];
function alreadySeen(title) {
return found.some(function(f) {return f.title == title;});
}
for (var i = changes.length - 1; i >= 0; i--) {
var change = changes[i];
if (change.time <= since)
break;
else if (alreadySeen(change.title))
continue;
else if (change.title in talks)
found.push(talks[change.title]);
else
found.push({title: change.title, deleted: true});
}
return found;
}


Вот и всё с кодом сервера. Запуск написанного кода даст вам сервер, работающий на порту 8000, который выдаёт файлы из публичной поддиректории и управляет интерфейсом тем по адресу /talks.


Клиент




Клиентская часть веб-сайта по управлению темами состоит из трёх файлов: HTML-страница, таблица стилей и файл JavaScript.

HTML




Серверы по общепринятой схеме в случае запроса пути, соответствующего директории, отдают файл под именем index.html из этой директории. Модуль файлового сервера ecstatic поддерживает это соглашение. При запросе пути / сервер ищет файл ./public/index.html (где ./public – это корневая директория) и возвращает его, если он там есть.

Значит, если надо показать страницу, когда браузер будет запрашивать наш сервер, её надо положить в public/index.html. Вот начало файла index:










Обмен опытом






Ваше имя:







Определяется заголовок и включается таблица стилей, где определяются стили – в числе прочего, рамочка вокруг тем. Затем добавлен заголовок и поле name. Пользователь должен вписать своё имя, чтобы оно было присоединено к его темам и комментариям.


Элемент


с ID “talks” будет содержать список тем. Скрипт заполняет список, когда он получает его с сервера.

Затем идёт форма для создания новой темы.



Submit a talk


Заголовок:

Summary:





Скрипт добавит обработчик события “submit” в форму, из которого он сможет сделать HTTP-запрос, сообщающий серверу про тему.


Затем идёт загадочный блок, у которого стиль display установлен в none, и который поэтому не виден на странице. Догадаетесь, зачем он нужен?







Создание сложных структур DOM через JavaScript приводит к уродливому коду. Можно сделать его покрасивее при помощи вспомогательных функций типа elt из главы 13, но результат всё равно будет выглядеть хуже, чем HTML, который в каком-то смысле является языком для построения DOM-структур.


Для создания DOM-структур для тем обсуждений, наша программа определит простую систему шаблонов, которая использует скрытые структуры, включаемые в документ, для создания новых структур – заменяя метки в файле между двойными фигурными кавычками на значения для конкретной темы.


И наконец, HTML включает файл скрипта, содержащего клиентский код.






Запуск




Первое, что клиент должен сделать при загрузке страницы, это запросить с сервера текущий набор тем. Так как мы будем делать много HTTP-запросов, мы определим небольшую обёртку вокруг XMLHttpRequest, которая примет объект для настройки запроса и обратного вызова по окончанию запроса.

function request(options, callback) {
var req = new XMLHttpRequest();
req.open(options.method || "GET", options.pathname, true);
req.addEventListener("load", function() {
if (req.status < 400)
callback(null, req.responseText);
else
callback(new Error("Request failed: " + req.statusText));
});
req.addEventListener("error", function() {
callback(new Error("Network error"));
});
req.send(options.body || null);
}


Начальный запрос показывает полученные темы на экране и начинает процесс длинных запросов, вызывая waitForChanges.



var lastServerTime = 0;

request({pathname: "talks"}, function(error, response) {
if (error) {
reportError(error);
} else {
response = JSON.parse(response);
displayTalks(response.talks);
lastServerTime = response.serverTime;
waitForChanges();
}
});


Перменная lastServerTime используется для отслеживания времени последнего обновления, полученного с сервера. После начального запроса, вид тем у клиента соответствует виду тем сервера, которые был у него в момент запроса. Таким образом, свойство serverTime, включаемое в ответ, предоставляет правильное начальное значение lastServerTime.


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



function reportError(error) {
if (error)
alert(error.toString());
}


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


Показ тем




Чтобы иметь возможность обновлять список тем при поступлении изменений, клиент должен отслеживать темы, которые он показывает сейчас. Тогда, если поступает новая версия темы, которая уже есть на экране, её можно заменить прямо на месте обновлённой версией. Сходным образом, когда поступает информация об удалении темы, нужный элемент DOM можно удалить из документа.

Функция displayTalks используется как для построения начального экрана, так и для его обновления при изменениях. Она будет использовать объект shownTalks, связывающий заголовки тем с узлами DOM, чтобы запомнить темы, которые уже есть на экране.



var talkDiv = document.querySelector("#talks");
var shownTalks = Object.create(null);

function displayTalks(talks) {
talks.forEach(function(talk) {
var shown = shownTalks[talk.title];
if (talk.deleted) {
if (shown) {
talkDiv.removeChild(shown);
delete shownTalks[talk.title];
}
} else {
var node = drawTalk(talk);
if (shown)
talkDiv.replaceChild(node, shown);
else
talkDiv.appendChild(node);
shownTalks[talk.title] = node;
}
});
}


Структура DOM для тем строится по шаблону, включённому в HTML документ. Сначала нужно определить instantiateTemplate, который находит и заполняет шаблон.


Параметр name – имя шаблона. Чтобы найти элемент шаблона, мы ищем элементы, у которых имя класса совпадает с именем шаблона, который является дочерним у элемента с ID “template”. Метод querySelector облегчает этот процесс. На странице есть шаблоны “talk” и “comment”.



function instantiateTemplate(name, values) {
function instantiateText(text) {
return text.replace(/\{\{(\w+)\}\}/g, function(_, name) {
return values[name];
});
}
function instantiate(node) {
if (node.nodeType == document.ELEMENT_NODE) {
var copy = node.cloneNode();
for (var i = 0; i < node.childNodes.length; i++)
copy.appendChild(instantiate(node.childNodes[i]));
return copy;
} else if (node.nodeType == document.TEXT_NODE) {
return document.createTextNode(
instantiateText(node.nodeValue));
}
}

var template = document.querySelector("#template ." + name);
return instantiate(template);
}


Метод cloneNode, который есть у всех узлов DOM, создаёт копию узла. Он не скопирует дочерние узлы, если не передать ему первым аргументом true. Функция instantiate рекурсивно создаёт копию шаблона, заполняя его по ходу дела.


Второй аргумент instantiateTemplate должен быть объектом, чьи свойства содержат строки, которые надо ввести в шаблон. Метка вроде {{title}} будет заменена значением свойства “title”.


Этот подход к шаблонам довольно груб, но для создания drawTalk его будет достаточно.



function drawTalk(talk) {
var node = instantiateTemplate("talk", talk);
var comments = node.querySelector(".comments");
talk.comments.forEach(function(comment) {
comments.appendChild(
instantiateTemplate("comment", comment));
});

node.querySelector("button.del").addEventListener(
"click", deleteTalk.bind(null, talk.title));

var form = node.querySelector("form");
form.addEventListener("submit", function(event) {
event.preventDefault();
addComment(talk.title, form.elements.comment.value);
form.reset();
});
return node;
}


После завершения обработки шаблона “talk” нужно много чего подлатать. Во-первых, нужно вывести комментарии, путём многократного добавления шаблона “comment” и добавления результатов к узлу класса «comments». Затем, обработчики событий нужно присоединить к кнопке, которая удаляет задачу и к форме, добавляющей комментарий.


Обновление сервера




Обработчики событий, зарегистрированные в drawTalk, вызывают функции deleteTalk и addComment непосредственно для действий, необходимых для удаления темы или добавления комментария. Это будет нужно для построения URL, которые ссылаются на темы с заданным именем, для которых мы определяем вспомогательную функцию talkURL.

function talkURL(title) {
return "talks/" + encodeURIComponent(title);
}


Функция deleteTalk запускает запрос DELETE и сообщает об ошибке в случае неудачи.



function deleteTalk(title) {
request({pathname: talkURL(title), method: "DELETE"},
reportError);
}


Для добавления комментария нужно построить его представление в формате JSON и отправить его как часть POST-запроса.



function addComment(title, comment) {
var comment = {author: nameField.value, message: comment};
request({pathname: talkURL(title) + "/comments",
body: JSON.stringify(comment),
method: "POST"},
reportError);
}


Переменная nameField, используемая для установки свойства комментария author, ссылается на поле вверху страницы, которое позволяет пользователю задать его имя. Мы также подключаем это поле к localStorage, чтобы его не приходилось заполнять каждый раз при перезагрузке страницы.



var nameField = document.querySelector("#name");

nameField.value = localStorage.getItem("name") || "";

nameField.addEventListener("change", function() {
localStorage.setItem("name", nameField.value);
});

Форма внизу страницы для создания новой темы получает обработчик событий “submit”. Этот обработчик запрещает действие по умолчанию (что привело бы к перезагрузке страницы), очищает форму и запускает PUT-запрос для создания темы.

var talkForm = document.querySelector("#newtalk");

talkForm.addEventListener("submit", function(event) {
event.preventDefault();
request({pathname: talkURL(talkForm.elements.title.value),
method: "PUT",
body: JSON.stringify({
presenter: nameField.value,
summary: talkForm.elements.summary.value
})}, reportError);
talkForm.reset();
});


Обнаружение изменений




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

Учитывая созданную на сервере систему и то, как мы определили displayTalks для обработки изменений тем, которые уже есть на странице, сам механизм длинных запросов оказывается неожиданно простым.



function waitForChanges() {
request({pathname: "talks?changesSince=" + lastServerTime},
function(error, response) {
if (error) {
setTimeout(waitForChanges, 2500);
console.error(error.stack);
} else {
response = JSON.parse(response);
displayTalks(response.talks);
lastServerTime = response.serverTime;
waitForChanges();
}
});
}


Эта функция вызывается однажды, когда программа запускается, и затем продолжает вызывать себя, чтобы убедиться, что запросы всегда работают. Когда запрос не удаётся, мы не вызываем reportError, чтобы не раздражать пользователя всплывающим окном каждый раз при проблеме соединения с сервером. Вместо этого ошибка выводится в консоль (для облегчения отладки), и делается следующая попытка через 2.5 секунды.


Когда запрос удаётся, на экран выводятся новые данные, и lastServerTime обновляется, чтобы отражать тот факт, что мы получили данные в соответствии с этим новым моментом времени. Запрос сразу стартует заново, и ждёт следующего обновления.


Если вы запустите сервер, и откроете два окна браузера с адресом localhost:8000, вы увидите, что действия, выполняемые вами в одном окне, моментально отображаются в другом.


Упражнения




Следующие упражнения заключаются в изменении системы, описанной в этой главе. Для работы над ними, убедитесь, что вы скачали код и установили Node.js.
Сохранение состояния на диск



Сервер держит все данные в памяти. Если он упадёт или перезапустится, все темы и комментарии будут потеряны.

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


Обнуление полей комментариев



Общая перерисовка всех тем работает неплохо, потому что нет различия между узлом DOM и его заменой, когда они одинаковые. Но есть исключения. Если вы начнётё печатать что-либо в поле комментария к теме в одном окне браузера, а затем в другом окне добавите комментарий к этой теме, поле в первом окне будет перерисовано, и будет потеряно и его содержимое, и фокус.

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


Улучшенные шаблоны



Большинство шаблонизаторов делают больше, чем просто заполняют шаблоны строками. По меньшей мере они позволяют добавлять в шаблоны условия, аналогично оператору if, и повторения частей шаблона, аналогично циклам.

Если б мы могли повторять кусок шаблона для каждого элемента массива, второй шаблон («comment») был бы нам не нужен. Мы могли просто сказать шаблону “talk”, чтобы он повторялся для массива, содержащегося в свойстве comments, и создавал бы узлы, которые являются комментариями, для каждого элемента массива.


Это могло бы выглядеть так:



{{author}}: {{message}}






Идея в следующем: когда при обработке шаблона встречается атрибут template-repeat, повторяющим шаблон, код проходит циклом по массиву, содержащемуся в свойстве, названном так же, как этот атрибут. Контекст шаблона (переменная values в instantiateTemplate) при работе цикла показывала бы на текущий элемент массива так, чтобы метку {{author}} искали бы в объекте comment, а не в теме.


Перепишите instantiateTemplate так, чтобы она это умела, и потом поменяйте шаблоны, чтоб они использовали эту возможность, и уберите лишние строки для создания комментариев из функции drawTalk.


Как бы вы организовали условное создание узлов, чтобы можно было опускать части шаблона, если определённое значение равно true или false?


А кто без скрипта?



Если кто-нибудь зайдёт на наш сайт с отключенным JavaScript, они получат сломанную неработающую страницу. Это не очень-то хорошо.

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


Попробуйте придумать способ, которым бы веб-сайт по обмену опытом можно было бы сделать работающим без JavaScript. Придётся ввести автоматические обновления страниц, а перезагружать странички пользователям придётся по старинке. Но было бы неплохо уметь просматривать темы, создавать новые и отправлять комментарии.


Не заставляю вас его реализовывать. Достаточно описать возможное решение. Кажется ли вам такой вариант сайта более или менее элегантным, чем тот, что мы уже сделали?


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog



Криптографические решения. От облачной подписи к доверенной среде

Данная статья является продолжением статьи «Криптографические решения. От криптопровайдеров до браузерных плагинов» и охватывает криптографические решения:

  • облачная подпись

  • отдельные браузеры с российской криптографией

  • отдельные почтовые клиенты с российской криптографией

  • российская криптография в фреймворках, платформах, интерпретаторах

  • настольные криптографические приложения

  • средства формирования доверенной среды






Облачная подпись




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

Для безопасного применения облачной подписи требуется решить задачу строгой аутентификации клиента при доступе к его закрытому ключу и задачу надежного хранения закрытого ключа на сервере. Примером подобного решения может служить КриптоПро DSS, который в качестве одного из вариантов аутентификации поддерживает Рутокен WEB (строгая двухфакторная аутентификация), а для хранения закрытого ключа использует HSM.

















































ПлатформыЛюбая с браузером и выходом в Интернет. Метод аутентификации может накладывать ограничения
Алгоритмы и криптографические протоколыЭЦП, шифрование, хэш-функция, имитозащита, HMAC, VKO
Интеграция с PKIX.509, PKCS#10, CMS, CRL, OCSP, TSP
Механизмы ЭЦПОтправка документа на сервера, подпись документа на сервере, возврат подписи

WEB API для интеграции в сторонние сервисы

SOAP-интерфейс для интеграции в сторонние сервисы
Механизмы аутентификациипо протоколу аутентификации Рутокен WEB

по SMS

логин-пароль
Форматы защищенных сообщенийPKCS#7, CMS, XMLSec, CADES
Интеграция с браузером100%
Мобильные платформыiOS, Android
Команднострочная утилитаЕсть
Хранилища ключейHSM, защищенная БД
Взаимодействие с USB-токенамиСуществует возможность аутентификации в сервисе облачной подписи по токенам (КриптоПРО DSS и Рутокен WEB)
Примеры (ГОСТ)КриптоПро DSS

“Облачная” подпись СКБ Контур

Сервис sign.me

Проблемы:



  • строгая аутентификация в сервисе

  • гарантии защиты закрытого ключа от НСД

  • снижение безопасности системы -> ограничение применения




Плюсы:


  • кроссплатформенность, кроссбраузерность

  • удобство для конечного пользователя — вообще ничего не надо устанавливать и настраивать

  • удобная интеграция в информационные системы (WEB API)


Отдельные браузеры с российской криптографией




Браузеры, созданные на базе open source проектов Mozilla FireFox и Chromium, используют в качестве криптоядра NSS или OpenSSL. OpenSSL поддерживает российские криптоалгоритмы. Для NSS также существуют разработки, которые обеспечивают поддержку российских криптоалгоритмов. Некоторое время назад на рынке появились полнофункциональные браузеры с поддержкой российской криптографии.

Подобное решение обладает большим, на данный момент невостребованным, потенциалом, так как позволяет создавать защищенные стандартные WEB-клиенты для систем с высокими требованиями к безопасности. Еще одним плюсом подобного браузера является его «портабельность». С учетом существования USB-токенов с защищенной FLASH-памятью созданы безопасные решения, в котором наиболее критические операции с закрытом ключом осуществляются на «борту» USB-токена, а сам браузер хранится в его защищенной от модификации FLASH-памяти. Подобное решение кроме высокого уровня безопасности является очень удобным в применении.


На базе NSS




На картинке представлена архитектура решения, реализованная в проекте по расширению NSS aToken.























































СпецификацияNSS c использованием PKCS#11-токенов, программных и аппаратных
ПлатформыСемейство Windows, GNU\Linux, OS X, iOS, Android
Алгоритмы и криптографические протоколыЭЦП, шифрование, хэш-функция, имитозащита, HMAC, VKO, TLS
Интеграция с PKIX.509, PKCS#10, CMS, CRL
Механизмы ЭЦПВызов из JavaScript встроенных в браузер функций
TLS-ГОСТВстроен в библиотеку и поддерживается браузером
Форматы защищенных сообщенийPKCS#7, CMS
Интеграция с браузером100%
Мобильные платформыiOS, Android
Хранилища ключейБраузерное хранилище, USB-токены
Взаимодействие с USB-токенамиХранилище ключей и сертификатов

Использование аппаратной реализации алгоритмов
ИнсталляцияПрограмма установки, в целом, не требуются права системного администратор

Portable. Например, запуск браузера с FLASH-памяти USB-токена
Примеры (ГОСТ)Mozilla FireFox, Chromium от Лисси

Проект atoken от R-Альфа (Mozilla FireFox)

КриптоFox (PKCS11-токен на базе КриптоПро CSP)

Проблемы:



  • только одно приложение с российской криптографией — сам браузер

  • обновление браузера

  • переучивать пользователя на использование кастомного браузера

  • сертификация (нет прецедентов)




Плюсы:


  • кроссплатформенность

  • прозрачность использования для пользователя

  • нет ограничений для разработчиков серверной части

  • не требуется инсталляция, запуск с FLASH-памяти USB-токена


Отдельные почтовые клиенты с российской криптографией




Отдельные почтовые клиенты с российской криптографией позволяют реализовать защиту переписки, используя электронную подпись и шифрование письма для абонента/группы абонентов (S/MIME). Данное решение удобно использовать в системах, построенных по принцу «точка-точка», в которых обмен информацией происходит непосредственно между абонентами, а сервер при этом используется только для маршрутизации сообщений.

















































ПлатформыСемейство Windows, GNU\Linux, OS X, iOS, Android
Алгоритмы и криптографические протоколыЭЦП, шифрование, хэш-функция, имитозащита, HMAC, VKO, TLS
Интеграция с PKIX.509, PKCS#10, CMS, CRL
Механизмы ЭЦПВызов из JavaScript встроенных в браузер функций
TLS-ГОСТВстроен в библиотеку и поддерживается браузером
Форматы защищенных сообщенийPKCS#7, CMS
Интеграция с браузером100%
Мобильные платформыiOS, Android
Хранилища ключейБраузерное хранилище, USB-токены
Взаимодействие с USB-токенамиХранилище ключей и сертификатов

Использование аппаратной реализации алгоритмов
ИнсталляцияПрограмма установки, в целом, не требуются права системного администратор

Portable. Например, запуск браузера с FLASH-памяти USB-токена
Примеры (ГОСТ)Mozilla ThunderBird от Лисси

DiPost от Фактор ТС

Российская криптография в фреймворках, платформах, интерпретаторах




Microsoft.NET




Расширения классов




В платформе существует набор криптографических классов, в которых предусмотрены механизмы расширения сторонними алгоритмами. Наиболее известным на рынке решением по расширению платформы Microsoft.NET российскими криптоалгоритмами является продукт КриптоПро. NET, представляющий собой надстройку над КриптоПро CSP.

Установка КриптоПро.NET позволяет использовать российские криптоалгоритмы, например,

в WEB-сервисах на базе ASP.NET, SOAP-сервисах, в клиентских браузерных приложениях MS.Silverlight.





















































ПлатформыMicrosoft.NET 2.0 и старше
Алгоритмы и криптографические протоколыЭЦП, шифрование, хэш-функция, имитозащита, HMAC, VKO, TLS, SOAP
Интеграция с PKIX.509, PKCS#10, CMS, CRL
Механизмы ЭЦПНабор классов. Есть полностью “управляемые” реализации. Есть реализации на базе Crypto API 2.0 и CNG
Механизмы аутентификацииклиентская аутентификация в рамках TLS

аутентификация в SOAP-сервисах

собственные механизмы аутентификации на базе ЭЦП случайных данных
TLS-ГОСТВстраивание
Форматы защищенных сообщенийPKCS#7, CMS, XMLSec, SOAP (OASIS Standard 200401), S/MIME
Интеграция с браузеромЭЦП и шифрование через MS Silverlight
Хранилища ключейРеестр, UBS-токены
Взаимодействие с USB-токенамиХранилище ключей и сертификатов

Использование аппаратной реализации алгоритмов

Через Crypto API 2.0
ПриложенияMicrosoft Lync 2010, Microsoft Office Forms Server 2007 и Microsoft SharePoint 2010, Microsoft XPS Viewer
ИнсталляцияMicrosoft. NET включен в состав Windows, начиная с Windows Vista. Поддержка российских криптоалгоритмов требует установки дополнительного ПО
Примеры (ГОСТ)КриптоПро. NET (на базе КриптоПро CSP)

Отдельные библиотеки




BouncyCastle — это open source библиотека, в которой реализована своя система криптографических классов для платформы Microsoft.NET. В библиотеке поддерживаются как базовые криптографические алгоритмы ГОСТ 28147-89, ГОСТ Р 34.10-2001, ГОСТ Р 34.11-94, так и криптографические форматы PKCS#7/CMS, PKCS#10, X.509 с учетом специфики, описанной в RFC российских производителей СКЗИ. Кроме того, по утверждениям разработчиков библиотека поддерживает формат CADES с российскими криптоалгоритмами.

Java




Архитектура криптографической системы платформы Java (Java ™ Cryptography Architecture) позволяет расширять набор поддерживаемых в платформе криптоалгоритмов. С учетом большой распространенности Java многие из российских разработчиков криптосредств предлагают сертифицированные JCP-провайдеры.

JCP

































































СпецификацияJava ™ Cryptography Architecture, JavaTM Cryptography Extension, JavaTM Secure Socket Extension
ПлатформыSun Java 2 ™ Virtual Machine
Алгоритмы и криптографические протоколыЭЦП, шифрование, хэш-функция, имитозащита, HMAC, VKO, TLS
Интеграция с PKIX.509, PKCS#10, CMS, CRL, OCSP, TSP
Механизмы ЭЦПНабор классов
Механизмы аутентификацииклиентская аутентификация в рамках TLS
TLS-ГОСТОтдельный TLS-провайдер, реализованный на Java в соответствии со спецификацией JavaTM Secure Socket Extension
Форматы защищенных сообщенийPKCS#7, CMS, XMLSec (например, через Apache XML Security API), S/MIME;
Интеграция с браузеромЭЦП/шифрование через Java-апплеты, загрузка апплетов через Java TLS
Интеграция со службой каталоговс произвольным LDAP-каталогом
Мобильные платформыAndroid
Хранилища ключейРеестр, файлы, UBS-токены, MicroSD-токены
Взаимодействие с USB-токенамиХранилище ключей и сертификатов

Использование аппаратной реализации криптоалгоритмов через PKCS#11 (в продуктах Java LCPKCS11 компании Лисси и в Java-провайдере для Рутокен ЭЦП компании Актив)
ИнсталляцияПрограмма установки, требуются права системного администратора
Примеры (ГОСТ)КриптоПро JCP, КриптоПро JTLS

Signal-COM JCP, Signal-COM Java TLS

LCJCE, LCJSSE, LCPKCS11

Java-провайдер для Рутокен ЭЦП

Trusted Java

Java-апплеты




Одним из вариантов использования СКЗИ в браузере является их интеграция в Java-апплеты.

В ряде случаев СКЗИ и криптографические библиотеки не требуют установки и представляют собой нативную библиотеку. В этом случае возможна ее интеграция непосредственно «внутрь» апплета и вызов функций СКЗИ через механизм JNI. При этой схеме библиотека будет инсталлирована в профайл пользователя при первой загрузке Java-апплета в браузере и ее отдельной инсталляции не потребуется.

Другим вариантом является написание Java-апплета, который вызывает предустановленное в системе СКЗИ (CSP, JCP и др.)

Более подробно пример подобной реализации, основанный на использовании Рутокен ЭЦП и OpenSSL, описан в статье http://ift.tt/1wt6WXU.


Примеры:



  • Апплет ЭТП «Стройторги» (реализован в соответствии с приведенной на схеме архитектурой)

  • Система ДБО Бифит


PHP




PHP является одним из наиболее распространенных языков WEB-разработки. Криптографическая подсистема PHP построена на базе OpenSSL, в котором есть поддержка российских криптоалгоритмов. Но при этом в самом PHP поддержки российских криптоалгоритмов нет. Некоторые российские производители СКЗИ приступали к формированию патча к PHP, который позволял бы использовать российскую криптографию, но до конца эти работы доведены не были.

Бинарная совместимость таких СКЗИ, как МагПро КриптоПакет, с OpenSSL позволила бы придать данному решению легитимность.

В настоящее время многие разработчики инфосистем на базе PHP используют непосредственный вызов командно-строчной утилиты OpenSSL для проведения криптоопераций с использованием российских алгоритмов.

Экзотическое решение реализовано в рамках проекта Рутокен WEB. В серверной компоненте решения проверка подписи ГОСТ Р 34.10-2001 реализована непосредственно на PHP с использованием математических примитивов из нативной библиотеки.


Perl




Еще одним экзотическим примером является реализация шифрования по ГОСТ 28147-89 непосредственно на Perl http://ift.tt/1H0Pong.

При этом в реальных проектах на Perl разработчики обычно используют вызовы командно-строчной утилиты из OpenSSL или какого-нибудь Linux-совместимого СКЗИ.

Ruby




Ruby использует в качестве криптоядра openssl, что позволило автору данной статьи http://ift.tt/1oz13Ep пропатчить его для поддержки российской криптографии.

JavaScript




Некоторое время назад на Хабре появилась статья, автор которой реализовал многие криптографические форматы непосредственно на JavaScript

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

http://ift.tt/RkxgSk


Проблемы:



  • Нет ГОСТов

  • Закрытый ключ находится в «хранилище браузеру», а не в отчуждаемом носителе

  • Как подключать PKCS#11-совместимые устройства?


Плюсы:



  • кроссплатформенное, кроссбраузерное решение

  • подпись на клиенте

  • Поддержка PKI

  • не требуется установка вообще ничего на клиент


Настольные криптографические приложения




Класс приложений, которые предоставляют законченный оконный пользовательский интерфейс для проведения клиентских криптоопераций. Как правило, используют некоторое СКЗИ в качестве криптоядра.

Операции:



  • подпись файла

  • проверка подписи под файлом, в том числе построение цепочки и проверка списка отзыва, OCSP, проверка таймштампа

  • зашифрование файла, в том числе для нескольких респондентов

  • расшифрование файла

  • поиск и выбор сертификата пользователя

  • просмотр сертификата

  • ведение базы сертификатов респондентов, интеграция со службой каталога (по протоколу LDAP) для поиска сертификата респондента

  • генерация ключевой пары, формирование запроса на сертификат

  • удаление ключевой пары

  • импорт/экспорт сертификатов (корневых, пользовательских, респондентов)

  • удаление сертификата


Примеры:



  • КриптоАРМ

  • КриптоНУЦ

  • File-PRO, Admin PKI

  • Блокхост ЭЦП

  • Sign Maker

  • ViPNet Crypto File


Средства формирования доверенной среды




Проблема формирования доверенной среды для выполнения криптоопераций, в частности ЭЦП, является отдельной большой темой. В данной статье не планируется ее подробно рассматривать, но хочется отметить, что концептуально разработчики идут следующими путями:


  • отдельное устройство, на котором визуализируются данные, предназначенные для подписи и сама подпись производится после подтверждения пользователя (trustscreen)

  • установка на компьютер и клиентскую ОС комплекса средств защиты информации (МДЗ, антивирусы и т.п.), с целью минимизации возможности заражения компьютера вредоносным ПО

  • загрузка отдельной доверенной ОС в режиме USB-live

  • параллельная работа клиентской ОС и доверенной среды на различных ядрах одного компьютера


На последнем способе формирования ДС хотелось бы остановиться подробнее.


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


Для случая многоядерного компьютера доверенная среда функционирует на 2 ядрах, на остальных ядрах функционирует клиентская ОС. Доверенная среда загружается перед загрузкой клиентской ОС либо с флешки, либо с электронного замка Соболь. Решение гарантирует, что клиентская ОС (а следовательно и потенциальное вредоносное ПО) не управляет поведением доверенной среды.

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


Для доступа к доверенной среде из клиентской ОС используется специальная библиотека (COM-объект). При подписи платежки через данную библиотеку Jinn перехватывает управление графическим адаптером и визуализирует на нем платежку. Если представленная информация верна, то после подтверждения пользователя Jinn подписывает платежку и возвращает управление клиентской ОС.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


пятница, 19 декабря 2014 г.

TeamCity 9.0: импорт проектов, настройки в VCS, фоновая очистка и панды

На прошлой неделе вышла новая версия нашего CI сервера: TeamCity 9.0. Вышла она под лозунгом: “Ваш бессменный билд-инженер”, и это неспроста. Новая функциональность направлена во многом на облегчение работы билд-инженеров, а также позволяет билд-серверу работать 24/7, без перерывов на обед maintenance.

Конечно, список всех улучшений, как всегда, весьма внушительный, и с ним можно ознакомиться при желании. Однако здесь мы остановимся на ключевых фичах последнего релиза.



Импорт проектов




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

Краткое видео о том, как это выглядит на деле (английский):


Настройки в VCS




Теперь можно хранить настройки проектов в системах контроля версий (пока поддерживаются Git и Mercurial), просматривать и инспектировать их — так же, как исходный код. Можно использовать свои любимые инструменты для аудита и просмотра изменений. И самое главное: риск допустить фатальную ошибку снизился, ведь в любой момент все можно откатить. Также можно следить за коллегами и видеть, кто, что и когда менял в настройках.

Еще одно короткое демо-видео (английский):


Очистка в фоновом режиме




Если раньше очистку сервера (clean-up) необходимо было включать каждый день, из-за чего сервер был недоступен от 15 минут до нескольких часов, то в новой версии очистка выполняется в фоновом режиме. За счет этого полностью предотвращаются простои серверов, и сборке билдов уделяется 100% серверного времени. Это особенно полезно для распределенных команд и команд с большим количеством агентов и серверов TeamCity.

Создание и редактирование диаграмм




Упрощенное управление настраиваемыми диаграммами позволяет редактировать их прямо из интерфейса TeamCity, без необходимости править XML.


Любимые билды




Появилась также функция Favorite builds, которая позволяет отмечать определенные билды как “любимые” и с легкостью отслеживать их в любое время, без необходимости держать большое число открытых вкладок в браузере.


Интеграции




TeamCity 9.0 также предлагает ряд полезных интеграций (в виде плагинов) с важными инструментами:


  • Интеграция с Microsoft Azure позволяет легко масштабировать Вашу «билд-ферму» по мере надобности. TeamCity запустит столько агентов в облаке Azure, сколько необходимо, а после спада нагрузки остановит их.

  • Для более эффективной обработки билдов теперь также можно использовать удаленные агенты TeamCity, установленные на виртуальных машинах VMware vSphere. Проанализировав очередь билдов, TeamCity сам запустит виртуальные машины с соответствующими агентами TeamCity.

  • Интеграция с Microsoft Visual Studio Online позволяет настроить VSO в качестве системы контроля версий в TeamCity, связать TeamCity билды и историю контроля версий с VSO Work Items, а также получать уведомления от TeamCity прямо в Team Rooms.


Загрузить последнюю версию TeamCity 9.0 можно с нашего сайта. Замечания и предложения по новой версии можно оставлять в нашем баг-трекере.


Также приглашаем вас ближе познакомиться с новыми возможностями TeamCity 9.0, посетив наш бесплатный вебинар 21 января 2015 года: регистрация уже открыта.


Напоминаем, что вы можете пользоваться TeamCity без ограничений по времени и функциональности, бесплатно. Если вам вдруг перестанет хватать трех агентов и 20-ти билд конфигураций, или вы захотите общаться с саппортом по почте, то всегда можно приобрести Enterprise лицензию, предварительно получив триал на 60 дней.


Удачных сборок!

Команда JetBrains


P.S. А про панд все написано здесь, кстати.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog


Как мы увеличили отзывчивость приложения


Почти любое современное приложение не обходится без загрузки картинок из сети и мы, Surfingbird, не исключение. Однако, нельзя просто загружать картинки последовательно, потому что, если пользователь перемотает пару экранов, ему придётся ждать пока загрузятся предыдущие изображения, которые уже и не нужны.

Поэтому, для увеличения отзывчивости приложения и снижения времени ожидания пользователя, мы применили несколько приёмов, о которых и хотим сейчас рассказать.



Хочется отметить, что многие используют ленивую загрузку картинок. Это отличная практика, мы её любим и уважаем. Это экономия трафика и памяти на телефоне. Однако, чтобы позаботиться о памяти, важно не только организовать грамотное хранение загруженных изображений, например, вытесняющий кэш, но и оптимизировать саму загрузку.

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


Возьмем случай, когда приложение — это набор картинок. Как поступит разработчик, который не задумывается об отзывчивости приложения? Он возьмет все картинки, положит их в очередь на загрузку и окажется, что они будут загружаться, в среднем, одна за другой.

Посмотрите, как бы это могло выглядеть.


Что мы видим? Пользователю приходится ждать, пока загрузятся предыдущие картинки.

При этом, пользователь не видит, что что-то вообще происходит. Сейчас для пользователя приложение тормозит, а всё потому что нет никакой возможности понять – грузится картинка или нет.


Важно, чтобы у пользователя было понимание, что всё хорошо, всё грузится, ещё чуть-чуть и появится картинка. Первое что мы сделали — это добавили прогрессбар загрузки картинки. Реализовать это просто: добавляем сабвью к нашей картинке и меняем прогресс по колбеку:



// не надо относится к этому коду как к живому
// многие вещи типа логики показа/сокрытия progressView и тому подобные вещи скрыты
// код дан для того, чтобы было проще понять суть

-(void)layoutSubviews
{
CGRect rect = self.progressView.frame;
rect.size.width = self.bounds.size.width * self.progress;
self.progressView.frame = rect;
}

-(void)setProgress:(CGFloat)progress
{
_progress = progress;

if (_progress > 1)
_progress = 1;
if (_progress < 0)
_progress = 0;

[self setNeedsLayout];
}





И прописываем операции downloadProgressBlock (для операций, унаследованных от AFURLConnectionOperation):

[imageView.af_imageRequestOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
weakSelf.progress = totalBytesRead/totalBytesExpectedToRead;
}];





Вот что из этого получается.

Несмотря на то, что ожидание загрузки картинки такое же, как и в первом случае, в итоге, у пользователя складывается ощущение, что всё хорошо.

Но это совсем не идеал. Как видно из примера, если пользователь перемотает совсем далеко, то ожидание загрузки картинки будет очень долгим. Так почему бы не показывать в первую очередь то, что на экране именно сейчас?


Для загрузки каждой картинки мы используем NSOperation. Как только мы получаем очередную порцию контента — создаем операцию загрузки для каждой картинки и помещаем её в очередь со средним приоритетом. Как только картинка оказывается в поле зрения — устанавливаем операции на ее загрузку высокий приоритет, и именно эта картинка грузится раньше тех, которые нам сейчас не нужны.


Есть еще одна тонкость — вероятнее всего, что те картинки, которые пользователь проскроллил, ему менее нужны, нежели те, которые внизу. Поэтому, как только картинка исчезает с экрана, мы устанавливаем операции на ее загрузку приоритет ещё меньший, нежели приоритет операций на предзагрузку.



- (void) willAppear
{
[self.af_imageRequestOperation setQueuePriority:NSOperationQueuePriorityVeryHigh];
}

- (void) willDisappear
{
[self.af_imageRequestOperation setQueuePriority:NSOperationQueuePriorityVeryLow];
}




В результате, нам удалось увеличить отзывчивость приложения и снизить время ожидания пользователя. Визуально приложение стало работать быстрее.

Если вы считаете, что подобные трюки не работают, советуем почитать это исследование фейсбука.


А вы применяете что-то подобное? Будет приятно обсудить в комментариях.


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Want something else to read? How about 'Grievous Censorship' By The Guardian: Israel, Gaza And The Termination Of Nafeez Ahmed's Blog