Менеджер відправки запитів в проєкті ZennoPoster

Звернувся до мене чоловік на форумі з проблемою – потрібно відправити декілька HTTP запитів методом POST, у якому URL буде одинаковим, а тіло запиту буде в JSON його потрібно сформувати використовуючи список ZennoPoster. І всі запити потрібно відправити бажано одночасно, і в результаті потрібно дізнатись скільки запитів було виконано успішно ( HTTP 200 ).

Я написав код, який вирішує цю задачу. Проте, я подумав, що подібне рішення буде корисним і іншим людям. Наприклад є у нас список проксі, і хочеться швидко одержати результат скільки саме в списку живих проксі серверів. Або є список фотографій, і їх потрібно завантажити кудись, наприклад в телеграф чи imgbb. Власне логіка суттєво не зміниться, якщо замість POST потрібно відправляти запит методом GET. Тому, давайте глянемо рішення.

Спочатку опишу функцію, яка буде приймати два текстових рядки, і з них буде формувати JSON.
Цю функцію я розміщу в першому блоці свій C# код, і останнім рядком добавлю її в контекст, щоб можна було визивати її з будь-якого іншого блока мого проєкта в програмі ProjectMaker. Власне про те як використовувати функції в своїх проєктах я уже розказував в попередніх публікаціях, тому якщо код не зрозумілий – краще знайти публікацію і почитати, тоді код нижче буде більш зрозумілим.

Func<string,string, string> GetBodyJson = (string text, string queryId) =>{
    dynamic f0 = new System.Dynamic.ExpandoObject();
    f0.data = text;
    f0.queryId = queryId;

    string json = Global.ZennoLab.Json.JsonConvert.SerializeObject(f0,  Global.ZennoLab.Json.Formatting.Indented);
    return json;
};

project.Context["body"] = GetBodyJson;

Наступний крок – описую функцію, яка буде відправляти HTTP запит методом POST. На вхід буде приймати URL, власне JSON і в даному випадку затримку, щоб виділити на запит рівно стільки часу, скільки ми згідні чекати на його завершення, часом це нормально так пришвидшує роботу проєкта, коли робота проводиться на повільних проксі серверах (коли краще завершити роботу, і повторити її з іншого проксі сервера уже в іншому потоці). А так як я планую цю функцію запускати в асинхронному коді, і в цілому не хочу думати що вона може повернути мені помилку – то одіваю код, де може статись помилка в конструкцію try/catch, і спеціально вивожу в лог повідомлення про помилку, щоб у випадку якщо вона буде розуміти що відбувається.
Заголовки для цього запиту будуть вказуватись саме в цій функції один раз, вона також буде використовувати UserAgent з профіля, буде одержувати проксі сервер проєкту (який прокине ZennoPoster з проксічекера, або буде установлений попередніми блоками в браузер чи проєкт). Також з запитом в цьому випадку буду використовувати CookieContainer, так як підрозумівається що замовник спочатку загрузить профіль, а тоді вже буде виконуватись вся інша робота.

Func<string,string,int,string> POST = (string url, string content, int Timeout) => {
    var method = ZennoLab.InterfacesLibrary.Enums.Http.HttpMethod.POST;
    var type = ZennoLab.InterfacesLibrary.Enums.Http.ResponceType.HeaderOnly;
    
    string content_type = "application/json";
    string proxy = project.GetProxy();
    string encoding =Encoding.UTF8.WebName;

    string cookie = string.Empty;// візьмемо з профіля
    string UserAgent = project.Profile.UserAgent;
    string [] headers =  new[]{
        string.Format("Content-Length: {0}",  content.Length)
    };
    var CookieContainer = project.Profile.CookieContainer;

    string text = string.Empty;
    try {
      text =    ZennoPoster.HTTP.Request(
      method: method, // метод яким буде відправлено запит
      url: url,// по якому адресу буде відправлено запит
      content: content, // які дані буде відправляти запит
      contentPostingType: content_type, // який тип контенту буде відправляти запит
      proxy: proxy, // з якого IP відправляти запит
      Encoding: encoding, // в якому кодуванні відправляти запит
      respType: type, // в якому вигляді повернути результат
      Timeout: Timeout, // скільки часу очікувати результат
      Cookies: cookie, // використовувати додаткові cookie
      UserAgent: UserAgent, // використовувати вказаний UserAgent
      UseRedirect: false, // включити слідування переадресаціям
      MaxRedirectCount: 0, // слідувати php переадресаціям
      AdditionalHeaders: headers, // добавити заголовки до цього запиту
      DownloadPath: null, // шлях до папки для збереження результату
      UseOriginalUrl: true, // не змінювати параметри запиту
      throwExceptionOnError: false, // генерувати помилку якщо не одержали результат
      cookieContainer: CookieContainer, // використання контейнера для cookie
      removeDefaultHeaders: true // видалення стандартних заголовків
    );
    }
    catch(Exception e){
        project.SendWarningToLog(string.Format("{0} {1}",e.Message, text));
    }

    return text;
};

project.Context["post"] = POST;

