О разработке одного desktop-приложения на python

Введение

На данный момент мы умеем отправлять и принимать данные по сети и организовывать обработку запросов на сервере. Настало время перейти на более высокий уровень — реализовать свой HTTP сервер.

Для начала определимся, что же такое HTTP. Hypertext Transfer Protocol (HTTP) — это протокол прикладного уровня, предназначенный для передачи гипертекстовых данных в распределенных информационных системах. Ух, сложнааа… А на самом деле нет. Давайте разбираться!

Протокол — это не более, чем соглашение между двумя или более участниками некоторого взаимодействия. Когда речь идет о сетевом взаимодействии, протоколы принято условно разделять на уровни. В самом низу находятся протоколы физического уровня, определяющие как данные передаются в физических средах, т.е. по проводам, оптоволокну, и т.п. Знакомые нам из первой части протоколы IP и TCP — это протоколы сетевого и транспортного уровня, соответственно. Они определяют более высокоуровневые детали взаимодействия, в частности, IP отвечает за адресацию компьютеров/узлов в сети, а TCP — за надежную передачу данных произвольной (т.е. в общем случае превышающей размер одного IP-пакета) длины между узлами. HTTP же располагается на самом высоком уровне — прикладном. От нижележащих протоколов HTTP ожидает гарантий надежности доставки данных, а сам концентрируется на определении понятий запросов и ответов (сообщений) и их семантике. Фактически, HTTP является основным протоколом передачи данных в вебе, а сами данные являются гипертекстом, зачастую представленным в формате HTML-страниц.

До версии HTTP/2, появившейся в 2015 году, HTTP был простым текстовым протоколом. Во второй версии протокол претерпел значительные доработки, стал эффективнее и приобрел новые возможности, но в то же время реализация клиентов и серверов усложнилась. На май 2019 только 37.1% сайтов Интернет используют HTTP/2, но наблюдается устойчивый восходящий тренд.

В этой статье мы рассмотрим, как можно реализовать простейший HTTP-сервер на Python. Ради простоты, мы будем работать с версией протокола HTTP/1.1, а код сервера будет скорее служить образовательным целям, нежели являться полнофункциональным веб-сервером.

What is an HTTP Server?

An HTTP web server is nothing but a process that is running on your machine and does exactly two things:

1- Listens for incoming http requests on a specific TCP socket address (IP address and a port number which I will talk about later)

2- Handles this request and sends a response back to the user.

Let me make my point less abstract.

Imagine you pull up your Chrome browser and type www.yahoo.com in the address bar.

Of course you are going to get the Yahoo home page rendered on your browser window.

But what really just happened under the hood?

Actually a lot of things have happened and I might dedicate a whole article to explain the magic behind how this happened.

But for the sake of simplicity, I will abstract away some of the details and talk about this at a very high level.

At a high level, when you type www.yahoo.com on your browser, your browser will create a network message called an HTTP request.

This Request will travel all the way to a Yahoo computer that has a web server running on it. This web server will intercept your request, and handle it by responding back with the html of the Yahoo home page.

Finally your browser renders this html on the screen and that’s what you see on your screen.

Every interaction with the Yahoo home page after that (for example, when you click on a link) initiates a new request and response exactly like the first one.

To reiterate, the machine that receives the http request has a software process called a web server running on it. This web server is responsible for intercepting these requests and handling them appropriately.

Alright, now that you know what a web server is and what its function is exactly, you might be wondering how does the request reach that yahoo machine in the first place?

Good question!

In fact this is one of my favorite questions that I ask potential candidates in a coding interview.

Let me explain how, but again….at a high level.

A Simple Server

To write Internet servers, we use the socket function available in socket module to create a socket object. A socket object is then used to call other functions to setup a socket server.

Now call bind(hostname, port) function to specify a port for your service on the given host.

Next, call the accept method of the returned object. This method waits until a client connects to the port you specified, and then returns a connection object that represents the connection to that client.

#!/usr/bin/python           # This is server.py file

import socket               # Import socket module

s = socket.socket()         # Create a socket object
host = socket.gethostname() # Get local machine name
port = 12345                # Reserve a port for your service.
s.bind((host, port))        # Bind to the port

