В статье описывается:
С версии 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 сообщение клиенту.
Официальный HOWTO содержит более детальное описание программирования сокетов в Python.
Пример 1
Пример 2 добавляет цикл в 15 строке для повторной обработки клиентских подключений, выполняемой до пользовательского прерывания (например с клавиатуры). Это яснее показывает, что серверный сокет никогда не используется для обмена данными с клиентом. Скорее, он лишь принимает соединение от клиента и создает новый сокет, который уже и используется для связи.
Блок finally в строках 23-24 нужен для того, чтобы слушающий серверный сокет закрывался в любом случае, даже при возникновении ошибок.
Пример 2
Сокеты, показанные в примере 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, часто работают по следующему принципу:
Пример 3 повторяет функционал примера 2, использующего асинхронные сокеты. Программа сложнее, потому что один поток поочередно взаимодействует со множеством клиентов.
Пример 3
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
При всей схожести, режим 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_CORK может блокировать (bottle up) отправку данных пока они не будут готовы. Эта опция, проиллюстрированная в строках 34 и 40 примера 5, может быть полезна для HTTP сервера, использующего конвейер HTTP/1.1.
Пример 5
С другой стороны, опция TCP_NODELAY сообщает системе, что любые данные, переданные в socket.send(), следует сразу же отправить клиенту без буферизации операционной системой. Эта опция, проиллюстрированная в строке 14 примера 6, может быть полезна для SSH клиентов и других приложений «реального времени».
Пример 6
Примеры на этой странице общедоступны и их можно скачать тут.
При закрытии сокета удаленным клиентом на локальный сокет приходит событие EPOLLIN, но при чтении recv не будет получено ничего. Так что момент
можно написать так:
В этом случае не будет зацикливания при обрыве связи. Встречал код, где разрыв происходит не сразу же, а после нескольких последовательных таких холостых срабатываний, чтобы исключить возможность ошибочного определения.
- Примеры использования блокирующих сокетов
- Преимущества асинхронных сокетов и 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
- import socket
- EOL1 = b'\n\n'
- EOL2 = b'\n\r\n'
- 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: 13\r\n\r\n'
- response += b'Hello, world!'
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- serversocket.bind(('0.0.0.0', 8080))
- serversocket.listen(1)
- connectiontoclient, address = serversocket.accept()
- request = b''
- while EOL1 not in request and EOL2 not in request:
- request += connectiontoclient.recv(1024)
- print(request.decode())
- connectiontoclient.send(response)
- connectiontoclient.close()
- serversocket.close()
Пример 2 добавляет цикл в 15 строке для повторной обработки клиентских подключений, выполняемой до пользовательского прерывания (например с клавиатуры). Это яснее показывает, что серверный сокет никогда не используется для обмена данными с клиентом. Скорее, он лишь принимает соединение от клиента и создает новый сокет, который уже и используется для связи.
Блок finally в строках 23-24 нужен для того, чтобы слушающий серверный сокет закрывался в любом случае, даже при возникновении ошибок.
Пример 2
Copy Source | Copy HTML
- import socket
- EOL1 = b'\n\n'
- EOL2 = b'\n\r\n'
- 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: 13\r\n\r\n'
- response += b'Hello, world!'
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- serversocket.bind(('0.0.0.0', 8080))
- serversocket.listen(1)
- try:
- while True:
- connectiontoclient, address = serversocket.accept()
- request = b''
- while EOL1 not in request and EOL2 not in request:
- request += connectiontoclient.recv(1024)
- print('-'*40 + '\n' + request.decode()[:-2])
- connectiontoclient.send(response)
- connectiontoclient.close()
- finally:
- 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, часто работают по следующему принципу:
- Создается epoll объект
- epoll объекту указывается наблюдать за определенными событиями на определенных сокетах
- У epoll объекта запрашивается на каких сокетах произошли указанные события с момента предыдущего опроса
- Выполняются некоторые действия на этих сокетах
- epoll объекту указывается изменить список сокетов и/или наблюдаемых событий
- Повторяются шаги с 3 по 5 до завершения
- Уничтожается 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
- import socket, select
- EOL1 = b'\n\n'
- EOL2 = b'\n\r\n'
- 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: 13\r\n\r\n'
- response += b'Hello, world!'
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- serversocket.bind(('0.0.0.0', 8080))
- serversocket.listen(1)
- serversocket.setblocking( 0)
- epoll = select.epoll()
- epoll.register(serversocket.fileno(), select.EPOLLIN)
- try:
- connections = {}; requests = {}; responses = {}
- while True:
- events = epoll.poll(1)
- for fileno, event in events:
- if fileno == serversocket.fileno():
- connection, address = serversocket.accept()
- connection.setblocking( 0)
- epoll.register(connection.fileno(), select.EPOLLIN)
- connections[connection.fileno()] = connection
- requests[connection.fileno()] = b''
- responses[connection.fileno()] = response
- elif event & select.EPOLLIN:
- requests[fileno] += connections[fileno].recv(1024)
- if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
- epoll.modify(fileno, select.EPOLLOUT)
- print('-'*40 + '\n' + requests[fileno].decode()[:-2])
- elif event & select.EPOLLOUT:
- byteswritten = connections[fileno].send(responses[fileno])
- responses[fileno] = responses[fileno][byteswritten:]
- if len(responses[fileno]) == 0:
- epoll.modify(fileno, 0)
- connections[fileno].shutdown(socket.SHUT_RDWR)
- elif event & select.EPOLLHUP:
- epoll.unregister(fileno)
- connections[fileno].close()
- del connections[fileno]
- finally:
- epoll.unregister(serversocket.fileno())
- epoll.close()
- 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
- import socket, select
- EOL1 = b'\n\n'
- EOL2 = b'\n\r\n'
- 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: 13\r\n\r\n'
- response += b'Hello, world!'
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- serversocket.bind(('0.0.0.0', 8080))
- serversocket.listen(1)
- serversocket.setblocking( 0)
- epoll = select.epoll()
- epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)
- try:
- connections = {}; requests = {}; responses = {}
- while True:
- events = epoll.poll(1)
- for fileno, event in events:
- if fileno == serversocket.fileno():
- try:
- while True:
- connection, address = serversocket.accept()
- connection.setblocking( 0)
- epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)
- connections[connection.fileno()] = connection
- requests[connection.fileno()] = b''
- responses[connection.fileno()] = response
- except socket.error:
- pass
- elif event & select.EPOLLIN:
- try:
- while True:
- requests[fileno] += connections[fileno].recv(1024)
- except socket.error:
- pass
- if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
- epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)
- print('-'*40 + '\n' + requests[fileno].decode()[:-2])
- elif event & select.EPOLLOUT:
- try:
- while len(responses[fileno]) > 0:
- byteswritten = connections[fileno].send(responses[fileno])
- responses[fileno] = responses[fileno][byteswritten:]
- except socket.error:
- pass
- if len(responses[fileno]) == 0:
- epoll.modify(fileno, select.EPOLLET)
- connections[fileno].shutdown(socket.SHUT_RDWR)
- elif event & select.EPOLLHUP:
- epoll.unregister(fileno)
- connections[fileno].close()
- del connections[fileno]
- finally:
- epoll.unregister(serversocket.fileno())
- epoll.close()
- 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
- import socket, select
- EOL1 = b'\n\n'
- EOL2 = b'\n\r\n'
- 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: 13\r\n\r\n'
- response += b'Hello, world!'
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- serversocket.bind(('0.0.0.0', 8080))
- serversocket.listen(1)
- serversocket.setblocking( 0)
- epoll = select.epoll()
- epoll.register(serversocket.fileno(), select.EPOLLIN)
- try:
- connections = {}; requests = {}; responses = {}
- while True:
- events = epoll.poll(1)
- for fileno, event in events:
- if fileno == serversocket.fileno():
- connection, address = serversocket.accept()
- connection.setblocking( 0)
- epoll.register(connection.fileno(), select.EPOLLIN)
- connections[connection.fileno()] = connection
- requests[connection.fileno()] = b''
- responses[connection.fileno()] = response
- elif event & select.EPOLLIN:
- requests[fileno] += connections[fileno].recv(1024)
- if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
- epoll.modify(fileno, select.EPOLLOUT)
- connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)
- print('-'*40 + '\n' + requests[fileno].decode()[:-2])
- elif event & select.EPOLLOUT:
- byteswritten = connections[fileno].send(responses[fileno])
- responses[fileno] = responses[fileno][byteswritten:]
- if len(responses[fileno]) == 0:
- connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 0)
- epoll.modify(fileno, 0)
- connections[fileno].shutdown(socket.SHUT_RDWR)
- elif event & select.EPOLLHUP:
- epoll.unregister(fileno)
- connections[fileno].close()
- del connections[fileno]
- finally:
- epoll.unregister(serversocket.fileno())
- epoll.close()
- serversocket.close()
С другой стороны, опция TCP_NODELAY сообщает системе, что любые данные, переданные в socket.send(), следует сразу же отправить клиенту без буферизации операционной системой. Эта опция, проиллюстрированная в строке 14 примера 6, может быть полезна для SSH клиентов и других приложений «реального времени».
Пример 6
Copy Source | Copy HTML
- import socket, select
- EOL1 = b'\n\n'
- EOL2 = b'\n\r\n'
- 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: 13\r\n\r\n'
- response += b'Hello, world!'
- serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- serversocket.bind(('0.0.0.0', 8080))
- serversocket.listen(1)
- serversocket.setblocking( 0)
- serversocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
- epoll = select.epoll()
- epoll.register(serversocket.fileno(), select.EPOLLIN)
- try:
- connections = {}; requests = {}; responses = {}
- while True:
- events = epoll.poll(1)
- for fileno, event in events:
- if fileno == serversocket.fileno():
- connection, address = serversocket.accept()
- connection.setblocking( 0)
- epoll.register(connection.fileno(), select.EPOLLIN)
- connections[connection.fileno()] = connection
- requests[connection.fileno()] = b''
- responses[connection.fileno()] = response
- elif event & select.EPOLLIN:
- requests[fileno] += connections[fileno].recv(1024)
- if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
- epoll.modify(fileno, select.EPOLLOUT)
- print('-'*40 + '\n' + requests[fileno].decode()[:-2])
- elif event & select.EPOLLOUT:
- byteswritten = connections[fileno].send(responses[fileno])
- responses[fileno] = responses[fileno][byteswritten:]
- if len(responses[fileno]) == 0:
- epoll.modify(fileno, 0)
- connections[fileno].shutdown(socket.SHUT_RDWR)
- elif event & select.EPOLLHUP:
- epoll.unregister(fileno)
- connections[fileno].close()
- del connections[fileno]
- finally:
- epoll.unregister(serversocket.fileno())
- epoll.close()
- 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
В этом случае не будет зацикливания при обрыве связи. Встречал код, где разрыв происходит не сразу же, а после нескольких последовательных таких холостых срабатываний, чтобы исключить возможность ошибочного определения.