История у сокетов давняя. Их применение началось с ARPANET в 1971 году и продолжилось в 1983-м, когда в операционной системе Berkeley Software Distribution (BSD) появился API под названием «сокеты Беркли». В 1990-х годах вместе со Всемирной паутиной возникло и сетевое программирование. Преимущества сокетов и первых подключаемых сетей применялись не только в веб-серверах и браузерах — широко стали применяться клиент-серверные приложения разных типов и размеров. Базовые протоколы API сокетов развивались многие годы, появились и новые, а низкоуровневый API остался прежним. Самые распространённые сегодня приложения с сокетами — это клиент-серверные приложения, где одна сторона действует как сервер и ожидает подключения клиентов. Именно такое приложение вы напишете благодаря руководству. А конкретнее, сосредоточимся на API сокетов для интернет-сокетов. Иногда их называют сокетами Беркли, или сокетами BSD. Есть и сокеты домена Unix, которые используются для взаимодействия между процессами внутри только одного компьютера. В модуле socket есть интерфейс к API сокетов Беркли. Вот основные функции и методы этого API: В Python имеется удобный и последовательный API, напрямую сопоставленный с системными вызовами, то есть аналогами функций из списка выше на C. В следующем разделе вы узнаете, как эти функции используются вместе. Кроме того, в стандартной библиотеке Python есть классы, которые упрощают применение этих функций. Хотя в этом руководстве socketserver не рассматривается, с этим фреймворком для сетевых серверов можно ознакомиться по ссылке. Доступно много модулей, где реализованы интернет-протоколы уровня выше, например HTTP и SMTP. Обзор этих протоколов смотрите в разделе документации Python «Интернет-протоколы и их поддержка». С помощью socket.socket() вы создадите объект сокета с указанием типа сокета socket.SOCK_STREAM. При этом по умолчанию применяется протокол управления передачей (TCP). Возможно, это то, что вам нужно. Но зачем вам TCP? Вот его особенности: Для сравнения: сокеты, которые создаются через socket.SOCK_DGRAM протокола пользовательских датаграмм ненадёжны: данные могут считываться получателем с изменением порядка очерёдности записей отправителя. Почему это важно? Сети — это система негарантированной доставки. Нет гарантии, что данные дойдут до места назначения или что отправленные данные будут получены. Сетевые устройства — маршрутизаторы и коммутаторы — также обладают конечной полосой пропускания и собственными, системными ограничениями. Как на клиентах и на серверах, у них есть процессоры, память, шины и интерфейсные буферы пакетов. С TCP при этом не нужно беспокоиться о потере пакетов, поступлении данных с изменением порядка очерёдности пакетов данных, а также о других подводных камнях. Чтобы разобраться лучше, ознакомьтесь с последовательностью вызовов API сокетов и с потоком данных TCP. Ниже слева сервер, а справа клиент: 
Поток TCP-сокетов. В центре изображения показан обмен данными между клиентом и сервером с помощью вызовов .send() и .recv(). Внизу соответствующие сокеты закрываются на клиенте и на сервере. (источник изображения) Начиная с верхнего левого угла, показаны серверные вызовы API на сервере, которые настраивают «прослушиваемый» сокет: Этим сокетом, как следует из его названия, прослушиваются подключения от клиентов. Чтобы принять или завершить такое подключение, на сервере вызывается .accept(). А чтобы установить подключение к серверу и инициировать трёхэтапное рукопожатие, на клиенте вызывается .connect(). Процесс рукопожатия важен, ведь он гарантирует доступность каждой стороны подключения в сети, то есть то, что клиент может связаться с сервером, и наоборот. Возможно, только один хост, клиент или сервер может связаться с другим. Теперь, когда вы узнали об API сокетов и взаимодействии клиента и сервера, вы готовы создать свои первые клиент и сервер. Начнём с примера, где полученное сервером сообщение просто возвращается клиенту, как эхо. Вот он: Не пытайтесь понять весь код сразу. В этих нескольких строках много чего происходит. И это только отправная точка, здесь можно увидеть базовый сервер в деле. Но что же происходит в вызове нашего API? С помощью socket.socket() создаётся объект сокета, которым поддерживается тип контекстного менеджера, который используется в операторе with. Вызывать s.close() не нужно: Передаваемые в socket() аргументы — это константы, используемые для указания семейства адресов и типа сокетов. AF_INET — это семейство интернет-адресов для IPv4. SOCK_STREAM — это тип сокета для TCP и протокол, который будет использоваться для передачи сообщений в сети. Метод .bind() применяется для привязки сокета к конкретному сетевому интерфейсу и номеру порта: Передаваемые в .bind() значения зависят от семейства адресов сокета. В этом примере используется socket.AF_INET (IPv4). Поэтому принимается кортеж с двумя значениями: (host, port). host может быть именем хоста, IP-адресом или пустой строкой. Если используется IP-адрес, то host должен быть строкой адреса формата IPv4. IP-адрес 127.0.0.1 — это стандартный IPv4-адрес для интерфейса «внутренней петли», когда к серверу подключаются только процессы в хосте. Если передавать пустую строку, подключения на сервере принимаются во всех доступных интерфейсах IPv4. port — это номер TCP-порта для приёма подключений от клиентов. Это должно быть целое число от 1 до 65535(0 резервируется). В некоторых системах, если номер порта меньше 1024, могут потребоваться привилегии суперпользователя. Относительно использования имён хостов с .bind() есть замечание: «Если в хостовой части адреса сокета IPv4/v6 использовать имя хоста, программа может стать непредсказуемой: Python использует первый возвращаемый из разрешения DNS адрес. Адрес сокета будет разрешён в фактический адрес IPv4/v6 по-разному, в зависимости от результатов из DNS-разрешения и/или конфигурации хоста. Чтобы поведение было предсказыемым, в хостовой части используйте числовой адрес». Документация. Подробнее об этом вы узнаете позже в разделе «Использование имён хостов». А пока достаточно понять, что при использовании имени хоста можно увидеть разные результаты в зависимости от того, чтó возвращается в процессе разрешения имён. Это может быть что угодно: при первом запуске приложения можно получить 10.1.2.3, а в следующий раз получится 192.168.0.1. Дальше может быть 172.16.7.8 и т. д. В примере ниже подключения на сервере принимаются благодаря .listen(), а сам сервер становится «прослушиваемым» сокетом: У метода .listen() есть параметр backlog. Он указывает число непринятых подключений, которые система разрешит до отклонения новых подключений. С версии Python 3.5 он необязателен. Если его нет, выбирается значение backlog по умолчанию. А если на сервере получается много одновременных запросов на подключение, значение backlog можно увеличить через установку максимальной длины очереди для отложенных подключений. Это предельное значение зависит от системы. Например, на Linux смотрите /proc/sys/net/core/somaxconn. Методом .accept() выполнение блокируется, и ожидается входящее подключение. При подключении клиента возвращается новый объект сокета, который представляет собой подключение и кортеж с адресом клиента. В кортеже содержится (host, port) — для подключений IPv4 или (host, port, flowinfo, scopeid) — для IPv6. Подробнее о значениях кортежей рассказывается в справочном разделе «Семейства адресов сокетов». Итак, теперь у вас есть новый объект сокета из .accept(). Это важно потому, что сокет будет использоваться для взаимодействия с клиентом. Он отличается от прослушиваемого, который применяется на сервере для приёма новых подключений: После того как в .accept() клиенту предоставляется объект сокета conn, для перебора блокирующих вызовов в conn.recv() используется бесконечный цикл while. Так любые отправляемые от клиента данные считываются и передаются обратно с помощью conn.sendall(). Если в conn.recv() возвращается пустой объект bytes и b'', значит, в клиенте подключение закрыто и цикл завершён. Чтобы автоматически закрыть сокет в конце блока, с conn применяется оператор with. Перейдём к клиенту: По сравнению с сервером клиент довольно прост. В нём создаётся объект сокета. Для подключения к серверу используется .connect(), и для отправки сообщения вызывается s.sendall(), s.recv() считывает ответ, а затем этот ответ выводится. В этом разделе запускаем клиент и сервер, следим за их поведением и за происходящим.  Откройте терминал или командную строку, перейдите в каталог со скриптами, убедитесь, что в переменной PATH у вас есть Python 3.6 или новее, а затем запустите сервер: Терминал зависнет, потому что сервер заблокирован или находится в состоянии ожидания, в .accept(): Ожидается подключение клиента. Затем откройте другое окно терминала или командную строку и запустите клиента: В окне сервера вы должны заметить что-то такое: Здесь на сервере выведен кортеж addr, возвращаемый из s.accept(). Это IP-адрес клиента и номер TCP-порта — 64623 (скорее всего, он будет другим, когда вы запустите сервер на своём компьютере). Чтобы увидеть текущее состояние сокетов на хосте, используйте netstat. На macOS, Linux и Windows он доступен по умолчанию. А ниже вывод netstat из macOS после запуска сервера: Обратите внимание: Local Address здесь 127.0.0.1.65432. Если бы в echo-server.py был HOST = "", а не HOST = "127.0.0.1", в netstat отображалось бы это: Local Address здесь *.65432. Это означает, что для приёма входящих подключений будут задействованы все поддерживающие семейство адресов доступные интерфейсы хоста. В этом примере в вызове socket() используется socket.AF_INET (IPv4) — смотрите tcp4 в столбце Proto. Здесь показывается только вывод эхо-сервера. Скорее всего, полный вывод будет гораздо больше, это зависит вашей системы. Стóит обратить внимание на столбцы Proto, Local Address и (state). В последнем примере netstat показывает, что на эхо-сервере используется TCP-сокет IPv4 (tcp4) в порте 65432 на всех интерфейсах (*.65432) и он находится в состоянии прослушивания (LISTEN). Другой способ получить к нему доступ (и дополнительную полезную информацию) — использовать программу lsof, которая выводит список открытых файлов. На macOS она доступна по умолчанию, а на Linux её можно установить пакетным менеджером: Если lsof используется с параметром -i, в её выводе предоставляется COMMAND, PID (идентификатор процесса) и USER (идентификатор пользователя) открытых интернет-сокетов. Выше показан процесс эхо-сервера. netstat и lsof различаются в зависимости от ОС, у них много опций. Загляните в их man или документацию, на них определённо стóит потратить немного времени. На macOS и Linux используйте man netstat и man lsof. На Windows — netstat /? . При попытке подключения к порту без прослушиваемого сокета есть типичная ошибка: Здесь либо указан неверный номер порта, либо не запускается сервер. Или, может быть, на пути стоит брандмауэр, которым подключение блокируется (об этом легко забыть). Также может быть сообщение об ошибке Connection timed out («Превышено время ожидания подключения»). Чтобы клиент подключался к TCP-порту, добавьте соответствующее правило брандмауэра! Посмотрите подробности взаимодействия клиента и сервера:  При работе с интерфейсом «внутренней петли» (IPv4 127.0.0.1 или IPv6 ::1) данные никогда не покидают хост и не касаются внешней сети. Выше интерфейс внутренней петли находится внутри хоста. Такова его природа: подключения и данные, которые пропускаются через него, локальны для хоста. Поэтому интерфейс внутренней петли и IP-адреса 127.0.0.1 и ::1 и называются localhost. В приложениях этот интерфейс используется для взаимодействия с другими процессами, которые запускаются на хосте, а также в целях безопасности и предоставления изолированности от внешней сети. Внутренний, доступный только изнутри хоста, этот интерфейс недоступен извне. Посмотреть на это в действии можно, если у вас есть сервер приложений со своей, закрытой базой данных. Если эта БД не используется на других серверах, наверное, она конфигурируется на прослушивание подключений только в интерфейсе localhost. Если так, то другим хостам сети нельзя подключиться к нему. Если не использовать в приложениях адреса 127.0.0.1 или ::1, вероятно, отличный от них адрес привяжется к интерфейсу Ethernet, подключённому к внешней сети. Это шлюз к другим хостам за пределами localhost:  Поосторожнее там! Это неприглядный, жестокий мир. Прежде чем отважиться покинуть безопасный localhost, обязательно прочитайте раздел «Использование имён хостов». Там есть примечания по безопасности, которое применяется, даже если вы используете не имена хостов, а только IP-адреса. У эхо-сервера определённо имеются ограничения. Самое большое — им обслуживается только один клиент, после чего работа завершается. У эхо-клиента такое же ограничение, но имеется дополнительная проблема. Когда в клиенте используется s.recv(), из b'Hello, world' можно вернуть только один байт (b'H'): Аргумент bufsize (1024) — это максимальный объём данных, получаемых одновременно. Он не означает, что из .recv() вернётся 1024 байт. Поведение метода .send() аналогичное: в нём возвращается число отправленных байтов, которое может оказаться меньше размера переданных данных. Это нужно проверить и, чтобы отправить данные, вызвать .send() столько раз, сколько потребуется: «В приложениях нужно проверять, что отправлены все данные. Если передана только их часть, нужно пробовать доставить оставшиеся данные». (Источник) В примере выше этого удалось избежать благодаря .sendall(): «В отличие от send(), в этом методе отправка байт данных продолжается, пока не будут отправлены все данные или не возникнет ошибка. В случае успеха метод возвращает None». (Источник) Здесь возникают две проблемы: Что можно сделать? В плане конкурентности подходов много. Один из популярных — применение асинхронного ввода-вывода. asyncio появился в стандартной библиотеке в Python 3.4. Традиционное решение — использовать потоки. Проблема конкурентности заключается в сложности её правильного понимания. Здесь нужно учитывать и остерегаться многих нюансов. Стóит проявиться одному из них, и приложение внезапно может просто-напросто выйти из строя. Это не должно отбить желание осваивать и применять конкурентное программирование. Если приложение нужно масштабировать, использование более одного процессора или ядра необходимо. Но это руководство опирается на дедушку системных вызовов – более традиционый метод .select(), объяснить который проще. Метод .select() проверяет завершение ввода-вывода более чем на одном сокете. Вызовите его, чтобы посмотреть, у каких сокетов ввод-вывод готов для чтения и/или для записи. Кроме .select(), есть ещё кое-что: чтобы использовать самую эффективную реализацию независимо от ОС, будет применяться стандартный модуль selectors: «Этим модулем обеспечивается высокоуровневое и эффективное мультиплексирование ввода-вывода, основанное на примитивах модуля select. Пользователям рекомендуется применять этот модуль, если нет необходимости точно управлять примитивами, которые используются на уровне ОС». (Источник) Тем не менее, применяя .select(), вы не сможете обеспечить конкурентное выполнение. В то же время, в зависимости от рабочей нагрузки, этот подход всё равно может оказаться достаточно быстрым. Это определяется количеством поддерживаемых в приложении клиентов и тем, что именно в нём должно делаться при обслуживании запроса. Модуль asyncio для управления задачами использует однопоточную кооперативную многозадачность и цикл событий. С помощью .select() вы напишете собственную версию цикла событий, пусть даже проще и синхроннее. При использовании нескольких потоков, даже если присутствует конкурентность, сейчас приходится применять GIL (глобальную блокировку интерпретатора) с CPython и PyPy. Этим, так или иначе, ограничивается объём работы, выполняемой параллельно. Всё это свидетельствует о том, что использование .select() может быть идеальным выбором. Не думайте, что придётся задействовать asyncio, потоки или новейшую асинхронную библиотеку. В сетевом приложении задача обычно, так или иначе, ограничена вводом-выводом: это может быть ожидание в локальной сети конечных точек на другой стороне сети, ожидание записи на диск и т. д. Если вы получаете запросы от клиентов, которыми инициируется связанная с ЦП работа, обратите внимание на модуль concurrent.futures. В нём есть класс ProcessPoolExecutor. Этот класс для асинхронного выполнения вызовов использует пул процессов. Если вы задействуете несколько процессов, в операционной системе может планироваться параллельное выполнение кода Python на нескольких процессорах или ядрах без глобальной блокировки интерпретатора. Идеи и вдохновение можно почерпнуть из доклада на PyCon John Reese — Thinking Outside the GIL with AsyncIO and Multiprocessing — PyCon 2018 («Джон Риз. AsyncIO и многопроцессорная обработка: выходя за рамки глобальной блокировки интерпретатора. PyCon 2018»). В следующем разделе вы увидите примеры сервера и клиента с решением этих проблем. Чтобы одновременно обрабатывать несколько подключений и вызывать .send() и .recv() нужное количество раз, в примерах используется .select(). В следующих разделах вы напишете сервер и клиента, где несколько подключений обрабатываются с помощью объекта selector из модуля selectors. Обратимся сначала к серверу с несколькими подключениями. В первой части настраивается прослушиваемый сокет: Самое большое отличие этого сервера от эхо-сервера — вызов настройки сокета в неблокируемом режиме внутри lsock.setblocking(False). Вызовы в этот сокет больше не будут блокирующими. Когда этот вызов применяется вместе с sel.select(), можно ожидать события в одном или нескольких сокетах, а затем считывать и записывать данные по мере их готовности. Демонстрацию вы увидите ниже. С помощью sel.register() сокет регистрируется, а через sel.select() в нём отслеживаются нужные события. Для прослушиваемого сокета, например, нужны события чтения, то есть selectors.EVENT_READ. Чтобы сохранять любые произвольные данные, которые бы понадобились вместе с сокетом, и отслеживать, чтó в нём отправлено и получено, используется data, возвращаемый вместе с .select(). Вот цикл событий: sel.select(timeout=None) realpython.com/python-sockets//#blocking-calls, пока не будет сокетов, готовых для ввода-вывода. В нём возвращается список кортежей — по одному для каждого сокета. В каждом кортеже есть key и mask. key — это namedtuple [именованный кортеж] SelectorKey, в котором содержится атрибут fileobj. key.fileobj — это объект сокета, а mask— маска события готовых операций. Если key.data будет None, то понятно, что это из прослушиваемого сокета, и подключение нужно принять. Чтобы получить новый объект сокета и зарегистрировать его с селектором, вызывается ваша функция accept_wrapper(). Она показана чуть ниже. Если key.data не None, то понятно, что это сокет уже принятого клиента, и его нужно обслуживать. Затем вызывается service_connection() с аргументами key и mask. Это всё, что нужно для работы с сокетом. Вот что делается в функции accept_wrapper(): Прослушиваемый сокет зарегистрирован для события selectors.EVENT_READ, поэтому он должен быть готов к считыванию. Вызывается sock.accept(), а затем, для перевода сокета в неблокируемый режим, – conn.setblocking(False). И это – основная цель данной версии сервера, ведь нужно, чтобы он не блокировался. Если сервер заблокируется, то он весь остановится до возврата. Это означает, что другие сокеты остаются в ожидании, даже если сервер не используется активно – вот и жутковатое «зависания», в котором серверу находиться нежелательно. Дальше при помощи SimpleNamespace создаётся объект для хранения данных, которые должны идти вместе с сокетом. Поскольку вам нужно знать, когда подключение клиента готово для чтения и записи, оба этих события задаются с помощью побитового «ИЛИ»: Маска events, сокет и объекты данных затем передаются в sel.register(). Теперь посмотрите, как обрабатывается готовое подключение клиента в service_connection(): Вот сердце простого сервера с несколькими подключениями. key — это namedtuple, возвращаемый из .select(), в котором содержится объект сокета fileobj и объект данных. В mask находятся готовые события. Если сокет готов к считыванию, mask & selectors.EVENT_READ окажется равным True, поэтому вызывается sock.recv(). Любые считанные данные добавляются в data.outb, их можно отправить позже. Обратите внимание на блок else:. В нём проверяется, не получены ли данные: Если данные не получены, значит, сокет на клиенте закрыли, поэтому на сервере нужно сделать то же самое. Но не забудьте перед закрытием вызвать sel.unregister(), чтобы сокет не отслеживался через .select(). Когда сокет готов к записи (работоспособный сокет всегда должен быть готов к ней), любые полученные данные, которые хранятся в data.outb, передаются клиенту с помощью sock.send(). Затем отправленные байты удаляются из буфера для пересылки: В методе .send() возвращается число отправленных байтов, с помощью этого числа позже байты можно отбросить при помощи нотации среза в буфере .outb. Теперь обратимся к multiconn-client.py, клиенту с несколькими подключениями. Он очень похож на сервер. Только вместо прослушивания подключений он начинается с их инициирования через start_connections(): Значение num_conns считывается из командной строки. Это количество создаваемых подключений к серверу. Аналогично серверу каждый сокет настраивается на неблокируемый режим. С помощью .connect() сразу выдаётся исключение BlockingIOError, поэтому вместо него используется метод .connect_ex(), где изначально возвращается индикатор ошибки errno.EINPROGRESS, а не выдаётся исключение, из-за которого подключение затрудняется. Когда подключение завершено, готовый к чтению и записи сокет возвращается из .select(). После настройки сокета для создания сохраняемых с ним данных используется SimpleNamespace. Сообщения, отправляемые от клиента на сервер, копируются с помощью messages.copy(), потому что при каждом подключении вызывается socket.send() и список меняется. В объекте data хранится всё необходимое для отслеживания того, что именно на клиенте нужно отправить, что именно отправлено и получено (в том числе общее количество байтов в сообщениях). Вот изменения [добавленные и удалённые строки, + и -] из серверного service_connection() для версии на клиенте: По сути, то же самое, но с важным отличием. В клиенте отслеживается количество полученных от сервера байтов, так что эту сторону подключения можно закрыть. Когда это обнаруживается на сервере, другая сторона подключения тоже закрывается. Cервер при этом зависит от поведения клиента, когда ожидается, что та сторона подключения закроется после отправки сообщений. Если клиент не закроется, подключение на сервере останется открытым. На практике может понадобиться защита от этого путём реализации на сервере тайм-аута для предотвращения накопления клиентских подключений, если в них по истечении определённого времени не отправляется запрос. Пора запускать multiconn-server.py и multiconn-client.py. И там и там используются аргументы командной строки. Можно запустить без аргументов и посмотреть варианты. Для сервера передайте номера host и port: А для клиента передайте num_connections, то есть количество создаваемых подключений к серверу: Вот вывод сервера при прослушивании в интерфейсе «внутренней петли» в порте 65432: А вот вывод клиента при создании в нём двух подключений к серверу: Отлично! Клиент и сервер с несколькими подключениями запущен. В следующем разделе вы расширите применение этого примера.