Журнал LinuxFormat - перейти на главную

LXF82:Python

Материал из Linuxformat
Перейти к: навигация, поиск
Python
Python для профессионалов
Python + PyGame
Python + Web
Python + Clutter

Содержание

РАЗРАБОТКА клиент-серверных приложений

ЧАСТЬ 2 Вознамерились написать открытую альтернативу Skype или собственный клиент BitTorrent? Сергей Супрунов научит всему необходимому – от основ архитектуры «клиент-сервер» до готовых библиотек для работы с существующими интернет-протоколами.

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

Клиент всегда прав

UDP-CLIENT.PY
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import sys
  4. from socket import *
  5. hostname = sys.argv[1]
  6. HEADER = '\x00\x00\x01\x00\x00\x01'
  7. HEADER += '\x00\x00\x00\x00\x00\x00'
  8. QUESTION = ''
  9. parts = hostname.split('.')
  10. for p in parts:
  11.     QUESTION += '%c%s' % (chr(len(p)), p)
  12. QUESTION += '\x00\x00\x01\x00\x01'
  13. QUERY = HEADER + QUESTION
  14. cs = socket(AF_INET, SOCK_DGRAM)
  15. cs.sendto(QUERY, ('127.0.0.1', 53))
  16. rsp = cs.recv(1024)
  17. start = len(QUERY) + 12
  18. print '%s.%s.%s.%s' % (ord(rsp[start]),
  19.     ord(rsp[start+1]),
  20.     ord(rsp[start+2]),
  21.     ord(rsp[start+3]))

Наиболее распространенным способом взаимодействия двух приложений через сеть являются уже знакомые нам сокеты. Модуль socket, входящий в стандартную поставку Python, помимо рассмотренных в прошлый раз Unix-сокетов поддерживает также сокеты домена Internet. Методология использования мало чем отличается от Unix-сокетов, за исключением того, что вместо параметра AF_UNIX используется AF_INET, а вместо имени файла указываются имя хоста и номер порта, которые будут обслуживаться создаваемым сокетом.

В качестве второго параметра в конструкторе сокета можно указать его тип: с установлением соединения, соответствующий протоколу TCP, или без соединения – протокол UDP. Допустимые значения – SOCK_STREAM и SOCK_DGRAM соответственно. По умолчанию подразумевается SOCK_STREAM.

Как пример, рассмотрим работу приложения, выполняющего роль примитивного (и весьма ограниченного функционально) клиента DNS, работающего по протоколу UDP (см. листинг udp-client.py).

Некоторую сложность здесь представляет то, что DNS относится к так называемым «двоичным» протоколам, в отличие от «текстовых», таких как HTTP или SMTP, где обмен идет обычными текстовыми строками. В случае с DNS оперировать приходится «сырыми» байтами. Сведения по формату сообщений можно почерпнуть из RFC 1035, раздел «4. MESSAGES».

В 6-й и 7-й строках рассматриваемого кода формируется заголовок (12 байт). Пренебрегая всем богатством возможностей протокола DNS, мы ограничиваемся простым запросом (OPCODE=0) одного доменного имени (QDCOUNT=1). Конструкции вида «\x00» позволяют задать в строке произвольный шестнадцатиричный код.

В строках 8–12 формируется поле запроса. Оно состоит из частей доменного имени (в оригинале разделенных точками), перед которыми указывается число символов в этой части. Например, имя mail.ru состоит из двух частей (mail – 4 символа, ru – 2 символа) и в запросе должно выглядеть так: \x04mail\x02ru\x00. Завершающий ноль, а также два двухбайтовых поля (QTYPE и QCLASS) добавляются к переменной QUESTION в строке 12.

Наконец, строки 14 и 15 – создание сокета (обратите внимание на второй параметр, SOCK_DGRAM, указывающий тип транспортного протокола UDP) и отправка запроса. Поскольку UDP работает без установки соединения, то вместо знакомой нам пары методов «connect – send» мы используем один «sendto», в котором указывается сразу и отправляемая информация, и адрес получателя. В данном примере предполагается, что DNS-сервер работает на локальной машине. Вы можете указать здесь свой DNS-сервер или передавать его имя в качестве параметра.

И в строках 17–21 из всей полезной информации, возвращаемой сервером, мы, игнорируя любые возможные ошибки, выбираем только IP-адрес, размещаемый по смещению, которое формируется в переменной start. Результат работы:

serg$ ./udp-client.py donpac.ru
80.254.111.2

Всегда к вашим услугам

IAMOK.PY
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import os, re
  4. from socket import *
  5. class IamOK:
  6.     def __init__(self, host='localhost', port=12345):
  7.         self.socket = socket(AF_INET, SOCK_STREAM)
  8.         self.socket.bind((host, port))
  9.         self.socket.listen(5)
  10.     def process(self):
  11.         while 1:
  12.             csocket, caddress = self.socket.accept()
  13.             csocket.send('IamOK server v.0.0. Ready to serve.\n')
  14.             csocket.send('You are from %s, port %s...\n' % caddress)
  15.             while 1:
  16.                 request = csocket.recv(64)
  17.                 if re.match('get\s+uptime', request, re.IGNORECASE):
  18.                     csocket.send(os.popen('/usr/bin/uptime').read())
  19.                 elif re.match('quit|bye|exit', request, re.IGNORECASE):
  20.                     break
  21.                 else:
  22.                     csocket.send('Unknown command.\n')
  23.             csocket.close()
  24. if __name__ == '__main__':
  25.     serv = IamOK()
  26.     serv.process()

В качестве примера сервера, на этот раз работающего по протоколу TCP, рассмотрим такой код (см Листинг iamok.py).

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

Здесь все должно быть понятно по прошлому уроку. Два отличия – в конструкторе socket.socket() указывается второй параметр – SOCK_STREAM (в данном случае его можно было бы и опустить, т.к. для Internet-домена и так по умолчанию используется протокол TCP). И метод bind() осуществляет привязку сокета не к файлу, а к имени хоста и номеру порта, на котором будут ожидаться входящие соединения. Кстати, выбирая номер порта, не забывайте, что порты до 1024-го относятся к привилегированным и могут быть задействованы только пользователем root.

Наш сервер понимает две команды: «get uptime», по которой возвращается информация о времени работы сервера, и «quit» (с двумя синонимами – «bye» и «exit»), по которой сеанс завершается.

Проверить работу сервера можно с помощью обычной telnet-сессии:

admin@dom:~/lxf/propy/l2$ telnet localhost 12345
Trying 127.0.0.1...
Connected to localhost.localdomain.
Escape character is ‘^]’.
IamOK server v.0.0. Ready to serve.
You are from 127.0.0.1, port 2650...
helo
Unknown command.
get uptime
23:09:26 up 58 min, 3 users, load average: 0.04, 0.15, 0.63
quit
Connection closed by foreign host.

Какая от этого может быть польза – решайте сами.

«Гуртом и батьку бить веселей»

THREAD-TEST.PY
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import threading as t
  4. import time, re
  5. diskbusy = t.Lock()
  6. def parseit(lognum):
  7.     global errors, total
  8.     diskbusy.acquire()
  9.     log = open('logs/syslog.%d' % lognum)
  10.     lines = log.readlines()
  11.     diskbusy.release()
  12.     for line in lines:
  13.         if re.search('failed|error', line):
  14.             errors += 1
  15.             total += 1
  16. class ParseLog(t.Thread):
  17.     def __init__(self, num):
  18.         self.lognum = num
  19.         t.Thread.__init__(self)
  20.     def run(self):
  21.         parseit(self.lognum)
  22. #------------------ 1
  23. def test1():
  24.     global errors, total
  25.     errors = total = 0
  26.     for i in range(10):
  27.         parseit(i)
  28.     print errors, total
  29. #------------------ 2
  30. def test2():
  31.     global errors, total
  32.     errors = total = 0
  33.     running = []
  34.     for i in range(10):
  35.         tr = ParseLog(i)
  36.         tr.start()
  37.         running.append(tr)
  38.     for tr in running:
  39.         tr.join()
  40.     print errors, total
  41.  
  42. start = time.time()
  43. test1()
  44. print 'Послед.: %f' % (time.time() - start)
  45. start = time.time()
  46. test2()
  47. print 'Потоки: %f' % (time.time() - start)

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

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

В стандартной поставке Python для работы с потоками есть два модуля – thread и threading. Первый позволяет управлять потоками на достаточно низком уровне, второй – использует средства первого для предоставления более удобного объектно-ориентированного интерфейса. Класс threading.Thread предоставляет «шаблон» потока. Для его использования в своей программе вам нужно переопределить метод run(), описав в нем действия, которые должны выполняться этим потоком. Ниже приведен пример, из которого все должно стать понятно.

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

Вот если бы нам удалось организовать работу потоков таким образом, чтобы, пока один из них выполняет чтение файла, другие не обращались к диску... Для решения этой задачи в языке Python доступно несколько средств синхронизации работы потоков. Простейшее из них – использование обычных переменных-флагов. Как вы помните, потоки разделяют оперативную память, принадлежащую процессу, в рамках которого они исполняются, так что изменения переменных будут «видны» всем процессам. Идея здесь проста:

if DISKBUSY:
# ожидание
else:
DISKBUSY = 1
# чтение файла
DISKBUSY = 0

То есть первый поток выставит истинное значение глобальной переменной DISKBUSY и приступит к чтению файла. «Опоздавшие» потоки будут ждать, пока переменная вновь не примет значение «ложь».

В такой реализации программисту предстоит решить не такую уж простую, как может показаться на первый взгляд, задачу – грамотно обеспечить ожидание. Бесконечный цикл проверки значения переменной слишком сильно нагружает процессор (не даром такие циклы называют напряженными). Напрашивающееся time.sleep(1) очень не эффективно – если ресурс освободится до того, как истечет время «спячки», то он будет простаивать. [Кроме того, подобный метод синхронизации сам по себе не атомарен – подробности ищите в статье «Очереди сообщений и семафоры»]

Однако в модуле threading есть готовая реализация описанной выше идеи – класс Lock, который предоставляет программисту так называемые блокировки, иногда именуемые «мьютексами» (mutex). Идя проста – создается объект данного класса (threading.Lock()), который имеет два метода: acquire() позволяет захватить объект, release() – освободить его. Метод aquire() является блокирующим: очередной поток, вызвавший его, будет ждать до тех пор, пока мьютекс не освободится.

Дальнейшим развитием идеи блокировок являются семафоры. Фактически, семафор – это тот же мьютекс, но позволяющий захватить себя несколько раз. Если вы создадите семафор командой semaphore = threading.Semaphore(3), то его смогут захватить (тем же методом semaphore.acquire()) одновременно три потока (каждый раз отнимая по единице из указанного при инициализации числа). Четвертый поток сможет захватить семафор только после того, как он будет высвобожден (semaphore.release()) одним из тех, которые удерживают его в настоящее время.

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

Здесь мы проводим два теста – последовательная обработка (1) и использование потоков с эксклюзивным доступом к диску (2). В строках 16–21 мы создаем подкласс класса Thread, в котором переопределяем метод run(). Запуск потока выполняется в строке 36. Обратите внимание на список running (строки 33, 37–39). С его помощью мы отслеживаем активность потоков – метод join() заставляет ждать, пока поток не завершит свою работу. Дальнейшая работа основного сценария продолжится только после того, как отработают все порожденные потоки.

В строке 5 мы создаем мьютекс, с помощью которого в строках 8 и 11 будет регулироваться доступ потоков к диску. В глобальной переменной errors ведется подсчет числа строк, в которых есть подстрока «failed» или «error», в total – общее число обработанных строк. Результат работы:

21931 2302755 Послед.: 33.271387
21931 2302755 Потоки: 22.867245

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

Все включено

В поставку Python входит несколько готовых модулей, позволяющих легко и быстро разработать сетевую программу, например, HTTP-сервер или FTP-клиент. Более детально вы сможете познакомиться с ними в документации или в хорошо прокомментированных исходных кодах самих модулей. Здесь же рассмотрим их возможности обзорно.

HTTP

HTTP-SERVER.PY
  1. #!/usr/bin/python
  2. # -*- coding: utf8 -*-
  3. import SimpleHTTPServer as http
  4. handler = http.SimpleHTTPRequestHandler
  5. server = http.BaseHTTPServer.HTTPServer((‘localhost’, 8080), handler)
  6. server.serve_forever()
HTTP-CLIENT.PY
  1. #!/usr/bin/python
  2. # -*- coding: utf8 -*-
  3. import httplib
  4. host = httplib.HTTP(‘localhost:8080’)
  5. host.putrequest(‘GET’, ‘/testpage.html)
  6. host.putheader(‘accept’, ‘text/html’)
  7. host.endheaders()
  8. code, msg, headers = host.getreply()
  9. print code, msg
  10. if code == 200:
  11.     print host.getfile().read()

Для работы с HTTP Python предоставляет четыре основных модуля: BaseHTTPServer, SimpleHTTPServer, CGIHTTPServer и httplib. Первые три реализуют простейшие серверы, причем второй и третий модули используют возможности первого, предоставляя программисту более высокоуровневый интерфейс к его методам. Модуль httplib служит для разработки HTTP-клиентов.

Например, простейший HTTP-сервер может выглядеть таким образом (см. листинг http-server.py).

Как видите – всего четыре «рабочих» строчки, и то строка под номером 4 служит лишь для присвоения столь длинного имени метода-обработчика более короткой и удобной переменной. Вести себя этот сервер будет как «самый настоящий»: он будет возвращаться запрошенные html-страницы или файлы из текущего и вложенных в него каталогов, при наличии файлов index.html или index.htm в каталоге, из которого сервер запущен, клиенту по умолчанию (когда указано только имя каталога) будут отдаваться они. Если индексные файлы отсутствуют, автоматически будет строиться страница-содержание каталога (аналогично работает Apache с включенным модулем mod_autoindex). В ответ на запрос несуществующего ресурса будут возвращаться сообщения об ошибке, и т.д.

Клиентское приложение будет не намного сложнее (см Листинг http-client.py).

В строках 4–7 формируется нужный HTTP-заголовок, затем получаем и распечатываем ответ сервера:

admin@dom:~/lxf/propy/l2$ ./http-client.py
200 OK
<HTML><HEAD>
<TITLE>Test page</TITLE>
</HEAD><BASE>
<H2>It is a test page</H2>
</BASE></HTML>

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

Электронная почта

Модули smtplib, poplib, imaplib предоставляют клиентские интерфейсы к соответствующим протоколам. Их использование не намного сложнее рассмотренного выше httplib, и, думаю, вы без труда в них разберетесь. Для более тонкой обработки содержимого почтовых сообщений (выделения заголовков, вложений и т.д.) вам помогут модули rfc822, mimetools, multifile, base64, mailbox и другие. Все они очень хорошо прокомментированы и снабжены достаточно подробной документацией. Простейший способ получить к ней доступ – функция help(). Например:

>>> import mailbox
>>> help(mailbox)

Этот код выведет встроенную справку по работе с модулем mailbox прямо в окне интерактивного терминала.

FTP

FTP-CLIENT.PY
  1. #!/usr/bin/python
  2. # -*- coding: uft-8 -*-
  3. import ftplib
  4. ftp = ftplib.FTP(‘ftp.freebsd.org)
  5. ftp.login(‘ftp’, ‘my@mail.ru)
  6. ftp.cwd(‘pub/FreeBSD’)
  7. retfile = ‘README.TXT
  8. ftp.retrbinary(‘RETR %s’ % retfile,
  9. open(retfile, ‘w+’).write, 1024)
  10. ftp.quit()

Для работы с протоколом FTP к вашим услугам модуль ftplib. Работа с ним ведется на достаточно низком уровне, и порой напоминает обычный сеанс FTP, выполняемый вручную (см. листинг ftp-client.py).

В итоге выполнения этого скрипта в текущем каталоге должен появиться файл README.TXT, скачанный с ftp-сервера ftp://ftp.freebsd.org.

Заключение

Итак, на этом мы завершим знакомство с основными сетевыми возможностями языка Python. Хочу заметить, что они выходят далеко за рамки простейших сценариев, пригодных для тестирования «больших» серверов или встраивания некоторых сетевых возможностей в ваши приложения. Приведу лишь несколько примеров. Так, в 90-х годах большой популярностью пользовался web-браузер Grail, разработанный на Python и предоставляющий весьма широкие для того времени возможности по обработке интернет-страниц – полная поддержка стандарта HTML 2.0 и, в значительной мере, HTML 3.2, поддержка различных форматов изображений и звука, способность работать с языком разметки SGML, поддержка FTP, и т.д.

Менеджер почтовых рассылок Mailman обеспечивает широкие возможности по управлению списками рассылок, включая web-интерфейс. Интернет-сервер Medusa обладает достаточно хорошими характеристиками, позволяя использовать его как для тестовых целей, так и для промышленной эксплуатации. Популярный сервер web-приложений Zope также полностью разработан на языке Python.

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

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

Персональные инструменты
купить
подписаться
Яндекс.Метрика