Многопоточность в Perl, или Как я посмотрел ролик о съёмках Warehouse 13

    image
    Всё началось с того, что я наткнулся на видео, которое рассказывало о съёмках одного из моих любимых сериалов, Warehouse 13:
    www.aoltv.com/2009/07/10/behind-the-scenes-of-warehouse-13

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

    Но, как принято на сайте AOL TV, видео оказалось доступно только жителям US. Зачем накладывать такие ограничения — я не могу взять в толк. Где и какая жаба их душит, непонятно.
    Но такая мелочь меня не могла остановить.

    Очевидно, необходимо было найти прокси-сервер, за которым я бы выглядел для сайта aoltv как настоящий америкос. А для этого нужно было сначала найти список проксей, потом найти рабочие, и потом — американские.

    Кстати, список подходящих проксей я нашёл здесь:
    www.proxz.com/proxy_list_anonymous_us_0.html

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

    Понятно, что проверка прокси практически не загружает компьютер и сеть, но при этом может проходить довольно долго, учитывая что прокся может не работать или находиться слишком далеко. Поэтому логичным было подключить многопоточность. Заодно была протестирована многопоточность в perl под Windows, с ActiveState perl 5.10.1

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

    Потоки в perl подключаются прагмой

    use threads;

    Подробности: perldoc.perl.org/threads.html

    Если при её использовании вы получите такой отказ:

    This Perl hasn't been configured and built properly for the threads module to work. (The 'useithreads' configuration option hasn't been used.)

    значит, пришло время собирать perl’ы.

    После этого мы можем создать новый поток командой

    $thread=threads->create(\&job_todo, $param);

    job_todo — это имя функции, в которой и должна быть описана работа потока.
    После ссылки на функцию можно передавать другие параметры, например, порядковый номер потока. Правда, у потока есть метод tid(), который и возвращает порядковый номер, но для примера оставим этот параметр.
    На выходе получаем объект $thread.

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

    Чтобы основная программа дождалась потоков, необходимо выполнить команду

    $thread->join();

    Ещё один вопрос с потоками — а как же им обмениваться информацией друг с другом, например, узнать, какие прокси из списка уже обработаны, а какие — нет? Дело в том, что по умолчанию каждый поток получает независимую копию объявленных до создания потоков переменных, и они остаются отрезаны друг от друга, как Золушка от принца.

    Для этого существует прагма

    use threads::shared;

    которая позволяет объявлять и использовать общие, т.е. shared, переменные. Если переменную объявить так:

    my $useme:shared=0;

    то у всех потоков переменная $useme станет общей — когда один поток меняет её значение, другие потоки видят уже измененное значение.

    Таким образом, мы можем сделать массив проксей, и к нему — shared указатель на позицию массива, по которой лежит следующая необработанная прокся.

    Итак, каждый поток в начале работы берёт следующую проксю из списка, и передвигает указатель далее по массиву.
    Проверяется прокся на дееспособность просто. Как известно, существуют три типа проксей — анонимные, открытые и нерабочие.
    А ещё, анонимные бывают сильно анонимные (от которых никак не узнать, кто за ней стоит) и слабо анонимные (которые в одной из переменных всё-таки передают ip-адрес скрытного хитреца).
    Нам нужно просто понять, работает ли прокся вообще, и какой ip у нас получается при использовании этой прокси. Для этого любой подойдёт сайт, который сообщает каждому его внешний ip-адрес.

    Я выбрал для эксперимента сайт www.myip.ru. Однако, если заниматься проверкой проксей на постоянной основе, лучше сделать свой простой скрипт и положить его на какой-нибудь сервер — и быстрее, и чужой сервер не грузим.
    Тем более, что скрипт этот может быть настолько простым:

    #!/usr/bin/perl
     
    print "Content-type: text/html\n\n$ENV{REMOTE_ADDR}";


    Итак, поток, взяв проксю, пытается через неё запросить сайт и посмотреть, какой ip возвращает сайт.
    Если запрос не проходит — прокси не работает, если сайт показывает наш собственный ip — значит, прокся открытая. А вот если сайт показывает другой ip, значит мы достигли цели, прокся и рабочая и анонимная.

    Список проксей скрипт получает из файла, формат которого неважен, главное что там встречаются ip-адреса проксей с портами, в виде 12.34.56.78:8080
    Конечно, можно пойти чуть дальше и автоматически парсить какой-нибудь из сайтов, публикующих список проксей.

    Также мне не потребовалось проверять прокси на принадлежность к стране, так как первая же прокси оказалась из US. Это тоже будет нетрудно сделать вам в качестве домашнего задания ( use Geo::IP; ).

    Далее приведён текст скрипта с комментариями. Используйте, как хотите. В случае получения лютой прибыли вышлю номер кошелька для пожертвований.
    Главное, не причиняйте никому вреда, и да пребудет с вами perl.

    PS: ролик поглядел,- коротко, но интересно.

    #!/usr/bin/perl -w
     
    # Подключаем библиотеки
    use strict;
    use LWP::UserAgent;
    use threads;
    use threads::shared;
     
    # Установка переменных
    my @threads;
    my @proxy;
    my $threads=100;
    my $last_p:shared=0;
    my $ip_checker='http://www.myip.ru/get_ip.php?loc=';
     
    # Список рабочих анонимных проксей
    open LOG, ">>proxy.log";
     
    # Узнаем свой текущий внешний ip
    my $ua = LWP::UserAgent->new;
    $ua->agent("Mozilla/5.0");
    my $res=$ua->get($ip_checker);
    exit if (!$res->is_success);
    my $s=$res->decoded_content;
    $s=~m/(\d+\.\d+\.\d+\.\d+)/s;
    my $myip=$1;
     
    # Читаем список проксей
    open FIL, 'proxy.lst';
    while ($s=<FIL>)
    {
      while ($s=~s/(\d+\.\d+\.\d+\.\d+:\d+)//)
      {
        push(@proxy, $1);
      }
    }
    print "Proxies found: ".@proxy."\n";
     
    # Создаём нужное количество потоков
    for my $t (1..$threads) {
      push @threads, threads->create(\&check_proxy, $t);
    }
    # Дожидаемся окончания работы всех потоков
    foreach my $t (@threads) {
      $t->join();
    }
     
    sub check_proxy
    # Проверка проксей
    {
      # Номер текущего потока
      my $num=shift;
      print "+ Thread $num started.\n";
     
      # Бесконечный цикл
      while (1)
      {
        # Берём следующий номер в списке
        my $seq=$last_p++;
        # Если список кончился, заканчиваем
        if ($seq>=@proxy)
        {
          print "- Thread $num done.\n";
          return;
        }
        # Получаем следующую проксю из списка
        my $proxy=$proxy[$seq];
     
        # Стартует качалка
        my $ua = LWP::UserAgent->new;
        $ua->agent("Mozilla/5.0");
        $ua->proxy(['http'], "http://$proxy/");
        my $res=$ua->get($ip_checker);
     
        # Отчёт
        printf("Thread %02d; Seq. %03d; Proxy %20s; Status: ", $num, $seq, $proxy);
        # Не работает
        if (!$res->is_success) {
          print "Unable to connect.\n";
          next;
        }
     
        # Отданная страничка
        my $s = $res->decoded_content;
     
        # На странице наш ip, значит прокся открытая
        if ($s =~ m/$myip/s)
        { 
          print "Open.\n";
        }
        # Прокся не обязательно оставляет свой ip.
        # Если на странице есть какой-то ip, но не наш - значит, прокся анонимная
        elsif ($s =~ m/(\d+\.\d+\.\d+\.\d+)/s)
        { 
          print "Anonymous!\n";
          print LOG "$proxy\n";
        } 
        # Что-то нехорошее произошло
        else
        { 
          print "Not working...\n";
        }
      }
    }
     
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 30

      +3
      1) Простой скрипт с REMOTE_ADDR — не катит, в REMOTE_ADDR по идее всегда будет тупо адрес проксика, который шлет запрос. Надо проверять HTTP_X_FORWARDED_FOR, HTTP_REAL_IP как минимум, а вообще неплохо бы по всему массиву заголовков пройтись и проверить, т.к. бывают атипичные.
      2) Из пункта 1 неявно следует, что 100% полагаться на myip.ru тоже особого смысла нет, т.к. неизвестно какие он IP адреса выдает, из каких «переменных»
        0
        p.s.: И применительно к решаемой задаче. Крайне редко анонимные фришные проксики бывают достаточно быстрыми для просмотра видео. А учитывая их нестабильность и относительно малую живучесть, смысла в них для этих целей не много на самом деле. Имхо разумнее замутить нечто вроде этого habrahabr.ru/blogs/infosecurity/107631/ или тупо купить американский проксик.
          0
          Ну, как решение единовременной проблемы меня устроило. И видео я посмотрел Ж)
          Если на постоянной основе пользоваться aol tv, то проще купить vpn.
          0
          Вообще-то именно в этом и состояла задача. Посмотреть, какой адрес оказывается в REMOTE_ADDR
          Не ставил задачу отделять высоко анонимные прокси, где действительно надо просматривать все возможные заголовки.
            0
            REMOTE_ADDR всегда содержит адрес сервера, непосредственно посылающего запрос на конечный сайт. То есть в Вашем случае — прокси сервера. Попытка найти в нём свой IP заведомо обречена на провал:)
              0
              Не всегда.
              Например, proxy.corbina.ru настроена именно так, что в remote_addr оказывается мой реальный ip.
                0
                Какой же это прокси тогда?
            0
            распечатайте все полученные заголовки и ищите среди них свой айпи
            если его там нет — прокси анонимная
            +6
            Вместо того, чтобы обращаться к некоему HTML-сайту, определяющему IP, выкачивать с десяток килобайт, а затем парсить — не лучше ли использовать ifconfig.me/ip?
              –1
              полезный какой сайтик, спасибо
              0
              Я как-то точно такой же многопоточный скрипт сделал (сначала надеялся на LWP::Parallel::UserAgent, но не получилось) — работает, но памяти жрёт… Для проверки анонимности запроса использовался свой скрипт на хостинге, который проверял наличие лишнего в запросах.

              Минусы перловой многопоточности: 1) сколько памяти занимает 100 потоков? — дофига; 2) а если в этих потоках не обращение к медленным web-серверам делать, а что-то более быстрое, — какую долю будет занимать обращение к shared переменным? В Windows и Linux результаты будут немного разные, но общий вывод не очень утешительный.
                +1
                Увидев картинку, я подумал, вы устройство Фарнсворта сделали и через него посмотрели ролик) Блог «Perl» не разглядел…
                  +2
                  Эту задачу можно решить без use threads;
                  Например, AnyEvent + AnyEvent::HTTP (или обертка над ним).
                  Если хотим, чтобы выглядело как треды, то use Coro;
                    0
                    Спасибо за информацию.
                    Вообще, я задумывал пример с threads, а прокси просто под горячую руку подвернулись.
                      0
                      Я бы сказал ее нужно решать без use threads :) Особенно учитывая количество сетевых операций, которые не thread-safe в перле.
                      +2
                      Еще один велосипед с тредами и сокетами. Люди, расскажите, кто вас научил распараллеливать сетевые операции тредами? Почему вы не используете select / AnyEvent?
                        0
                        Отличная идея!
                        Если будет время разобраться с anyevent, и про них статью напишу.
                        А вообще, статья задумывалась в первую очередь как пример использования threads
                        +1
                        Фиг с ним с перлом. Сериал надо посмотреть. Название правильное.
                          0
                          Какой то странный код даже для обучающего оО

                          В каком формате список прокси лежит? Если в виде — каждый прокси на строку в файле, то цикл перебора можно упростить до:
                          # Читаем список проксей
                          open FIL, 'proxy.lst';
                          my @proxy = ;
                          @proxy = grep { /(\d+\.\d+\.\d+\.\d+:\d+)/ } @proxy;
                          print "Proxies found: ".@proxy."\n";


                          выборка следующего прокси из массива тоже не особо ясная конструкция, ее можно привести к виду:
                          # Получаем следующую проксю из списка
                          my $proxy=shift(@proxy);
                          # Если список кончился, заканчиваем
                          unless ($proxy) {
                          print "- Thread $num done.\n";
                          return;
                          }


                          И вообще лучше не использовать бесконечный цикл с тредами, а использовать AnyEvent.

                            0
                            Блин, хабрапарсер пожевал код :(
                            my @proxy = <FIL>; должно быть
                              0
                              Суть в том, что файл может быть в любом формате — например, весь текст выделен в браузере и скопирован в файл, а на странице было указано несколько проксей в одной строке, например в таблице.
                              Поэтому указанный способ более универсален.
                              Чем лучше отформатирован список, тем, понятно, проще его будет обрабатывать.
                                0
                                Ну тогда строку: @proxy = grep { /(\d+\.\d+\.\d+\.\d+:\d+)/ } @proxy;
                                надо заменить на: @proxy = map { /(\d+\.\d+\.\d+\.\d+:\d+)/g } @proxy;
                                  0
                                  И почему никто не использует m//g в списковом контексте:
                                  my @ips = $text =~ m/\d+\.\d+\.\d+\.\d+/g;
                              0
                              А без lock при использовании threads::shared будут дублированные проверки проксей.
                                0
                                Хм.
                                В каком месте может быть у них столкновение? Пока что-то не вижу.
                                  0
                                  # Берём следующий номер в списке
                                  my $seq=$last_p++;

                                  А разве здесь несколько потоков не могут получить одинаковые данные?

                              • UFO just landed and posted this here
                                  0
                                  Да, а пока в России его никто не крутит, значит и посмотреть ролик двухминутный нельзя.
                                  А если какой-нибудь тв3 его купит, не факт, что он купит права для этого интернет-ресурса.
                                  Так что своими силами справляемся Ж)
                                  0
                                  Имею такую ситуацию: в одной компьютерной сети есть считалка трафика, которая разгребает потоки netflow и складывает результаты в БД.

                                  Считалка написана на Perl и в критические моменты скушивает аж одно ядро процессора. Мне то процессора не жалко, но дело в том что есть ещё 7 ядер которые загружены на 1..2 процента.

                                  Умеет ли Пёрл параллелиться на несколько процессов? или здесь только fork() спасёт ситуацию?
                                  Потому что thread'ы оно хорошо, но с точки зрения планировщика операционной системы это всё-равно остаётся одним процессом.
                                    –1
                                    Ну, насколько я понимаю, fork() — это и есть разделение на несколько процессов.

                                  Only users with full accounts can post comments. Log in, please.