s.listen(5)                 # Now wait for client connection.
while True:
   c, addr = s.accept()     # Establish connection with client.
   print 'Got connection from', addr
   c.send('Thank you for connecting')
   c.close()                # Close the connection

Popular Non Full-Stack Frameworks

These projects provide the base «application server», either running as its own independent process, upon Apache or in other environments. On many of these you can then introduce your own choice of templating engines and other components to run on top, although some may provide technologies for parts of the technology stack.

  • Bottle (0.12.17 Released 2019-06-23) is a fast and simple micro-framework for small web-applications. It offers request dispatching (Routes) with url parameter support, Templates, key/value Databases, a build-in HTTP Server and adapters for many third party WSGI/HTTP-server and template engines. All in a single file and with no dependencies other than the Python Standard Library.

  • CherryPy (18.4.0 Released 2019-11-03) is a pythonic, object-oriented HTTP framework. CherryPy powered web applications are in fact stand-alone Python applications embedding their own multi-threaded web server. TurboGears, web2py (see above) also use CherryPy.

  • Flask (1.1.1 Released 2019-07-08) is “a microframework for Python based on Werkzeug, Jinja 2 and good intentions.” Includes a built-in development server, unit tesing support, and is fully Unicode-enabled with RESTful request dispatching and WSGI compliance.

  • Hug (2.6.0 Released 2019-08-29) Embrace the APIs of the future. Hug aims to make developing APIs as simple as possible, but no simpler. It’s one of the first fully future looking frameworks: only supporting Python3+.

  • Pyramid (1.10.4 Released 2019-04-15) a small, fast, down-to-earth, open source Python web development framework. It makes real-world web application development and deployment more fun, more predictable, and more productive. Pyramid is a Pylons Project, and is the successor to the Pylons web framework.

Что такое веб-сервер?

Начнем с того, что четко ответим на вопрос, что же такое веб-сервер?

В первую очередь — это сервер. А сервер — это процесс (да, это не железка), обслуживающий клиентов. Сервер — фактически обычная программа, запущенная в операционной системе. Веб-сервер, как и большинство программ, получает данные на вход, преобразовывает их в соответствии с бизнес-требованиями и осуществляет вывод данных. Данные на вход и выход передаются по сети с использованием протокола HTTP. Входные данные — это запросы клиентов (в основном веб-браузеров и мобильных приложений). Выходные данные — это зачастую HTML-код подготовленных веб-страниц.

На данном этапе логичными будут следующие вопросы: что такое HTTP и как передавать данные по сети? HTTP — это простой текстовый (т.е. данные могут быть прочитаны человеком) протокол передачи информации в сети Интернет. Протокол — это не страшное слово, а всего лишь набор соглашений между двумя и более сторонами о правилах и формате передачи данных. Его рассмотрение мы вынесем в отдельную тему, а далее попробуем понять, как можно осуществлять передачу данных по сети.

1. TCP клиент-сервер

TCP – стандартный протокол для межсетевого взаимодействия. Его основным достоинством является принцип гарантированной доставки – все пакеты, посланные сервером, будут доставлены клиенту.

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

Код простого сервера: вначале мы создаем сокет, представляющий собой указатель на объект соединения. Этому сокету мы передаем два аргумента: первый аргумент говорит о том, что это интернет-сокет, второй – что мы используем TCP-протокол.

Первый метод, который мы используем – , он инициализирует ip-адрес и порт. При этом проверяется, не занят ли порт другой программой.

Второй метод – – устанавливает количество клиентских соединений, которые будет обслуживать операционная система.

Третья функция – – блокирует приложение до тех пор, пока не придет сообщение от клиента. Функция возвращает кортеж из двух параметров – объект самого соединения и адрес клиента.

Четвертая функция – – читает данные из сокета. Аргумент устанавливает максимальное количество байтов в сообщении.

Пятая функция – – отсылает данные клиенту.

Шестая функция – – закрывает сокет.

Функция просто блокирует клавиатуру.

import socket
import sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = 'localhost'
port = 8007
s.bind((host), (port))
s.listen(1)
conn, addr = s.accept()
data = conn.recv(1000000)
print 'client is at', addr , data
conn.send(data)
z = raw_input()
conn.close()

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

import socket
import sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = 'localhost'
port = 8007
s.connect((host, port))
s.send('hello')  
data = s.recv(1000000) 
print 'received', data, len(data), 'bytes'
s.close()

Что стоит за этими простыми методами? По сути, они представляют собой оболочки (wrappers) для аналогичных системных вызовов. Так, метод фактически вызывает вызов операционной системы. Посылаемые данные копируются в буфер операционной системы и при этом могут разбиваться на отдельные блоки (chunk). После того как последний блок будет скопирован в буфер, функция вернет управление программе, при этом совсем не факт, что все данные уже уйдут по назначению и будут получены на том конце клиентом. Клиент по дороге может обнаружить, что отдельные блоки пропали, и запросит их у сервера повторно. Но это уже не наша забота – за полную гарантированную отсылку данных отвечает операционная система, а не приложение.

Обработка запроса

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

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

Дополнительно, в зависимости от заголовка запроса Accept, сервер будет возвращать данные либо в формате HTML, либо JSON.

Прежде, чем приступать непосредственно к обработке, давайте расширим возможности класса Request, чтобы впоследствии код обработки получился чуть более высокоуровневым. Добавим полезные методы и , которые будут разбивать цель вида на и , соответственно:

Обработка запросов начинается в методе . Сам метод занимается скорее диспетчеризацией запросов на основе метода и цели, чем непосредственно обработкой:

Давайте посмотрим на метод создания пользователя :

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

Следующий функция нашего приложения — это возвращение списка зарегистрированных пользователей . В данном случае нам понадобится полноценный ответ, содержащий в себе перечисление всех пользователей на сервере. А в качестве дополнительной возможности, наш сервер будет поддерживать два формата данных — text/html и application/json:

Важно обратить внимание на способ представления. Так как наш ответ содержит символы кириллицы, ASCII кодировка нам не подходит. Мы работаем с как со строкой в кодировке UTF-8

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

Мы работаем с как со строкой в кодировке UTF-8. Однако, прежде чем создать объект ответа, мы кодируем строку в последовательность байт, а заголовок Content-Length, представляющий собой размер ответа, принимает значение длины уже в байтах. Заголовок Content-Type при этом содержит секцию , по которой клиенты нашего сервера могут определить кодировку тела ответа.

Реализацию последнего метода нашего приложения можно посмотреть в в конце статьи.

Python Socket Client Server

Now that we know a few methods for transmitting bytes, let’s create a client and server program with Python.

import socket
serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serv.bind(('0.0.0.0', 8080))
serv.listen(5)
while True:
    conn, addr = serv.accept()
    from_client = ''
    while True:
        data = conn.recv(4096)
        if not data: break
        from_client += data
        print from_client
        conn.send("I am SERVER")
    conn.close()
    print 'client disconnected'

How Does it Work?

This code makes a socket object, and binds it to localhost’s port 8080 as a socket server. When clients connect to this address with a socket connection, the server listens for data, and stores it in the “data” variable.

Then, the program logs the client data using “print,” and then sends a string to the client: I am SERVER.

Let’s take a look at client code that would interact with this server program.

Пару слов о кодировке

В соответствии со спецификацией, одно сообщение HTTP может одновременно содержать данные, представленные в различных кодировках. В то же время, служебные данные, такие как request line, status line и заголовки должны быть преставлены некоторым надмножеством однобайтовой ASCII кодировки, определенном в стандарте ISO/IEC 8859-1. Почему существует такое требование становится очевидно при попытке реализации собственного HTTP-сервера. Как мы уже видели выше, HTTP-запрос — это обычный текст, а текст в компьютерном мире — это последовательность байт плюс дополнительное знание, в какой кодировке эти байты должны быть интерпретированы. Без знания кодировки в общем случае невозможно (и зачастую небезопасно) каким-либо образом интерпретировать текстовые данные, представленные последовательностью байт. Так как никакой предварительной фазы обмена информацией о кодировке в протоколе не предусмотрено, логичным решением является заранее договориться, что все данные по умолчанию передаются в одной и той же кодировке, и такой кодировкой была выбрана ASCII. В таком случае, у сервера всегда существует возможность произвести разбор запроса на составляющие, т.е. отделить request line от блока заголовков, а заголовки друг от друга.

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

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

