Leaf.xNet - Библиотека для работы с запросами: Get, Post, Put, Path, Delete, Option.

RoyalBank

Client
Регистрация
07.09.2015
Сообщения
557
Реакции
555
Баллы
93
intro.jpg


Приветствую всех!

В статье рассмотрим примеры работы с библиотекой Leaf.xNet, являющейся форком xNet.

Из коробки доступны следующие методы:
  • GET
  • POST
  • PATCH
  • DELETE
  • PUT
  • OPTIONS

Подключаем библиотеку:
  1. Leaf.xNet.dll копируем в папку ExternalAssemblies
  2. Добавляем в GAC
  3. Прописываем в Директивы using и общий код:
C#:
Развернуть Свернуть Копировать
using Leaf.xNet; // dll
using HttpStatusCode = Leaf.xNet.HttpStatusCode; // debug request
using System.Net; // cookie


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

///++++++++++++++++++++++++++++++++++++++++++
/// Конструкция работы с запросом #1: using
///++++++++++++++++++++++++++++++++++++++++++


C#:
Развернуть Свернуть Копировать
using (var request = new HttpRequest())
{
    request.Get("https://zennolab.com");
}

Преимущества:
  • Короткая запись если необходимо сделать простой запрос;
  • Используя using, нам не нужно закрывать (dispose) запрос;
Также мы можем объявить запрос предварительно и затем передать его в using. В случае, если необходимо скачать файл используя cookie.

C#:
Развернуть Свернуть Копировать
HttpRequest request = null;

using (request)
{
    var resp = request.Get("http://google.com/file.zip");
    resp.ToFile("C:\\myDownloadedFile.zip");
}


///++++++++++++++++++++++++++++++++++++++++++
/// Конструкция работы с запросом #2: try, catch, finally
///++++++++++++++++++++++++++++++++++++++++++


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

C#:
Развернуть Свернуть Копировать
HttpRequest request = null;

try
{

}
catch (HttpException ex)
{
    project.SendErrorToLog(String.Format("HttpException: {0}", ex.Message)), true);

    switch (ex.Status)
    {
        case HttpExceptionStatus.Other:
            project.SendErrorToLog("Unknown error", true);
        break;

        case HttpExceptionStatus.ProtocolError:
            project.SendErrorToLog(String.Format("Status code: {0}", (int)ex.HttpStatusCode)), true);
        break;

        case HttpExceptionStatus.ConnectFailure:
            project.SendErrorToLog("Failed to connect to the HTTP-server.", true);
        break;

        case HttpExceptionStatus.SendFailure:
            project.SendErrorToLog("Failed to send request to HTTP-server.", true);
        break;

        case HttpExceptionStatus.ReceiveFailure:
            project.SendErrorToLog("Failed to load response from HTTP-server.", true);
        break;
    }
}
finally
{
    request.Dispose();
}

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


///++++++++++++++++++++++++++++++++++++++++++
/// Отладка запроса
///++++++++++++++++++++++++++++++++++++++++++


Помимо отладки ошибок запроса через конструкцию catch, мы можем также отладить ответ в конструкции try.

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

C#:
Развернуть Свернуть Копировать
HttpRequest request = null;

request.IgnoreProtocolErrors = true; // В этом случае блок catch не будет выходить по ошибке.

string _request = null;