Як бачимо, цю функцію я також закинув в контекст, щоб потім можна було звертатись до неї з інших блоків. Проте, потрібно розуміти, що сам виклик методів і об’єктів GetProxy, UserAgent, CookieContainer може бути затратною процедурою в часі (наприклад тоді, коли ми будемо працювати в декілька потоків з цією функцією у своєму C# коді – і один потік наприклад буде одержувати куки з CookieContainer, а інший буде пробувати туди їх записати) – в більшості випадків все буде добре, і буде працювати нормально. Але у деяких специфічних умовах можливо треба планувати так, щоб не звертатись до них (замінити UserAgent і GetProxy на конкретні значення, замість CookieContainer використовувати рядок cookie).
Про запити я уже писав також окремо, тому більш детально зупинятись не буду – проте якщо щось не зрозуміло, рекомендую переглянути декілька додаткових статей про запити на цьому блозі.

Наступний крок – логіка у якій буде оброблятись черга на виконання запитів. Є у моєму проєкті список з іменем list, у якому є вже текстові дані. Проте, якщо їх немає, то для того щоб заповнити список використовую перший рядок коду, описаного нижче. Важливо, що цей список не повинен бути прив’язаним до файлу, тому що заповнюється він і беруться з нього дані без синхронізації потоків (тобто різні потоки ZennoPoster повинні в цьому випадку працювати зі своєю локальною копією цього списку).

Власне список заповнюється спочатку Demo даними.
Потім ініціалізуються функції, які були описані в попередніх блоках з контексту. Тут також потрібно відмітити, що використовується приведення типу даних, тому що в іншому випадку компілятор видає помилку (адже потім ці функції прокидаються у вигляді аргументів у лямбда функцію).

Після чого форумується повний список всіх задач, які потрібно виконати (в нашому випадку відбувається підготовка всіх необхідних запитів, тобто все підготуємо, але ще не виконуємо). А вже потім, одночасно використовуючи Linq AsParallel або просто Linq ForEach послідовно запускаємо на виконання всі завдання. Так як ми використовуємо Task, то вони всі будуть виконуватись асинхронно, тобто виконання одного не буде мішати виконуватись іншому.

Залишається тільки дочекатись поки всі завдання будуть виконані і повернуть результат. Приблизний час виконання буде близьким до timeout, який ми вказуємо самостійно + якийсь мізерний час на запуск потоків. Щоб було зрозуміло – то точно таке завдання якщо робити послідовно – то на 100 запитів буде затрачено timeout x 100 = 5000 x 100 = 500 секунд, а варіант, який я пропоную – це все займе всього 5 чи 10 секунд (в залежності на скільки завантажений процесор, наскільки швидко запускає потоки).

Ось код реалізації, яку я описав вище:

for(int i = 0;i<100;i++) project.Lists["list"].Add(string.Format("t{0}",i)); // demo - видалити потім

var body = (Func<string,string, string>)project.Context["body"]; // функція підготовки json
var post = (Func<string,string,int,string>) project.Context["post"]; // функція відправки HTTP запиту POST

int timeout = 5000; // скільки мілісекунд очікувати відповідь, менше 5 секунд немає сенсу

string queryId = "id_123";
string url = "https://nghttp2.org/httpbin/post"; // на этом тестировал

var lines = project.Lists["list"].GetItems("ALL", true); // здесь список с контентом

var tasks = new List<System.Threading.Tasks.Task<bool>>(); // сюда подготовим список запросов на выполнение
foreach(string text in  lines) {
    string content = body(text, queryId);
    var arguments = Tuple.Create(post, url, content, timeout);   
    var task = new System.Threading.Tasks.Task<bool>(state => {
        var args = (Tuple<Func<string, string, int, string>, string, string, int>)state;
        string resp = args.Item1(args.Item2, args.Item3, args.Item4).Trim();
        string[] resps = resp.Split(new [] {"\r\n\r\n"}, StringSplitOptions.RemoveEmptyEntries);
        resp = resps[resps.Length-1].Substring(0, resp.IndexOf('\n')+1).ToLower().Replace("  ", " ");
        return (resp.IndexOf("200 ok") > -1); // повіряє на 200 ok, повертає true
    }, arguments);
    tasks.Add(task);
}
    
var sw = System.Diagnostics.Stopwatch.StartNew(); // запускаю вимірювання часу
//tasks.AsParallel().ForAll(x=>x.Start()); // тут запуск відбувається паралельно
tasks.ForEach(x=>x.Start()); // тут запуск задач відбувається послідовно

System.Threading.Tasks.Task.WaitAll(tasks.ToArray()); // очікуємо завершення всіх задач
int count = tasks.Count(item => item.Result); // підрахуємо результат

sw.Stop(); // завершую вимірювання часу
project.SendInfoToLog(string.Format("Stop: {0:c} ms", sw.Elapsed)); // показує скільки часу було затрачено на всі запити

project.SendInfoToLog(string.Format("True: {0}",count),true); // показує в лог скільки відповідей 200 ok

Я вдячний Олексію, який звернувся до мене з цим завданням – мені получилось зібрати свій конспект в простий і понятний код для виконання практичних завданнь, думаю і сам час від часу буду заходити в цю публікацію і копіювати його. А також за декілька десятків USDT, які він вирішив відправити мені за допомогу у вирішенні його проблеми.

Цікаво, яким чином можна ще швидше виконувати запити, щоб наприклад запустити не 10-20 запитів в секунду, а наприклад 60-500 запитів в секунду? Якщо є ідеї – радий побачити їх в коментарях.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *