Как применять Linux epoll в Python

http://scotdoyle.com/python-epoll-howto.html
  • Перевод
  • Tutorial
В статье описывается:
  • Примеры использования блокирующих сокетов
  • Преимущества асинхронных сокетов и Linux epoll
  • Примеры асинхронного использования сокетов через epoll
  • Вопросы производительности
  • Исходный код

Введение


С версии 2.6 Python включает API для работы с Linux библиотекой epoll. Данная статья кратко демонстрирует данное API примерами кода на Python 3.

От переводчика.
Я старался не злоупотреблять англоязычными терминами насколько это возможно. Так что «register/unregister» стали «подпиской/отпиской», «print to the console» — «выводом на консоль». «Production server» решил перевести как «нагруженный сервер», так как ничего лучше, чем «сервер на продакшене» в голову не приходит. «Thread» перевел как «поток», а не «нить».
Названия событий, режимов и флагов решил вообще не переводить, давая лишь однократный пример возможного перевода.
Хоть и написано, что код для Python 3, все прекрасно работает и на Python 2.6.


Примеры использования блокирующих сокетов


Первый пример это простой Python 3.0 сервер, который слушает порт 8080 на предмет входящих HTTP запросов, выводит их на консоль, и отправляет ответное HTTP сообщение клиенту.
  • Строка 9: Создание серверного сокета.
  • Строка 10: Разрешаем выполнять bind() в строке 11 даже в случае, если другая программа недавно слушала тот же порт. Без этого, программа не сможет работать с портом в течение 1-2 минут после окончания работы с тем же портом в ранее запущенной программе.
  • Строка 11: Вешаем (bind'им) серверный сокет на порт 8080 для всех доступных IPv4 адресов данной машины.
  • Строка 12: Указываем серверному сокету начать прием входящих соединений от клиентов.
  • Строка 14: Программа будет останавливаться в этой точке до получения входящего соединения. Когда это произойдет, серверный сокет создаст новый сокет, который будет использоваться на данной машине для связи с клиентом. Этот новый сокет представлен объектом clientconnection, который возвращается вызовом accept(). Объект address содержит IP адрес и номер порта удаленной машины.
  • Строки 15-17: Формируем данные, которые будут отправлены клиенту для завершения HTTP запроса. HTTP протокол описан тут.
  • Строка 18: Выводим запрос в консоль в качестве проверки правильности действия.
  • Строка 19: Отсылаем ответ клиенту.
  • Строки 20-22: Закрываем соединение с клиентом так же как и слушающий серверный сокет.

Официальный HOWTO содержит более детальное описание программирования сокетов в Python.

Пример 1
Copy Source | Copy HTML
  1. import socket
  2.  
  3. EOL1 = b'\n\n'
  4. EOL2 = b'\n\r\n'
  5. response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
  6. response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
  7. response += b'Hello, world!'
  8.  
  9. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. serversocket.bind(('0.0.0.0', 8080))
  12. serversocket.listen(1)
  13.  
  14. connectiontoclient, address = serversocket.accept()
  15. request = b''
  16. while EOL1 not in request and EOL2 not in request:
  17.     request += connectiontoclient.recv(1024)
  18. print(request.decode())
  19. connectiontoclient.send(response)
  20. connectiontoclient.close()
  21.  
  22. serversocket.close()

Пример 2 добавляет цикл в 15 строке для повторной обработки клиентских подключений, выполняемой до пользовательского прерывания (например с клавиатуры). Это яснее показывает, что серверный сокет никогда не используется для обмена данными с клиентом. Скорее, он лишь принимает соединение от клиента и создает новый сокет, который уже и используется для связи.
Блок finally в строках 23-24 нужен для того, чтобы слушающий серверный сокет закрывался в любом случае, даже при возникновении ошибок.

Пример 2
Copy Source | Copy HTML
  1. import socket
  2.  
  3. EOL1 = b'\n\n'
  4. EOL2 = b'\n\r\n'
  5. response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
  6. response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
  7. response += b'Hello, world!'
  8.  
  9. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. serversocket.bind(('0.0.0.0', 8080))
  12. serversocket.listen(1)
  13.  
  14. try:
  15.     while True:
  16.         connectiontoclient, address = serversocket.accept()
  17.         request = b''
  18.         while EOL1 not in request and EOL2 not in request:
  19.             request += connectiontoclient.recv(1024)
  20.         print('-'*40 + '\n' + request.decode()[:-2])
  21.         connectiontoclient.send(response)
  22.         connectiontoclient.close()
  23. finally:
  24.     serversocket.close()


Преимущества асинхронных сокетов и Linux epoll


Сокеты, показанные в примере 2 называются блокирующими сокетами, потому что программа на Python приостанавливает свое выполнение до прихода события. Вызов accept() в строке 16 блокируется до получения соединения от клиента. Вызов recv() в строке 19 блокируется до получения данных от клиента (или пока не будет данных для приема). Вызов send() в строке 21 блокируется до того, как все данные, отправляемые клиенту, не будут добавлены в очередь отправки Linux.

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

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

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

Linux 2.6 имеет ряд механизмов для управления асинхронными сокетами, три из которых представлены в Python API через select, poll и epoll. epoll и poll лучше чем select, потому что программе на Python не нужно следить за всеми интересующими событиями в сокете. Вместо этого можно положиться на операционную систему сообщать о том, какие события возникли на каких сокетах. А epoll в свою очередь лучше poll, потому что он не требует от операционной системы проверки всех сокетов на интересующие события каждый раз, когда это запрашивается Python программой. Скорее, при запросе от Python, Linux проверяет, произошли ли эти события, и возвращает список событий. Итак, epoll более эффективный и масштабируемый механизм для большого числа (тысяч) одновременных соединений, как показано на этих графиках.

Примеры асинхронного использования сокетов через epoll


Программы, использующие epoll, часто работают по следующему принципу:
  1. Создается epoll объект
  2. epoll объекту указывается наблюдать за определенными событиями на определенных сокетах
  3. У epoll объекта запрашивается на каких сокетах произошли указанные события с момента предыдущего опроса
  4. Выполняются некоторые действия на этих сокетах
  5. epoll объекту указывается изменить список сокетов и/или наблюдаемых событий
  6. Повторяются шаги с 3 по 5 до завершения
  7. Уничтожается epoll объект

Пример 3 повторяет функционал примера 2, использующего асинхронные сокеты. Программа сложнее, потому что один поток поочередно взаимодействует со множеством клиентов.
  • Строка 1: Модуль select содержит функционал epoll.
  • Строка 13: Блокирующие по умолчанию сокеты нужно использовать в неблокирующем (асинхронном) режиме.
  • Строка 15: Создание epoll объекта.
  • Строка 16: Подписываемся на события чтения на серверном сокете. Событие чтения происходит в тот момент, когда серверный сокет принимает подключение.
  • Строка 19: Словарь соединений отображает файловые дескрипторы (целые числа) в соответствующие им объекты сетевых соединений.
  • Строка 21: Запрос к epoll объекту для выяснения, произошли ли какие-либо из ожидаемых событий. Параметр «1» указывает, что мы готовы ждать события до 1 секунды. Если любые из интересующих событий произойдут раньше, то запрос сразу вернет список этих событий.
  • Строка 22: События возвращаются последовательностью кортежей (fileno, event code). fileno это синоним файлового дескриптора и всегда является целым числом.
  • Строка 23: Если на серверном сокете произошло событие чтения, то можно создавать новый клиентский сокет.
  • Строка 25: Устанавливаем новый сокет в неблокирующий режим.
  • Строка 26: Подписываемся на события чтения (EPOLLIN) на новом сокете.
  • Строка 31: Если на клиентском сокете произошло событие чтения, то читаем новые данные, пришедшие от клиента.
  • Строка 33: После получения запроса отписываемся от событий чтения и подписываемся на события записи (EPOLLOUT). Эти события происходят, когда можно отправить данные ответа клиенту.
  • Строка 34: Печатаем запрос, показывая, что несмотря на переключения между клиентами, данные можно собрать воедино и обработать как единое сообщение.
  • Строка 35: Если на клиентском сокете произошло событие записи, то можно попробовать отправить новые данные клиенту.
  • Строки 36-38: Отправка данных ответа порцией за раз, пока весь ответ не будет передан операционной системе для отправки.
  • Строка 39: После полной отправки ответа отписываемся от дальнейших событий чтения или записи.
  • Строка 40: Вызов shutdown сокету не обязателен для явного закрытия соединения. Данный пример использует его, чтобы заставить клиента завершить связь первым. Вызов shutdown сообщает клиенту, что больше не будет отправлено или получено данных и что ему стоит по хорошему закрыть сокет со своей стороны.
  • Строка 41: Событие HUP (hang-up, зависание) сообщает, что клиентский сокет отключился (был закрыт), то есть следует его закрыть. Нет необходимости подписываться на события HUP. Они всегда происходят на сокетах, которые подписаны в epoll объекте.
  • Строка 42: Отписываемся от событий в данном сокете.
  • Строка 43: Закрываем сокет.
  • Строки 18-45: Блок try-catch используется в этом примере потому, что программа может быть прервана с клавиатуры.
  • Строки 46-48: Открытые сокеты не нужно закрывать, потому что Python закрывает их при завершении работы программы. Однако явное закрытие — это хорошая практика.

Пример 3
Copy Source | Copy HTML
  1. import socket, select
  2.  
  3. EOL1 = b'\n\n'
  4. EOL2 = b'\n\r\n'
  5. response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
  6. response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
  7. response += b'Hello, world!'
  8.  
  9. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. serversocket.bind(('0.0.0.0', 8080))
  12. serversocket.listen(1)
  13. serversocket.setblocking( 0)
  14.  
  15. epoll = select.epoll()
  16. epoll.register(serversocket.fileno(), select.EPOLLIN)
  17.  
  18. try:
  19.     connections = {}; requests = {}; responses = {}
  20.     while True:
  21.         events = epoll.poll(1)
  22.         for fileno, event in events:
  23.             if fileno == serversocket.fileno():
  24.                 connection, address = serversocket.accept()
  25.                 connection.setblocking( 0)
  26.                 epoll.register(connection.fileno(), select.EPOLLIN)
  27.                 connections[connection.fileno()] = connection
  28.                 requests[connection.fileno()] = b''
  29.                 responses[connection.fileno()] = response
  30.             elif event & select.EPOLLIN:
  31.                 requests[fileno] += connections[fileno].recv(1024)
  32.                 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
  33.                     epoll.modify(fileno, select.EPOLLOUT)
  34.                     print('-'*40 + '\n' + requests[fileno].decode()[:-2])
  35.             elif event & select.EPOLLOUT:
  36.                 byteswritten = connections[fileno].send(responses[fileno])
  37.                 responses[fileno] = responses[fileno][byteswritten:]
  38.                 if len(responses[fileno]) ==  0:
  39.                     epoll.modify(fileno,  0)
  40.                     connections[fileno].shutdown(socket.SHUT_RDWR)
  41.             elif event & select.EPOLLHUP:
  42.                 epoll.unregister(fileno)
  43.                 connections[fileno].close()
  44.                 del connections[fileno]
  45. finally:
  46.     epoll.unregister(serversocket.fileno())
  47.     epoll.close()
  48.     serversocket.close()

epoll имеет два режима работы, называемые инициируемый фронтом (edge-triggered) и инициируемый уровнем (level-triggered). В режиме edge-triggered вызов epoll.poll() вернет событие только после того, как события чтения или записи произойдут на сокете. Вызвавшая программа должна обработать все данные, связанные с этим событием, без повторных вызовов epoll.poll(). Когда данные от определенного события исчерпываются, дополнительные попытки работы с сокетом будут приводить к исключениям. Наоборот, в режиме level-triggered, повторные вызовы epoll.poll() будут давать повторные уведомления об интересующих событиях, пока не будут обработаны все данные, связанные с событиями. Никаких исключений не возникает при нормальной работе в режиме level-triggered.

Для примера предположим, что серверный сокет был подписан в epoll объекте на события чтения. В режиме edge-triggered программе следует вызывать accept() для приема новых соединений пока не произойдет исключение socket.error. В режиме level-triggered может быть сделан единственный вызов accept(), а затем epoll объект может быть запрошен снова для следующих событий в очереди.

Пример 3 использует режим level-triggered, который является режимом по умолчанию. Пример 4 демонстрирует как использовать режим edge-triggered. В строках 25, 36 и 45 вводятся циклы, которые работаю пока не возникнет исключение (или станет известно, что все данные обработаны). Строки 32, 38 и 48 ловят исключения. Наконец, строки 16, 28, 41 и 51 добавляют маску EPOLLET, которая задает режим edge-triggered.

Пример 4
Copy Source | Copy HTML
  1. import socket, select
  2.  
  3. EOL1 = b'\n\n'
  4. EOL2 = b'\n\r\n'
  5. response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
  6. response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
  7. response += b'Hello, world!'
  8.  
  9. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. serversocket.bind(('0.0.0.0', 8080))
  12. serversocket.listen(1)
  13. serversocket.setblocking( 0)
  14.  
  15. epoll = select.epoll()
  16. epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)
  17.  
  18. try:
  19.     connections = {}; requests = {}; responses = {}
  20.     while True:
  21.         events = epoll.poll(1)
  22.         for fileno, event in events:
  23.             if fileno == serversocket.fileno():
  24.                 try:
  25.                     while True:
  26.                         connection, address = serversocket.accept()
  27.                         connection.setblocking( 0)
  28.                         epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)
  29.                         connections[connection.fileno()] = connection
  30.                         requests[connection.fileno()] = b''
  31.                         responses[connection.fileno()] = response
  32.                 except socket.error:
  33.                     pass
  34.             elif event & select.EPOLLIN:
  35.                 try:
  36.                     while True:
  37.                         requests[fileno] += connections[fileno].recv(1024)
  38.                 except socket.error:
  39.                     pass
  40.                 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
  41.                     epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)
  42.                     print('-'*40 + '\n' + requests[fileno].decode()[:-2])
  43.             elif event & select.EPOLLOUT:
  44.                 try:
  45.                     while len(responses[fileno]) >  0:
  46.                         byteswritten = connections[fileno].send(responses[fileno])
  47.                         responses[fileno] = responses[fileno][byteswritten:]
  48.                 except socket.error:
  49.                     pass
  50.                 if len(responses[fileno]) ==  0:
  51.                     epoll.modify(fileno, select.EPOLLET)
  52.                     connections[fileno].shutdown(socket.SHUT_RDWR)
  53.             elif event & select.EPOLLHUP:
  54.                 epoll.unregister(fileno)
  55.                 connections[fileno].close()
  56.                 del connections[fileno]
  57. finally:
  58.     epoll.unregister(serversocket.fileno())
  59.     epoll.close()
  60.     serversocket.close()

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

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

Вопросы производительности


Длина очереди подключений к серверу

В 12ой строке всех примеров показан вызов метода serversocket.listen(). Параметром для этого метода является длина очереди подключений к серверу (listen backlog). Он сообщает операционной системе о максимальном принимаемом числе TCP/IP подключений, которые могут быть размещены в системной очереди до того, как их примет Python программа. Каждый раз, когда Python программа вызывает accept() на серверном сокете, одно из подключений удаляется из очереди и освободившееся место может быть использовано для другого входящего соединения. При заполненной очереди, новые входящие подключения молча игнорируются, что приводит к ненужным задержкам на клиентской стороне. Нагруженный сервер обычно обрабатывает сотни и тысячи одновременных подключений, так что значение 1 будет неадекватным. В качестве примера, при использовании ab для нагрузочного тестирования вышеприведенных примеров с сотней одновременных HTTP 1.0 клиентов, длина очереди менее 50 подчас может привести к сильному падению производительности.

Настройки TCP

Опция TCP_CORK может блокировать (bottle up) отправку данных пока они не будут готовы. Эта опция, проиллюстрированная в строках 34 и 40 примера 5, может быть полезна для HTTP сервера, использующего конвейер HTTP/1.1.

Пример 5
Copy Source | Copy HTML
  1. import socket, select
  2.  
  3. EOL1 = b'\n\n'
  4. EOL2 = b'\n\r\n'
  5. response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
  6. response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
  7. response += b'Hello, world!'
  8.  
  9. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. serversocket.bind(('0.0.0.0', 8080))
  12. serversocket.listen(1)
  13. serversocket.setblocking( 0)
  14.  
  15. epoll = select.epoll()
  16. epoll.register(serversocket.fileno(), select.EPOLLIN)
  17.  
  18. try:
  19.     connections = {}; requests = {}; responses = {}
  20.     while True:
  21.         events = epoll.poll(1)
  22.         for fileno, event in events:
  23.             if fileno == serversocket.fileno():
  24.                 connection, address = serversocket.accept()
  25.                 connection.setblocking( 0)
  26.                 epoll.register(connection.fileno(), select.EPOLLIN)
  27.                 connections[connection.fileno()] = connection
  28.                 requests[connection.fileno()] = b''
  29.                 responses[connection.fileno()] = response
  30.             elif event & select.EPOLLIN:
  31.                 requests[fileno] += connections[fileno].recv(1024)
  32.                 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
  33.                     epoll.modify(fileno, select.EPOLLOUT)
  34.                     connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)
  35.                     print('-'*40 + '\n' + requests[fileno].decode()[:-2])
  36.             elif event & select.EPOLLOUT:
  37.                 byteswritten = connections[fileno].send(responses[fileno])
  38.                 responses[fileno] = responses[fileno][byteswritten:]
  39.                 if len(responses[fileno]) ==  0:
  40.                     connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK,  0)
  41.                     epoll.modify(fileno,  0)
  42.                     connections[fileno].shutdown(socket.SHUT_RDWR)
  43.             elif event & select.EPOLLHUP:
  44.                 epoll.unregister(fileno)
  45.                 connections[fileno].close()
  46.                 del connections[fileno]
  47. finally:
  48.     epoll.unregister(serversocket.fileno())
  49.     epoll.close()
  50.     serversocket.close()