Задача HTTP-сервера

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

Простейший HTTP-запрос выглядит следующим образом:

То, что мы видим выше — это так называемое сообщение . Опуская вопрос кодировки данных, сообщение HTTP/1.1 — это обычный текст, который состоит из строк, разделенных символами CRLF, т.е. . Первая строка запроса называется . Она определяет метод , цель и версию протокола. Далее идут . В ранних версиях протокола секция заголовков могла отсутствовать полностью, но в HTTP/1.1 заголовок Host является обязательным.

Назначение вышеописанных элементов мы рассмотрим чуть позже, а сейчас перейдем к примеру HTTP-ответа:

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

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

Error Handling

In general, applications should try to trap their own, internal
errors, and display a helpful message in the browser. (It is up
to the application to decide what «helpful» means in this context.)

However, to display such a message, the application must not have
actually sent any data to the browser yet, or else it risks corrupting
the response. WSGI therefore provides a mechanism to either allow the
application to send its error message, or be automatically aborted:
the exc_info argument to start_response. Here is an example
of its use:

try:
    # regular application code here
    status = "200 Froody"
    response_headers = [("content-type", "text/plain")]
    start_response(status, response_headers)
    return 
except:
    # XXX should trap runtime issues like MemoryError, KeyboardInterrupt
    #     in a separate handler before this bare 'except:'...
    status = "500 Oops"
    response_headers = [("content-type", "text/plain")]
    start_response(status, response_headers, sys.exc_info())
    return 

If no output has been written when an exception occurs, the call to
start_response will return normally, and the application will
return an error body to be sent to the browser. However, if any output
has already been sent to the browser, start_response will reraise
the provided exception. This exception should not be trapped by
the application, and so the application will abort. The server or
gateway can then trap this (fatal) exception and abort the response.

