Привет, коллеги!

Сегодня у нас на повестке дня выбор между двумя технологиями: Polling и Webhook. Почему почему именно Webhook является go-to решением для большинства проектов?

Помните, как в начале 2010-х все разрабы активно юзали Polling? Это был золотой стандарт для многих мессенджер-ботов. Но технологии не стоят на месте. Webhook занял свое место, предлагая свои решения.

Polling

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

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

Если бот обрабатывает состояния или сессии пользователей нужно учитывать, что при Polling состояние должно храниться на стороне сервера. Это означает необходимость использования БДшек и т.п

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

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

Если бот достигает этих лимитов, будут вылетать ошибки RetryAfter от API. Постоянные попытки повторной отправки сообщений, игнорируя ошибки API, могут привести к временной блокировке бота.

В python-telegram-bot (с версии 20), существует встроенный механизм BaseRateLimiter, который контролирует количество API запросов, совершаемых ботом за определенный временной интервал

Простейший скрипт Polling бота:

import time
import requests

TOKEN = "токен_бота"
URL = f"https://api.telegram.org/bot{TOKEN}/getUpdates"

def get_updates():
    response = requests.get(URL)
    return response.json()

def main():
    while True:
        updates = get_updates()
        if updates["result"]:
            # Обработка каждого нового сообщения
            for item in updates["result"]:
                print(f"Сообщение от пользователя: {item['message']['text']}")
        time.sleep(2)  # Задержка в 2 секунды между запросами

if __name__ == "__main__":
    main()

Бот каждые две секунды спрашивает у сервера Telegram, не пришло ли новых сообщений. В реальности тут конечно должна быть обработка ошибок, логирование и другие элементы

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

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

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

Webhook

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

В начале вам нужно настроить Webhook. Это делается путем отправки запроса к Telegram API с указанием URL вашего сервера. Телега будет использовать этот URL для отправки обновлений:

from telegram.ext import Updater, CommandHandler
from telegram import Bot
import logging

# логирование
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                    level=logging.INFO)
logger = logging.getLogger(__name__)

# функция для обработки команды /start
def start(update, context):
    update.message.reply_text('Привет!')

def main():
    TOKEN = 'token'
    bot = Bot(TOKEN)

    # Updater и передача токена вашего бота
    updater = Updater(token=TOKEN, use_context=True)

    # диспетчер для регистрации обработчиков
    dp = updater.dispatcher

    # регистрация команды /start
    dp.add_handler(CommandHandler("start", start))

    # настроим вебхук
    updater.start_webhook(listen='0.0.0.0',
                          port=8443,
                          url_path=TOKEN,
                          key='YOUR_PRIVATE.key',
                          cert='YOUR_PUBLIC.pem',
                          webhook_url='https://YOUR.SERVER.IP.ADDRESS:8443/' + TOKEN)

    updater.idle()

if __name__ == '__main__':
    main()

YOUR_PRIVATE.key и YOUR_PUBLIC.pem это путь к ключу SSL, и YOUR.SERVER.IP.ADDRESS адрес сервера.

Для Webhook необходим SSL-сертификат. Telegram требует, чтобы обмен данными был защищен. Телеграм поддерживает ограниченное количество портов для Webhook: 443, 80, 88, 8443.

Для настройки SSL-терминации на веб-сервере нужно установить и настроить Nginx или Apache. На Nginx:

server {
    listen 443 ssl;
    server_name YOUR.SERVER.IP.ADDRESS;

    ssl_certificate /path/to/YOUR_PUBLIC.pem;
    ssl_certificate_key /path/to/YOUR_PRIVATE.key;

    location / {
        proxy_pass http://localhost:8443;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Как только кто-то взаимодействует с ботом (например, отправляет ему сообщение), телеграм делает запрос на юрл вебхук с деталями этого события. Эти данные приходят в виде JSON-объекта, который содержат информацию, к примеру:

{
    "update_id": 123456789,
    "message": {
        "message_id": 111,
        "from": {
            "id": 222,
            "is_bot": false,
            "first_name": "viktor",
            "username": "viktor12313"
        },
        "chat": {
            "id": 333,
            "first_name": "viktor",
            "username": "viktor12313",
            "type": "private"
        },
        "date": 1609459200,
        "text": "ку"
    }
}

Сервер затем обрабатывает этот запрос, парсит полученный JSON и выполняет необходимые действия, например, отвечает на сообщение пользователя.

Следующий шаг - это создание обработчика для входящих сообщений. В коде это обычно реализуется через определение функций с декораторами, указывающими на типы обрабатываемых сообщений (например, текст, изображения и т.д.). В качестве примера, можно использовать библиотеку fastapi вместе с telebot. Создаётся экземпляр класса AsyncTeleBot, затем настраивается webhook и определяются функции для обработки команд (/start, /help) и обычных текстовых сообщений. При получении обновления, бот использует await для асинхронной обработки сообщения и отправляет ответ:

import fastapi
from telebot.async_telebot import AsyncTeleBot
import telebot

API_TOKEN = 'токен'
WEBHOOK_URL = 'вебхук'

bot = AsyncTeleBot(API_TOKEN)
app = fastapi.FastAPI()

@app.post('/')  
async def process_webhook(update: dict):  
    if update:  
        update = telebot.types.Update.de_json(update)  
        await bot.process_new_updates([update])  

@bot.message_handler(commands=['help', 'start'])  
async def send_welcome(message):  
    await bot.reply_to(message, "Бот запущен...")  

@bot.message_handler(func=lambda message: True, content_types=['text'])  
async def echo_message(message):  
    await bot.reply_to(message, message.text)  

Итого в таблице сравнение выглядит так:

Критерий

Polling

Webhook

Время отклика

Относительно медленное

Почти мгновенное

Нагрузка на сервер

Высокая, требует постоянных запросов

Низкая, сервер ждёт уведомлений

Сложность реализации

Простая, легко реализуемая

Средняя, требует настройки HTTPS, SSL

Масштабируемость

Ограничена, увеличение нагрузки на сервер с ростом числа пользователей

Высокая, эффективна при большом количестве пользователей

Использование ресурсов

Активное, постоянный опрос сервера

Пассивное,проще говоря принимай когда пришло

Управление трафиком

Неэффективное, из-за постоянных запросов

Эффективное, обрабатываются только актуальные события

Безопасность

Более уязвим для ддос атак из-за открытых запросов

Более безопасный, использует зашифрованные соединения

Настройка и обслуживание

Простая, не требует дополнительной настройки

Сложнее, требует настройки веб-сервера и SSL-сертификата


Webhook хоть и немного сложнее в настройке, имеет гораздо больше плюсов по сравнению с Polling.

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

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


  1. starik-2005
    17.01.2024 12:41
    +4

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


    1. Singrana
      17.01.2024 12:41
      -2

      Вариант - у вас есть сервер, на нем принимаются запросы, и проксируются через тоннель к вам, на рабочую машину


    1. Cregennan
      17.01.2024 12:41
      +2

      Есть несколько вариантов (для дебага бота):

      • Использовать ZeroTier/TailScale для пробития NAT

      • Временно создать туннель с помощью Ngrok или похожего софта

      Для продакшена же согласен, нужно будет брать белый IP


    1. mayorovp
      17.01.2024 12:41

      Если у вас совсем-совсем своего IP нет - то да, остаётся только long polling.

      Но если есть хотя бы динамический IP - вы уже можете получить доменное имя через dynamic dns и получить на него сертификат от Let's Encrypt


    1. iscareal
      17.01.2024 12:41

      Я локально, когда делал бота на webhook использовал ngrok. Сейчас же в vs code во вкладке Ports можно сделать себе url, который не будет меняться в проекте. По поводу сервера - cloudflare покатит, мне кажется, чтобы не мучиться с сертификатами


      1. SidVisceos
        17.01.2024 12:41

        В vs code можно арендовать сервер чтоли?


        1. mayorovp
          17.01.2024 12:41

          Сервер нельзя (наверное), но порт, как оказалось - можно.


        1. iscareal
          17.01.2024 12:41

          Можно прокинуть порт. Бесплатная альтернатива ngrok


    1. iRedds
      17.01.2024 12:41

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


  1. devlev
    17.01.2024 12:41
    +6

    Зачем отправлять запросы при Polling методе каждые 2 секунды, если сам по себе Polling метод подразумевает, что сервер как бы ждет некоторое время и держит соединение не отдавая его содержимое, пока данные не появятся?

    Это тогда не Polling получается а какой Getting.

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


    1. kesn
      17.01.2024 12:41

      del


  1. aamonster
    17.01.2024 12:41
    +7

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

    Э... А что, long poll ещё не изобрели?


  1. SidVisceos
    17.01.2024 12:41
    -4

    Тут и малограмотным должно быть понятно. Если у меня 1000 ботов на сервере - для лонгов нужно 1000 демонов поднимать и заставлять их жить. При хуках пофиг один бот или 1000.


  1. mrprogre
    17.01.2024 12:41

    А у моего бота такая схема: каждые 10 минут java приложение, которое запущено как сервис на моем сервере, сохраняет новости в БД на том же сервере. А пользователи через бота получают эти новости селектами к таблицам БД. В такой схеме есть смысл применять вебхук?


    1. Sau
      17.01.2024 12:41
      +1

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

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


      1. SidVisceos
        17.01.2024 12:41

        Не совсем понимаю как хуки могут что то замедлить.

        Пример:

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

        Может я Вас неправильно понял?


        1. Sau
          17.01.2024 12:41

          Одинокий хук, конечно, ничего не замедлит. Предположим раньше бот быстро отвечал пользователям. Но вот в новостях сказали что этот бот развод и все ломанулись проверять баланс вводя /wallet. Вот пользователь ввёл /wallet, ничего не произошло. Он ещё раз ввёл, ещё, чертыхнулся и пошёл жаловаться другим пользователям. Они тоже пошли вводить /wallet. Проходит пять минут, пользователь думает что может починили и снова пишет /wallet. И снова нет. В итоге сервер засыпан запросами. Сколько процессов сможет выдержать ваш сервер? А если и сможет выдержать всё, то у биржи может быть ограничение на число одновременных запросов.


          1. SidVisceos
            17.01.2024 12:41

            Разумно, логично. Спасибо.

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


            1. mayorovp
              17.01.2024 12:41

              Да без проблем, 2400 ботов - это всего лишь 2400 HTTP соединений с long polling. Держать столько соединений вообще не проблема пока памяти достаточно.


          1. SidVisceos
            17.01.2024 12:41

            Да. Не забывайте о возможности слать всякие sendMessage не после приёма хука, а в ответ на хук. То есть пришёл апдейт хуком и ты не отвечаешь кодом 200 и после шлёшь ответ, а прям вместе с кодом 200 отвечаешь на хук. Одно но. Один хук = один ответ (1 сообщение или правка 1 сообщения или 1 answerCallbackCuery,....).


  1. ValeryIvanov
    17.01.2024 12:41
    +8

    Сначала подумал, что автор статьи под polling имеет ввиду long polling, но нет, в статье описывается именно обычный polling и его недостатки, тогда как телеграм использует именно long polling.
    Отсюда делаю вывод, что автор не вникал в предмет статьи и/или вовсе написал статью используя нейросеть


  1. maxp
    17.01.2024 12:41

    Согласен с мнением предыдущего оратора.
    Увы, автор статьи не счёл нужным вникать в суть вопроса до того как рассказывать аудитории про "бутылочные горлышки" и т.п.

    Коротко по сути вопроса:
    в Телеграмме long polling (не просто polling!) сделан грамотно, удобен в использовании, надежен и бутылочным горлышком не является до тех по пока ваш инстанс обслужвает одного бота.
    Webhook предпочтительнее когда:
    - вебсервер уже есть для чего-то другого, а бот работает периодически на не частые запросы (в виде лямбды, например)
    - один инстанс приложения обслуживает много разных ботов.

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


  1. FisHlaBsoMAN
    17.01.2024 12:41
    +1

    Что это за ересь? Зачем динамически менять интервал? Ради чего? Зачем его ставить всего 2 секунды? Кто так делает вообще? Вебхуки это плохо переносимая технология и она требует внешнего адреса и настройки вебсервера в режиме проксирования или выделенно прослушивающего https порт инстанса бота. Вообще в лонгполе (да и везде) всё решается асинхронным подходом. aiohttp берешь и прикручиваешь, перебирая полученные события где то там в фоне, в ожидании новых ответов. Не успеваешь обрабатывать - делай очереди. У меня десяток ботов на самописном движке, дергающим getupdates и переезд на новый хостинг у меня занял пол часа от силы на добавление темплейта сисд юнита и распихивания папок с данными, создание венва с requirements.txt и включения этих юнитов..

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

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