try
{
    // Создадим цикл из пяти запросов.
    for (int i = 1; i <= 5; i++)
    {
        var responce = request.Get("https://zennolab.com");

        if (responce.StatusCode == HttpStatusCode.OK)
        {
            // HTTP status 200 - выходим из цикла, запрос вернул ответ
            _request = responce.ToString();
            break;
        }
        else if (responce.StatusCode == HttpStatusCode.NotFound)
        {
            // HTTP status 404
            throw new Exception("Сервер вернул 404 - Страница не найдена");
        }
        if (responce.StatusCode == HttpStatusCode.InternalServerError)
        {
            // HTTP status 500
            project.SendErrorToLog("Сервер вернул 500 - Сервер приболел, сделаем паузу", true);
            Thread.Sleep(2000);
        }
        else if (Responce.Address.AbsolutePath.Contains("/errors/500.html"))
        {
            // Можем проверять абсолютный путь конечной ссылки
            project.SendErrorToLog("Сервер вернул 500 - Сервер приболел, сделаем паузу", true);
            Thread.Sleep(2000);
        }
        else if (Responce.Address.Host.Contains("zennostore"))
        {
            // Можем проверять адрес хоста на случай переадресации.
            throw new Exception("Сервер перекинул на хост zennostore");
        }
    }
}
// С Подробной информацией по статус кодам можно ознакомитсья по ссылке:
// https://docs.microsoft.com/en-us/dotnet/api/system.net.httpstatuscode?redirectedfrom=MSDN&view=net-5.0


///++++++++++++++++++++++++++++++++++++++++++
/// Настройки для запроса
///++++++++++++++++++++++++++++++++++++++++++


C#:
Развернуть Свернуть Копировать
HttpRequest request = null;

request.Cookies = new CookieStorage();

request.UserAgent = Http.ChromeUserAgent(); // Создает UserAgent

/// UserAgents were updated in January 2019.
// ChromeUserAgent()
// FirefoxUserAgent()
// IEUserAgent()
// OperaUserAgent()
// OperaMiniUserAgent()

request.IgnoreProtocolErrors = true; // Блок catch не будет выходить по ошибке если 4xx или 5xx.
request.EnableEncodingContent = true;

request.KeepAlive = true;

request.AllowAutoRedirect = true;
request.MaximumAutomaticRedirections = 5;
request.ReconnectLimit = 3;
request.ReconnectDelay = 50;

request[HttpHeader.DNT] = "1";
request[HttpHeader.UpgradeInsecureRequests] = "1";

request["Host"] = String.Format("www.{0}", "zennolab.com");
request[HttpHeader.Accept] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
request[HttpHeader.AcceptLanguage] = "en-US,en;q=0.5";
request.AcceptEncoding = "gzip, deflate, br";


///++++++++++++++++++++++++++++++++++++++++++
/// Работа с GET запросами:
///++++++++++++++++++++++++++++++++++++++++++


C#:
Развернуть Свернуть Копировать
// Если не нужен ответ
request.Get("https://zennolab.com").None();


// Можем сделать запрос без возврата значения
request.Get("https://zennolab.com");


// Либо объявить var и работать с ним.
var httpResponse = request.Get("https://zennolab.com").ToString();

string html = httpResponse.ToString();
string headerToken = httpResponse["cf-request-id"];


// Скачиваем файл.
var resp = request.Get("http://google.com/file.zip");
resp.ToFile("C:\\myDownloadedFile.zip");


///++++++++++++++++++++++++++++++++++++++++++
/// Работа с POST запросами:
///++++++++++++++++++++++++++++++++++++++++++


C#:
Развернуть Свернуть Копировать
// Если ответ не нужен
request.Post("https://zennolab.com").None();

// Запрос без возврата значения в переменную
request.Post("https://zennolab.com");

// Объявляем var и работаем с ним.
var httpResponse = request.Post("https://zennolab.com");
string html = httpResponse.ToString();


//++++++++++++++++++++++++++++++++++++++++++
// Отправка запроса с параметрами


C#:
Развернуть Свернуть Копировать
var httpResponse = request.Post("https://zennolab.com", new StringContent("referrer=&queryString="));

// Либо объявить StringContent предварительно

var stringContent = new StringContent("referrer=&queryString=");
var httpResponse = request.Post("https://zennolab.com", stringContent);


//++++++++++++++++++++++++++++++++++++++++++
// Отправка мультипарт запроса


C#:
Развернуть Свернуть Копировать
var multipartContent = new MultipartContent()
{
    {new StringContent("Harry Potter"), "login"},
    {new StringContent("Crucio"), "password"},
    {new FileContent(@"C:\hp.rar"), "file1", "hp.rar"}
};

var httpResponse = request.Post("https://zennolab.com", multipartContent);