Servers should trap and log any exception that aborts an
application or the iteration of its return value. If a partial
response has already been written to the browser when an application
error occurs, the server or gateway may attempt to add an error
message to the output, if the already-sent headers indicate a
text/* content type that the server knows how to modify cleanly.

Some middleware may wish to provide additional exception handling
services, or intercept and replace application error messages. In
such cases, middleware may choose to not re-raise the exc_info
supplied to start_response, but instead raise a middleware-specific
exception, or simply return without an exception after storing the
supplied arguments. This will then cause the application to return
its error body iterable (or invoke write()), allowing the middleware
to capture and modify the error output. These techniques will work as
long as application authors:

Server Extension APIs

Some server authors may wish to expose more advanced APIs, that
application or framework authors can use for specialized purposes.
For example, a gateway based on mod_python might wish to expose
part of the Apache API as a WSGI extension.

In the simplest case, this requires nothing more than defining an
environ variable, such as mod_python.some_api. But, in many
cases, the possible presence of middleware can make this difficult.
For example, an API that offers access to the same HTTP headers that
are found in environ variables, might return different data if
environ has been modified by middleware.

In general, any extension API that duplicates, supplants, or bypasses
some portion of WSGI functionality runs the risk of being incompatible
with middleware components. Server/gateway developers should not
assume that nobody will use middleware, because some framework
developers specifically intend to organize or reorganize their
frameworks to function almost entirely as middleware of various kinds.

So, to provide maximum compatibility, servers and gateways that
provide extension APIs that replace some WSGI functionality, must
design those APIs so that they are invoked using the portion of the
API that they replace. For example, an extension API to access HTTP
request headers must require the application to pass in its current
environ, so that the server/gateway may verify that HTTP headers
accessible via the API have not been altered by middleware. If the
extension API cannot guarantee that it will always agree with
environ about the contents of HTTP headers, it must refuse service
to the application, e.g. by raising an error, returning None
instead of a header collection, or whatever is appropriate to the API.

Similarly, if an extension API provides an alternate means of writing
response data or headers, it should require the start_response
callable to be passed in, before the application can obtain the
extended service. If the object passed in is not the same one that
the server/gateway originally supplied to the application, it cannot
guarantee correct operation and must refuse to provide the extended
service to the application.

These guidelines also apply to middleware that adds information such
as parsed cookies, form variables, sessions, and the like to
environ. Specifically, such middleware should provide these
features as functions which operate on environ, rather than simply
stuffing values into environ. This helps ensure that information
is calculated from environ after any middleware has done any URL
rewrites or other environ modifications.

Other HTTP Features

In general, servers and gateways should «play dumb» and allow the
application complete control over its output. They should only make
changes that do not alter the effective semantics of the application’s
response. It is always possible for the application developer to add
middleware components to supply additional features, so server/gateway
developers should be conservative in their implementation. In a sense,
a server should consider itself to be like an HTTP «gateway server»,
with the application being an HTTP «origin server». (See RFC 2616,
section 1.3, for the definition of these terms.)

However, because WSGI servers and applications do not communicate via
HTTP, what RFC 2616 calls «hop-by-hop» headers do not apply to WSGI
internal communications. WSGI applications must not generate any
«hop-by-hop» headers , attempt to use HTTP features that would
require them to generate such headers, or rely on the content of
any incoming «hop-by-hop» headers in the environ dictionary.
WSGI servers must handle any supported inbound «hop-by-hop» headers
on their own, such as by decoding any inbound Transfer-Encoding,
including chunked encoding if applicable.

Applying these principles to a variety of HTTP features, it should be
clear that a server may handle cache validation via the
If-None-Match and If-Modified-Since request headers and the
Last-Modified and ETag response headers. However, it is
not required to do this, and the application should perform its
own cache validation if it wants to support that feature, since
the server/gateway is not required to do such validation.

Similarly, a server may re-encode or transport-encode an
application’s response, but the application should use a
suitable content encoding on its own, and must not apply a
transport encoding. A server may transmit byte ranges of the
application’s response if requested by the client, and the
application doesn’t natively support byte ranges. Again, however,
the application should perform this function on its own if desired.

Пакеты модулей

При импорте имен модулей фактически загружаются файлы Python, хранящиеся где-то в файловой системе. Как уже упоминалось ранее, импортированные модули должны находиться в каталоге, который указан в пути поиска модуля (sys.path). В Python есть нечто большее, чем «импорт имен» – вы можете импортировать весь каталог, содержащий файлы Python, в виде пакета модулей. Этот импорт известен как импорт пакетов.

Как импортировать пакеты модулей? Давайте создадим каталог с именем mydir, который включает в себя модуль mod0.py и два подкаталога subdir1 и subdir2, содержащий модули mod1.py и mod2.py соответственно. Структура каталогов выглядит так:

Обычный подход, который объяснялся до сих пор, заключался в том, чтобы добавить пути mydir, subdir1 и subdir2 к каталоги поиска модуля (sys.path), чтобы иметь возможность импортировать mod0.py, mod1. py и mod2.py. Это может привести к большим накладным расходами, если модули распределены по множеству разных подкаталогов. В любом случае, импорт пакетов будет предпочтительнее, потому что работают с импортом имени самой папки.

Недопустимая инструкция, приводящая к ошибке InvalidSyntax:

Правильный способ выполнить это – установить каталог /Users/Code/Projects/ в путь поиска модуля (добавив его в переменную среды PYTHONPATH или указав её в файле .pth), а затем импортировать модули, используя точечный синтаксис.

Вы, наверное, уже замечали, что некоторые Python каталоги содержат файл __init__.py. Это было фактически требование в Python2.x, чтобы сообщить Python, что ваш каталог является пакетым модулем. Файл __init__.py также является обычным файлом Python, который запускается всякий раз, когда этот каталог импортируется, и подходит для инициализации значений, например, для подключения к базе данных.

В большинстве случаев эти файлы просто пусты. В Python3.x эти файлы являются необязательными, и их можно использовать, если это необходимо. Следующие несколько строк показывают, как имена, определенные в __init__.py, становятся атрибутами импортируемого объекта (имя каталога, содержащего его).

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

Строка импорта mod2 сообщает Python о поиске модуля mod2 в sys.path, и поэтому он не увенчался успехом. В данном случае нам поможет относительный импорт. Следующий оператор относительного импорта использует двойную точку («..»), которая обозначает родительский элемент текущего пакета (‘mydir /’). Для создания полного относительного пути к модулю mod2 необходимо включить следующий subdir2.

Относительный импорт – это огромная тема, которая может занять целую книгу. Они также сильно отличаются между версиями Python2.x и 3.x. Поддержка Python 2.x заканчивается в 2020 году, поэтому в тех случаях, когда существует большая разница между версиями Python, например, в относительном импорте, лучше сосредоточиться на версии 3.x.

Больше информации

Тут описано использование Streaming протокола, который выполняет функции TCP/IP в сети I2P. SAM API так же предоставляет возможность посылать и принимать анонимные датаграмы, на подобие UDP протокола. Этот функционал в i2plib пока отсутствует и будет добавлен позже.

Это только самая базовая информация, но ее уже вполне достаточно, чтобы начать свой проект в I2P

Невидимый Интернет подходит для написания различных приложений, в которых в первую очередь важно сохранять приватность пользователей. Никаких ограничений на разработчиков сеть не накладывает, это могут быть как client-server, так и P2P приложения

  • Примеры программ с использованием i2plib
  • Документация i2plib
  • Исходный код i2plib на GitHub
  • Документация SAM API
  • Документация asyncio
  • Техническое описание сети I2P

Python Socket Programming Tutorial

Natively, Python provides a socket class so developers can easily implement socket objects in their source code. We can start implementing sockets in our progam with three simple steps:

  1. To use a socket object in your program, start off by importing the socket library. No need to install it with a package manager, it comes out of the box with Python.

import socket

Build Socket Objects

Now we can create socket objects in our code.

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

This code creates a socket object that we are storing in the “sock” variable. The constructor is provided a family and type parameter respectively.  The family parameter is set to the default value, which is the Address Format Internet.

The type parameter is set to Socket Stream, also the default which enables “sequenced, reliable, two-way, connection-based byte streams” over TCP.

Open and Close Connection

Once we have an initialized socket object, we can use some methods to open a connection, send data, receive data, and finally close the connection.

## Connect to an IP with Port, could be a URL
sock.connect(('0.0.0.0', 8080))
## Send some data, this method can be called multiple times
sock.send("Twenty-five bytes to send")
## Receive up to 4096 bytes from a peer
sock.recv(4096)
## Close the socket connection, no more data transmission
sock.close()

Много клиентов — один поток с асинхронным I/O

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

(смотреть на )

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

Зачастую реализация программы, использующей асинхронный ввод-вывод, строится на основе цикла событий. Это бесконечный цикл, читающий события (представленные в виде функций обратного вызова) из очереди с приоритетами. Приоритетом обычно является время готовности к выполнению. Клиентский код, встретив необходимость чтения или записи в сокет, иницирует операцию I/O и регистрирует в очереди событий колбек, который необходимо вызвать для продолжения обработки запроса в момент, когда сокет будет готов. После чего, уступает управление циклу событий. Цикл событий опрашивает все файловые дескрипторы, ожидающие I/O и на основе этой информации обновляет приоритеты событий в очереди. Затем цикл выбирает самый высокоприоритетный элемент и передает выполнение ему.

Модуль предоставляет цикл событий «из коробки». Строка запускает наш сервер на выполнение внутри цикла событий. Сервер при этом реализован также этим модулем (). Нам же лишь остается определить функцию-обработчик (), которую сервер будет вызывать на каждое входящее соединение.

Код, основанный на колбеках обычно достаточно тяжело как писать, так и читать, так как он выглядит нелинейно. При этом колбеки необходимы, чтобы продолжить выполнение кода, прерванного на операции I/O, когда данные станут доступными. С другой стороны, мы знаем, что корутины умеют приостанавливать и возобновлять собственное выполнение в произвольных точках, сохраняя стек. Разумно предположить, что можно воспользоваться этим свойством, приостанавливая выполнения функции и передавая управление циклу событий, встретив необходимость I/O. Для этого Python вводит ключевое слово , а функция, желающая его использовать, должна быть объявлена через , что делает ее корутиной.

Такой код выполняется в одном потоке, при этом возможно обрабатывать большое число одновременных соединений:

Много клиентов — один поток с асинхронным I/O.

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

К сожалению, преимущество подхода является и его недостатком. Так как весь код выполняется в одном потоке, утилизация всех ядер процессора затрудняется

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

Другими известными апологетами обработки запросов с использованием асинхронного ввода-вывода являются веб-сервер nginx и программная платформа Node.js.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *