Работа с сетевыми сокетами на Python

Сетевой сокет — это эндпоинт межпроцессного взаимодействия в компьютерной сети. В Python Standard Library есть модуль socket, предоставляющий низкоуровневый сетевой интерфейс. Этот интерфейс является общим для разных языков программирования, поскольку он использует системные вызовы на уровне операционной системы.

Для создания сокета существует функция, называемая socket. Она принимает аргументы family, type и proto (подробнее см. в документации). Чтобы создать TCP-сокет, нужно использовать socket.AF_INET или socket.AF_INET6 для family и socket.SOCK_STREAM для type.

Пример Python socket:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Функция возвращает объект сокета, который имеет следующие основные методы:

  • bind()
  • listen()
  • accept()
  • connect()
  • send()
  • recv()

Методы bind(), listen() и accept() специфичны для серверных сокетов, а метод connect() — для клиентских. send() и recv() являются общими для обоих типов сокетов. Приведем пример Echo-сервера, взятый из документации:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 50000))
s.listen(1)
conn, addr = s.accept()
while 1:
    data = conn.recv(1024)
    if not data:
        break
    conn.sendall(data)
conn.close()

Здесь мы создаем серверный сокет, привязываем его к localhost и 50000-му порту и начинаем прослушивать входящие соединения.

Чтобы принять входящее соединение, мы вызываем метод accept(), который будет блокироваться до тех пор, пока не подключится новый клиент. Когда это произойдет, метод создаcт новый сокет и вернет его вместе с адресом клиента.

Затем он в бесконечном цикле считывает данные из сокета партиями по 1024 байта, используя метод recv(), пока не вернет пустую строку. После этого он отправляет все входящие данные обратно, используя метод sendall(), который в свою очередь многократно вызывает метод send(). И после этого сервер просто закрывает клиентское соединение. Данный пример может обрабатывать только одно входящее соединение, потому что он не вызывает accept() в цикле.

Код на стороне клиента выглядит проще:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 50000))
s.sendall('Hello, world')
data = s.recv(1024)
s.close()
print 'Received', repr(data)

Вместо методов bind() и listen() он вызывает только метод connect() и сразу же отправляет данные на сервер. Затем он получает обратно 1024 байта, закрывает сокет и выводит полученные данные.

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

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

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

Существует множество интерфейсов для разных операционных систем:

Все они примерно одинаковы, поэтому давайте создадим сервер с помощью Python select. Пример Python select:

import select, socket, sys, Queue
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(0)
server.bind(('localhost', 50000))
server.listen(5)
inputs = [server]
outputs = []
message_queues = {}

while inputs:
    readable, writable, exceptional = select.select(
        inputs, outputs, inputs)
    for s in readable:
        if s is server:
            connection, client_address = s.accept()
            connection.setblocking(0)
            inputs.append(connection)
            message_queues[connection] = Queue.Queue()
        else:
            data = s.recv(1024)
            if data:
                message_queues[s].put(data)
                if s not in outputs:
                    outputs.append(s)
            else:
                if s in outputs:
                    outputs.remove(s)
                inputs.remove(s)
                s.close()
                del message_queues[s]

    for s in writable:
        try:
            next_msg = message_queues[s].get_nowait()
        except Queue.Empty:
            outputs.remove(s)
        else:
            s.send(next_msg)

    for s in exceptional:
        inputs.remove(s)
        if s in outputs:
            outputs.remove(s)
        s.close()
        del message_queues[s]

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

Создание серверного сокета происходит так же, кроме одной строки: server.setblocking(0). Это нужно для того, чтобы сокет не блокировался. Такой сервер более продвинутый, поскольку он может обслуживать более одного клиента. Главная причина заключается в сокетах selecting:

readable, writable, exceptional = select.select(
    inputs, outputs, inputs)

Здесь мы вызываем метод select.select для того, чтобы операционная система проверила, готовы ли указанные сокеты к записи и чтению, и нет ли каких-либо исключений. Метод передает три списка сокетов, чтобы указать, какой сокет должен быть доступен для записи, какой — для чтения и какой следует проверить на наличие ошибок.

Этот вызов (если не передан аргумент timeout) блокирует программу до тех пор, пока какие-либо из переданных сокетов не будут готовы. В этот момент вызов вернет три списка сокетов для указанных операций.

Затем метод последовательно перебирает эти списки и, если в них есть сокеты, выполняет соответствующие операции. Если сокет сервера присутствует в inputs, это значит, что появился новый клиент. Поэтому вызывается метод accept(), далее возвращаемый сокет добавляется к inputs, а также добавляется очередь для входящих сообщений, которые будут отправлены обратно. Если в inputs есть другой сокет, это значит, что сообщения прибыли и готовы к чтению, поэтому метод читает их и помещает в соответствующую очередь.

Для сокетов с возможностью записи он получает сообщения (если они есть) из очереди и записывает их в сокет. Если в сокете есть ошибки, метод удаляет сокет из списков.

Так работают сокеты на низком уровне. Однако в большинстве случаев нет необходимости реализовывать настолько низкоуровневую логику. Рекомендуется использовать более высокоуровневые абстракции, такие как Twisted, Tornado или ZeroMQ, в зависимости от ситуации.