//++++++++++++++++++++++++++++++++++++++++++
// Отправка JSON в запросе


C#:
Развернуть Свернуть Копировать
var stringContent = new StringContent("{\"login\":\"shelest@gmail.com\",\"password\":\"admin123\"}");
var httpResponse = request.Post("https://zennolab.com", stringContent, "application/json");


//++++++++++++++++++++++++++++++++++++++++++
// RequestParams


C#:
Развернуть Свернуть Копировать
var urlParams = new RequestParams
{
    { ["id"] = "zY6vR6hU8kG0wE7u" },
    { ["имя"] = "Игорь" },
    { ["jsonContent"] = "{\"login\":\"shelest@gmail.com\",\"password\":\"admin123\"}" }
}
var httpResponse = request.Post("https://zennolab.com", urlParams);

// Либо через объявление

var urlParams = new RequestParams();
urlParams["id"] = "zY6vR6hU8kG0wE7u";
urlParams["имя"] = "Игорь";
urlParams["jsonContent"] = "{\"login\":\"shelest@gmail.com\",\"password\":\"admin123\"}";

var httpResponse = request.Post("https://zennolab.com", urlParams);


///++++++++++++++++++++++++++++++++++++++++++
/// Работа с Cookie
///++++++++++++++++++++++++++++++++++++++++++


Для работы с куки необходимо объявить CookieStorage.

C#:
Развернуть Свернуть Копировать
HttpRequest request = new HttpRequest();
request.Cookies = new CookieStorage();

C#:
Развернуть Свернуть Копировать
// Получение нужной куки

HttpRequest request = new HttpRequest();
request.Cookies = new CookieStorage();

string xf_session = null;

using (request)
{
    request.Get("https://zennolab.com");

    var cookies = request.Cookies.GetCookies("https://zennolab.com");
    foreach (Cookie cookie in cookies)
    {
        // Перебор всех кук в лог
        project.SendInfoToLog(String.Format("Name: {0} ::: Value: {1}", cookie.Name, cookie.Value), true);

        // Получаем в переменную куку xf_session
        if (cookie.Name == "xf_session") xf_session = cookie.Value;
    }
}

// Добавление новой или обновление существующей куки
// request.Cookies.Set(string name, string value, string domain, string path = "/");

request.Cookies.Set("pll_language", "en", "zennolab.com", "/");


//++++++++++++++++++++++++++++++++++++++++++
// Сохранение и загрузка кук


C#:
Развернуть Свернуть Копировать
//++++++++++++++++++++++++++++++++++++++++++
// Сохранение в файл

HttpRequest request = new HttpRequest();

// Объявляем новый контейнер, который сохраним в файл.
request.Cookies = new CookieStorage();

// Сохранение CookieStorage в файл .jar
request.Cookies.SaveToFile("D:\\cookie.jar", true);

C#:
Развернуть Свернуть Копировать
//++++++++++++++++++++++++++++++++++++++++++
// Загрузка из файла

HttpRequest request = new HttpRequest();

// Объявлять новый CookieStorage не нужно.
request.Cookies = CookieStorage.LoadFromFile("D:\\cookie.jar");

C#:
Развернуть Свернуть Копировать
//++++++++++++++++++++++++++++++++++++++++++
// Сохранение в массив байт

HttpRequest request = new HttpRequest();
request.Cookies = new CookieStorage();

// Сохранение CookieStorage в массив байт - byte[]
var byteArray = request.Cookies.ToBytes();

// Преобразовываем массив байт в строку - string.
string base64 = Convert.ToBase64String(byteArray);

C#:
Развернуть Свернуть Копировать
//++++++++++++++++++++++++++++++++++++++++++
// Загрузка из массива байт

string base64 = "";

HttpRequest request = new HttpRequest();

// Преобразование строки base64 в массив байт
byte[] decByte = Convert.FromBase64String(base64);

// Загрузка CookieStorage массива байт - byte[].
request.Cookies = CookieStorage.FromBytes(decByte);


//++++++++++++++++++++++++++++++++++++++++++
// Работа с Proxy
//++++++++++++++++++++++++++++++++++++++++++


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

Библиотека поддерживает работу со следующими протоколами:
  • HTTP
  • Socks4
  • Socks4A
  • Socks5
C#:
Развернуть Свернуть Копировать
// Вариант 1
request.Proxy = ProxyClient.Parse(ProxyType.HTTP, "ip:port:username:password");


// Вариант 2
request.Proxy = new HttpProxyClient("127.0.0.1", 8888, "username", "password");
request.Proxy = new Socks4ProxyClient("127.0.0.1", 8888, "username", "password");
request.Proxy = new Socks4aProxyClient("127.0.0.1", 8888, "username", "password");
request.Proxy = new Socks5ProxyClient("127.0.0.1", 8888, "username", "password");


// Вариант 3, в случае, когда авторизацию можно задать позже
request.Proxy = HttpProxyClient.Parse("127.0.0.1:8888");

request.Proxy.Username = "username";
request.Proxy.Password = "password";
 
Номер конкурса статей
  1. Четырнадцатый конкурс статей
Тема статьи
  1. Нестандартные хаки

Вложения

Последнее редактирование:
Спасибо за материал!
Статью ещё не прочитал, но оформление понравилось.
Да и давно мечтаю разобраться с этой библиотечкой!
 
  • Спасибо
Реакции: radv и RoyalBank
Спасибо, но я до такого ещё не дорос. Нужно как-то победить лень, разобраться со своими завалами и начинать учиться.
Добавил статью в закладки для подробного разбора
 
  • Спасибо
Реакции: fyodor44 и RoyalBank
То есть чтоб участвовать в конкурсе будет достаточно скопировать описание библиотеки и примерами из гитхаба?))
Не знал.
 
Полезная штука. :ay: Все собирался перейти с xNet на leaf.xnet да лень самому было копаться и искать информацию :az:
 
  • Спасибо
Реакции: RoyalBank
То есть чтоб участвовать в конкурсе будет достаточно скопировать описание библиотеки и примерами из гитхаба?))
Стоит отметить хорошее описание на самом гитхабе, но этого описания мало, чтобы быстро и просто внедрить библиотеку в реальный проект, а после этого не тратить время на отладку, разбираясь почему сыпятся те или иные ошибки.

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

В данной статье я постарался обобщить свой опыт работы с библиотекой сделав акцент на реальных потребностях, которые могут возникнуть при написании шаблона. И приведя в пример, те констукты, к которым я пришел за время использования библиотеки.
Вот это самое ценное и есть. А сухого описания на гитхабе не всегда хватает.
 
  • Спасибо
Реакции: Alexmd и RoyalBank
хорошо оформлено, все разложено по полочкам, лично для меня полезная информация, обязательно попробую воспользоваться данной библиотекой, проголосовал за статью, спасибо
 
  • Спасибо
Реакции: RoyalBank
У разработчика Leaf есть платная версия стоимостью 100$ с обходом Cloudflare и SSL Fingerprint и поддержкой recaptcha и hcaptcha.
За статью спасибо, но ожидал большего, как уже намекнули выше про описание с GitHub)
 
За статью спасибо, но ожидал большего, как уже намекнули выше про описание с GitHub)
Спасибо за обратную связь, в статье ориентировался на тех, кто до этого никогда не работал с этой библиотекой.
 
Супер, как раз на днях искал, и вот вуаля!
 
Последнее редактирование:
  • Спасибо
Реакции: RoyalBank
Стоит ли изучать xNet библ или начать сразу с этого?
 
Статью не читал, но одобряю :D. Жарю на Xnet, всё хочу уйти с нее на Leaf.xNet, да никак всё не могу собраться с силами да разобраться в чем там отличия. А тут опа и мануальчик вводный. На досуге ознакомлюсь. Заранее спасибо.
 
  • Спасибо
Реакции: RoyalBank
А как отправить контент типа
multipart/form-data; boundary=----WebKitFormBoundary12312312
чтобы там было что-то типа
----WebKitFormBoundary12312312
{"login": "porosenok"}

----WebKitFormBoundary12312312
{"pass": "123456"}

и тд

на гите нашел рекомендацию смотреть .Raw () но что-то посмотрел и ничего не понял
 
multipart/form-data; boundary=----WebKitFormBoundary12312312

Если я правильно понял, то WebKitFormBoundary12312312 - это заголовок ContentType, его можно добавить двумя способами, как постоянный для всех запросов - вариант 1, или как временный, только для текущего запроса - вариант 2.


C#:
Развернуть Свернуть Копировать
HttpRequest request = new HttpRequest();

// Вариант 1 - Постоянный заголовок
// request[HttpHeader.ContentType] = "multipart/form-data; boundary=----WebKitFormBoundary12312312";

// Вариант 2 - Временный заголовок, только для текущего запроса.
request.AddHeader("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundary12312312");


var multipartContent = new MultipartContent()
{
    {new StringContent("login"), "porosenok"},
    {new StringContent("pass"), "123456"}
};

var httpResponse = request.Post("https://zennolab.com", multipartContent);
 
Если я правильно понял, то WebKitFormBoundary12312312 - это заголовок ContentType, его можно добавить двумя способами, как постоянный для всех запросов - вариант 1, или как временный, только для текущего запроса - вариант 2.


C#:
Развернуть Свернуть Копировать
HttpRequest request = new HttpRequest();

// Вариант 1 - Постоянный заголовок
// request[HttpHeader.ContentType] = "multipart/form-data; boundary=----WebKitFormBoundary12312312";

// Вариант 2 - Временный заголовок, только для текущего запроса.
request.AddHeader("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundary12312312");


var multipartContent = new MultipartContent()
{
    {new StringContent("login"), "porosenok"},
    {new StringContent("pass"), "123456"}
};

var httpResponse = request.Post("https://zennolab.com", multipartContent);
Спасибо, но увы не срабатывает запрос таким образом. В Фиддлере смотрю там в разделе webforms оно должно быть справа название, слева значение, а там все слева кашей идет
 
Спасибо, но увы не срабатывает запрос таким образом. В Фиддлере смотрю там в разделе webforms оно должно быть справа название, слева значение, а там все слева кашей идет
Код выше, как пример. Чтобы запрос работал необходимо смотреть, всю цепочку запросов в фиддлере и переносить все составляющие от кук до заголовков в код.
 
Код выше, как пример. Чтобы запрос работал необходимо смотреть, всю цепочку запросов в фиддлере и переносить все составляющие от кук до заголовков в код.
В том то и дело у меня есть один этот запрос в postman на ура выполняется в таком виде в каком есть. Все куки там уже включены (я их и в код сишарпа добавил естественно). Увы, перенести из-за кривизны этой не получается ((

Еще WebKitFormBoundary слово убралось и свое значение рандом поставилось. Попробовать content-type таким образом задать, но вообще код не стартует:

C#:
Развернуть Свернуть Копировать
var httpResponse = request.Post("https://site.ru", multipartContent, "multipart/form-data; boundary=----WebKitFormBoundary123123123");


ps и сервер мне в ответ отдает то что не хватает некоторых данных, то есть значит то как эти данные передались неверно, сервер их не видит
 
В том то и дело у меня есть один этот запрос в postman на ура выполняется в таком виде в каком есть
Как вариант составить такую же строку, как при отправке через StringBuilder и отправлять её.


C#:
Развернуть Свернуть Копировать
StringBuilder sb = new StringBuilder();

sb.Append("----WebKitFormBoundary12312312").Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"login\"").Append("\r\n");
sb.Append("login").Append("\r\n");
sb.Append("----WebKitFormBoundary12312312").Append("\r\n");
sb.Append("Content-Disposition: form-data; name=\"password\"").Append("\r\n");
sb.Append("password").Append("\r\n");

var content = sb.ToString();


var httpResponse = request.Post("https://site.ru", content, "multipart/form-data; boundary=----WebKitFormBoundary123123123");
 
  • Спасибо
Реакции: semafor
В начале статьи стоило бы написать, что такое ентот лификснет и чем он может быть полезен и/или лучше чего-то там более известного... Совсем для ламеров, вроде меня...
 
А чем работа с помощью этой библиотеки отличается от работы на кубиках? в чем ее преимущество (кроме того, что можно впихнуть код в большущий код 1 кубика С#)?
 
В начале статьи стоило бы написать, что такое ентот лификснет и чем он может быть полезен и/или лучше чего-то там более известного... Совсем для ламеров, вроде меня...
Библиотека нужна тогда, когда с помощью стандартных средств не получается решить какую-то проблему, например отправляете запрос, а Зенно искажает параметры - из-за чего запрос не доходит. Если же у Вас получается реализовать стандартными методами - то стоит отложить чтение статьи до момента, когда эта информация станет для Вас актуальной.

А чем работа с помощью этой библиотеки отличается от работы на кубиках? в чем ее преимущество (кроме того, что можно впихнуть код в большущий код 1 кубика С#)?
Отличается тем, что бывает что стандартными средствами Зенно не получается выполнить определенные задачи, а библиотека позволяет более гибко работать как с самими соединениями, так и с формированием параметров. Но, как уже писал выше - не стоит заморачиваться до тех пор, пока у Вас в реальности нет проблемы при работе со стандартными методами.
 
Библиотека нужна тогда, когда с помощью стандартных средств не получается решить какую-то проблему, например отправляете запрос, а Зенно искажает параметры - из-за чего запрос не доходит. Если же у Вас получается реализовать стандартными методами - то стоит отложить чтение статьи до момента, когда эта информация станет для Вас актуальной.


Отличается тем, что бывает что стандартными средствами Зенно не получается выполнить определенные задачи, а библиотека позволяет более гибко работать как с самими соединениями, так и с формированием параметров. Но, как уже писал выше - не стоит заморачиваться до тех пор, пока у Вас в реальности нет проблемы при работе со стандартными методами.
Просто сейчас добавили кубики с Put, Delete и т.д. а с параметрами мне не доводилось мучаться)
 
Просто сейчас добавили кубики с Put, Delete и т.д. а с параметрами мне не доводилось мучаться)
Мне уже приходилось встречать ситуации, когда нужно отправить запрос в определенном виде, формировал запрос в этом виде, и в результате оказывалось что Зенно искажала параметры. И никак невозможно было понять что же именно не так. А после использования сборки параметров от данной библиотечки всё заработало.

Также была как-то проблема с запросами - запросы открывали соединения для каждого запроса - что быстро забивало количество открытых соединений и bing выдавал 429 - и это также решалось методом принудительного удержания открытых соединений этой библиотеки.

Такая же шляпа была когда покупались прокси, которые продаются с ограничением количества потоков - при удержании открытым соединения при принудительном закрытии соединений оказалось что нагрузка на прокси уменьшилась в 10 раз (а это значит что можно было в 10 раз больше потоков запускать).

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

В целом, выше описали моменты, которые позволяет закрыть библиотека. Так же стороннее решение, позволит избежать проблем с обновлением функционала из коробки и его работоспособностью в предыдущих версиях.
Позволяет экономить код в объеме, т.к. не нужно прописывать все по умолчанию. Позволяет сделать грамотную отладку запросов с кодами ответов страницы и другими критериями, не распаршивая регулярками ответ запроса. Ну и скачать файл, можно в две строки.
 
  • Спасибо
Реакции: Asmus003 и Meteorburn
Прикольно, не знал, что на самом деле всё очень даже не сложно тут.
 
  • Спасибо
Реакции: RoyalBank
У меня были проекты на xNet и я был вполне доволен этой библиотекой. В статье прекрасно представлены возможности более крутой модификации Leaf.xNet. Спасибо за ценную информацию и примеры использования. Буду пробовать!
 
  • Спасибо
Реакции: RoyalBank

Кто просматривает тему: (Всего: 0, Пользователи: 0, Гости: 0)