С другой стороны, опция TCP_NODELAY сообщает системе, что любые данные, переданные в socket.send(), следует сразу же отправить клиенту без буферизации операционной системой. Эта опция, проиллюстрированная в строке 14 примера 6, может быть полезна для SSH клиентов и других приложений «реального времени».

Пример 6
Copy Source | Copy HTML
  1. import socket, select
  2.  
  3. EOL1 = b'\n\n'
  4. EOL2 = b'\n\r\n'
  5. response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
  6. response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
  7. response += b'Hello, world!'
  8.  
  9. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  10. serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  11. serversocket.bind(('0.0.0.0', 8080))
  12. serversocket.listen(1)
  13. serversocket.setblocking( 0)
  14. serversocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  15.  
  16. epoll = select.epoll()
  17. epoll.register(serversocket.fileno(), select.EPOLLIN)
  18.  
  19. try:
  20.     connections = {}; requests = {}; responses = {}
  21.     while True:
  22.         events = epoll.poll(1)
  23.         for fileno, event in events:
  24.             if fileno == serversocket.fileno():
  25.                 connection, address = serversocket.accept()
  26.                 connection.setblocking( 0)
  27.                 epoll.register(connection.fileno(), select.EPOLLIN)
  28.                 connections[connection.fileno()] = connection
  29.                 requests[connection.fileno()] = b''
  30.                 responses[connection.fileno()] = response
  31.             elif event & select.EPOLLIN:
  32.                 requests[fileno] += connections[fileno].recv(1024)
  33.                 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
  34.                     epoll.modify(fileno, select.EPOLLOUT)
  35.                     print('-'*40 + '\n' + requests[fileno].decode()[:-2])
  36.             elif event & select.EPOLLOUT:
  37.                 byteswritten = connections[fileno].send(responses[fileno])
  38.                 responses[fileno] = responses[fileno][byteswritten:]
  39.                 if len(responses[fileno]) ==  0:
  40.                     epoll.modify(fileno,  0)
  41.                     connections[fileno].shutdown(socket.SHUT_RDWR)
  42.             elif event & select.EPOLLHUP:
  43.                 epoll.unregister(fileno)
  44.                 connections[fileno].close()
  45.                 del connections[fileno]
  46. finally:
  47.     epoll.unregister(serversocket.fileno())
  48.     epoll.close()
  49.     serversocket.close()

Исходный код


Примеры на этой странице общедоступны и их можно скачать тут.

От переводчика


При закрытии сокета удаленным клиентом на локальный сокет приходит событие EPOLLIN, но при чтении recv не будет получено ничего. Так что момент
Copy Source | Copy HTML
elif event & select.EPOLLIN:
    try:
        while True:
            requests[fileno] += connections[fileno].recv(1024)

можно написать так:
Copy Source | Copy HTML
elif event & select.EPOLLIN:
    try:
        while True:
            data = connections[fileno].recv(1024)
            if not data:
                epoll.modify(fileno, select.EPOLLET)
                connections[fileno].shutdown(socket.SHUT_RDWR)
            else:
                requests[fileno] += data

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

Подробнее
Реклама
Комментарии 30
  • +3
    Спасибо, статья очень помогла!
    • +3
      Production server я бы перевёл как «рабочий сервер», в отличии от «сервера разработки» и «сервера сборки».
      • +1
        можно просто «боевой сервер», думаю многим будет понятно
      • +1
        Существуют чудесные фреймворки для разработки асинхронных приложений, такие как twisted — они позволяют абстрагироваться от деталей select/poll/epoll/что-там-еще-придумали-на-данной-платформе.
        • +4
          Где в статье написано, что это начало «убийцы» Twisted/Tornado/Diesel/etc?
          Это описание того, как работает epoll в Linux.

          А абстрагироваться от деталей позволяет libev/libevent.
        • +1
          > разрыв происходит не сразу же, а после нескольких последовательных таких холостых срабатываний, чтобы исключить возможность ошибочного определения.

          Немного не понял Вашу мысль. Если epoll возвращает EPOLLIN, значит на сокете гарантированно есть события. Если данных нет, а событие есть — значит клиент закрыл соединение. Зачем тут делать лишние системные вызовы, это ведь, как-никак, лишние переключения контекста и трата ресурсов?
          • +1
            В моем примере и нет лишних вызовов. Речь же шла о том, что встречаются примеры подобного кода с несколькими проверками (2-3). Я решил, что стоит об этом написать.
            • +1
              А Вы не знаете, зачем авторы этих примеров делают несколько проверок, на какие ошибки рассчитывают?
          • +7
            В примерах данной статьи нет смысла использовать epoll вообще, так как мы сами блокируем сокет в строке
            responses[connection.fileno()] = response
            Мы не выходим из этой строки до тех пор пока не запишем в нее наш response. Те пока сервер пишет одному клиенту, он не обсуживает других. Убедится в этом можно записал в response строку на пару мегабайт, запустить сервер где нибудь не на локалхосте что бы скорость была не высока, а затем открыть две вкладки — сервер будет отвечеть только в одну из них, и только завершив, начнет во вторую. Те сокеты то ты опрашивем без форков, но конкурентой обработки в коде нет.
            • +5
              Все даже хуже оказалось, такак сокет не может быть готовым отправлять данные все время, то даже строку в 100кб пример передать не может, соединение рвется в призвольные моменты, ну и заблокированым остается как я и описал выше.
              • +5
                Проще всего убедится заменив
                response += b'Hello, world!'
                на
                response += b'H!' * 2000
                • +1
                  Это перевод, он дает представление о работе с epoll в целом.
                  Практический подход конкретно на python'е и на конкретных задачах я еще опишу в скором времени уже от себя.
              • 0
                Что-то не то вы говорите по-моему responses[connection.fileno()] = response — тут вроде вообще работы с сокетом не наблюдается.
                Сейчас потестирую…
                • 0
                  response_body = b'Hello, world!'*100000
                  
                  response  = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
                  response += b'Content-Type: text/plain\r\nContent-Length: '+str(len(response_body))+b'\r\n\r\n'
                  response += response_body

                  И длину очереди повыше постваил serversocket.listen(50). Результат:
                  $ ab -n 5000 -c 200 http://localhost:8081/
                  This is ApacheBench, Version 2.3 <$Revision: 655654 $>
                  Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
                  Licensed to The Apache Software Foundation, http://www.apache.org/
                  
                  Benchmarking localhost (be patient)
                  Completed 500 requests
                  Completed 1000 requests
                  Completed 1500 requests
                  Completed 2000 requests
                  Completed 2500 requests
                  Completed 3000 requests
                  Completed 3500 requests
                  Completed 4000 requests
                  Completed 4500 requests
                  Completed 5000 requests
                  Finished 5000 requests
                  
                  
                  Server Software:        
                  Server Hostname:        localhost
                  Server Port:            8081
                  
                  Document Path:          /
                  Document Length:        1300000 bytes
                  
                  Concurrency Level:      200
                  Time taken for tests:   23.696 seconds
                  Complete requests:      5000
                  Failed requests:        0
                  Write errors:           0
                  Total transferred:      6500530000 bytes
                  HTML transferred:       6500000000 bytes
                  Requests per second:    211.01 [#/sec] (mean)
                  Time per request:       947.830 [ms] (mean)
                  Time per request:       4.739 [ms] (mean, across all concurrent requests)
                  Transfer rate:          267903.43 [Kbytes/sec] received
                  
                  Connection Times (ms)
                                min  mean[±sd] median   max
                  Connect:        0  134 956.2      0    9024
                  Processing:   136  293 474.7    212   13151
                  Waiting:        5  213 410.9    180   13140
                  Total:        137  427 1223.4    212   22161
                  
                  Percentage of the requests served within a certain time (ms)
                    50%    212
                    66%    223
                    75%    231
                    80%    236
                    90%    257
                    95%   1477
                    98%   3231
                    99%   6137
                   100%  22161 (longest request)
                  

                  Если -c побольше ставить, то может и вывалиться
                  • 0
                    Дело не в валится/не валится. Вы запустите сервер, запустите ab как и делали. Потом в новом терминале запустите wget на этот же URL. Я ни разу не получил полный ответ, т.е. Hello, world!' повторенный 100 тыс раз.
                    • 0
                      Пруфпик:


                      Может Content-length неправильно прописали?
                      • 0
                        Правильно, видимо дело в версии питона. На 2.6.6 рвется на 3.2.1rc1 — работает.
                        • 0
                          Я на 2.7.1 запускал. Попробовал 2.6 — тоже нормально.

                          Но вообще довольно нестабильно работает 3-й пример. Например после отработки ab скрипт впадает в бесконечный цикл (т.е. i очень быстро инкрементируется, но при этом нормально отвечает на запросы). Если добавить код из примечаний переводчика (if not data:) то ок.
                          Ну и вылетает при высокой параллельности — нужно очередь увеличивать.
                • 0
                  Извиняюсь за глупый вопрос, но что значит b перед строкой?
                  • 0
                    Строка байт, в отличие от строки в unicode u'...' или «сырой» r'...'.
                  • 0
                    Шикарно. Сэкономили кучу времени, спасибо.
                    • –3
                      Пардон, в статью пока некогда вникать сейчас, просто хочу заметить, что можно перевести «production server», как боевой сервер. Может и не совсем официально звучит, но суть отражает.
                      • +1
                        Спасибо, тема интересная. Но примеры очень сложно читать и понимать т.к. комментарии находятся далеко от кода. Понимаю что перевод, но может встроить комментарии прям в код — как обычные питоньи комментарии?
                        • 0
                          Если будут еще желающие на такой вариант и не будет, соответственно, армии противников — сделаю.
                          • 0
                            Писать комментарии к строчкам кода отдельно от кода используя нумерацию — это очень жестоко по отношению к читателям. Первые примеры еще более-менее воспринимаются, так как кода и комментариев мало и есть возможность охватить их почти без скроллинга страницы. Но вот третий пример — совершенно нечитаемый. В итоге просто прочитал комментарии, а потом просмотрел код. Как-то пытаться их соотносить по номерам — развлечение для особо терпеливых и собранных.

                            Если не трудно, поясните, для чего так было сделано?
                            • 0
                              В таком формате написан оригинал. Решил не менять уж порядка вещей, так как это перевод, а не пересказ.
                              • 0
                                В оригинале не ограничена длина строки текста, как это сделано на Хабре. Поэтому там это не столь сильно мешает, как здесь.
                                • 0
                                  На этой неделе выложу «продолжение», в котором будет простой полнофункциональный пример. Уже не перевод, а отсебятина. Распишу в нем все в виде комментариев в коде.
                        • +1
                          Интересно можно ли использовать метод epoll что бы написать неблокирующий клиент используя httplib или urllib2, те делаем запросы сразу N URL,
                          получаем дескриптор сокета от данного объекта HTTPResponse.fileno() и засовываем в epoll.register
                          • 0
                            Для перевода существующего кода использующего urlllib и т.п., есть eventlet и gevent

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