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

LXF82:Python

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
м LXF82:Python» переименована в «LXF82: Говно»)
(исправлен заголовок DNS запроса и цикл кодирования запроса в листинге UDP-CLIENT.PY)
 
(не показаны 5 промежуточных версий 2 участников)
Строка 19: Строка 19:
 
from socket import *
 
from socket import *
 
hostname = sys.argv[1]
 
hostname = sys.argv[1]
HEADER = \x00\x01\x00\x00\x00\x01’
+
HEADER = '\x00\x00\x01\x00\x00\x01'
HEADER += \x00\x00\x00\x00\x00\x00’
+
HEADER += '\x00\x00\x00\x00\x00\x00'
QUESTION = ‘’
+
QUESTION = ''
parts = hostname.split(.)
+
parts = hostname.split('.')
 
for p in parts:
 
for p in parts:
QUESTION += %c%s’ % (chr(len(p)), p)
+
    QUESTION += '%c%s' % (chr(len(p)), p)
QUESTION += \x00\x00\x01\x00\x01’
+
QUESTION += '\x00\x00\x01\x00\x01'
 
QUERY = HEADER + QUESTION
 
QUERY = HEADER + QUESTION
 
cs = socket(AF_INET, SOCK_DGRAM)
 
cs = socket(AF_INET, SOCK_DGRAM)
cs.sendto(QUERY, (‘127.0.0.1’, 53))
+
cs.sendto(QUERY, ('127.0.0.1', 53))
 
rsp = cs.recv(1024)
 
rsp = cs.recv(1024)
 
start = len(QUERY) + 12
 
start = len(QUERY) + 12
print %s.%s.%s.%s’ % (ord(rsp[start]),
+
print '%s.%s.%s.%s' % (ord(rsp[start]),
ord(rsp[start+1]),
+
    ord(rsp[start+1]),
ord(rsp[start+2]),
+
    ord(rsp[start+2]),
ord(rsp[start+3]))
+
    ord(rsp[start+3]))</source>
</source>
+
 
|Ширина=310px}}
 
|Ширина=310px}}
 
Наиболее распространенным способом взаимодействия двух приложений через сеть являются уже знакомые нам сокеты. Модуль
 
Наиболее распространенным способом взаимодействия двух приложений через сеть являются уже знакомые нам сокеты. Модуль
Строка 91: Строка 90:
 
from socket import *
 
from socket import *
 
class IamOK:
 
class IamOK:
def __init__(self, host=’localhost’, port=12345):
+
    def __init__(self, host='localhost', port=12345):
self.socket = socket(AF_INET, SOCK_STREAM)
+
        self.socket = socket(AF_INET, SOCK_STREAM)
self.socket.bind((host, port))
+
        self.socket.bind((host, port))
self.socket.listen(5)
+
        self.socket.listen(5)
def process(self):
+
    def process(self):
while 1:
+
        while 1:
csocket, caddress = self.socket.accept()
+
            csocket, caddress = self.socket.accept()
csocket.send(‘IamOK server v.0.0. Ready to serve.\n’)
+
            csocket.send('IamOK server v.0.0. Ready to serve.\n')
csocket.send(‘You are from %s, port %s...\n’ % caddress)
+
            csocket.send('You are from %s, port %s...\n' % caddress)
while 1:
+
            while 1:
request = csocket.recv(64)
+
                request = csocket.recv(64)
if re.match(‘get\s+uptime’, request, re.IGNORECASE):
+
                if re.match('get\s+uptime', request, re.IGNORECASE):
csocket.send(os.popen(/usr/bin/uptime’).read())
+
                    csocket.send(os.popen('/usr/bin/uptime').read())
elif re.match(‘quit|bye|exit’, request, re.IGNORECASE):
+
                elif re.match('quit|bye|exit', request, re.IGNORECASE):
break
+
                    break
else:
+
                else:
csocket.send(‘Unknown command.\n’)
+
                    csocket.send('Unknown command.\n')
csocket.close()
+
            csocket.close()
if __name__ == ‘__main__’:
+
if __name__ == '__main__':
serv = IamOK()
+
    serv = IamOK()
serv.process()
+
    serv.process()
 
</source>
 
</source>
|Ширина=310px}}
+
|Ширина=550px}}
 
В качестве примера сервера, на этот раз работающего по протоколу TCP,
 
В качестве примера сервера, на этот раз работающего по протоколу TCP,
 
рассмотрим такой код (см Листинг iamok.py).
 
рассмотрим такой код (см Листинг iamok.py).
Строка 157: Строка 156:
 
<source lang="python" line="GESHI_NORMAL_LINE_NUMBERS">
 
<source lang="python" line="GESHI_NORMAL_LINE_NUMBERS">
 
#!/usr/bin/python
 
#!/usr/bin/python
# -*- coding: utf8 -*-
+
# -*- coding: utf-8 -*-
 
import threading as t
 
import threading as t
 
import time, re
 
import time, re
 
diskbusy = t.Lock()
 
diskbusy = t.Lock()
 
def parseit(lognum):
 
def parseit(lognum):
global errors, total
+
    global errors, total
diskbusy.acquire()
+
    diskbusy.acquire()
log = open(‘logs/syslog.%d’ % lognum)
+
    log = open('logs/syslog.%d' % lognum)
lines = log.readlines()
+
    lines = log.readlines()
diskbusy.release()
+
    diskbusy.release()
for line in lines:
+
    for line in lines:
if re.search(‘failed|error’, line):
+
        if re.search('failed|error', line):
errors += 1
+
            errors += 1
total += 1
+
            total += 1
 
class ParseLog(t.Thread):
 
class ParseLog(t.Thread):
def __init__(self, num):
+
    def __init__(self, num):
self.lognum = num
+
        self.lognum = num
t.Thread.__init__(self)
+
        t.Thread.__init__(self)
 
+
    def run(self):
def run(self):
+
        parseit(self.lognum)
parseit(self.lognum)
+
 
#------------------ 1
 
#------------------ 1
 
def test1():
 
def test1():
global errors, total
+
    global errors, total
errors = total = 0
+
    errors = total = 0
for i in range(10):
+
    for i in range(10):
parseit(i)
+
        parseit(i)
print errors, total,
+
    print errors, total
 
#------------------ 2
 
#------------------ 2
 
def test2():
 
def test2():
global errors, total
+
    global errors, total
errors = total = 0
+
    errors = total = 0
running = []
+
    running = []
for i in range(10):
+
    for i in range(10):
tr = ParseLog(i)
+
        tr = ParseLog(i)
tr.start()
+
        tr.start()
running.append(tr)
+
        running.append(tr)
for tr in running:
+
    for tr in running:
tr.join()
+
        tr.join()
 +
    print errors, total
  
print errors, total,
 
 
start = time.time()
 
start = time.time()
 
test1()
 
test1()
print ‘Послед.: %f’ % (time.time() - start)
+
print 'Послед.: %f' % (time.time() - start)
 
start = time.time()
 
start = time.time()
 
test2()
 
test2()
print ‘Потоки: %f’ % (time.time() - start)
+
print 'Потоки: %f' % (time.time() - start)
 
</source>
 
</source>
|Ширина=310px}}
+
|Ширина=350px}}
 
Потоки, наряду с рассмотренными ранее процессами, являются эффективным способом параллельного выполнения различных задач. В
 
Потоки, наряду с рассмотренными ранее процессами, являются эффективным способом параллельного выполнения различных задач. В
 
ряде случаев этим можно воспользоваться не только для одновременного обслуживания нескольких подключений в клиент-серверных
 
ряде случаев этим можно воспользоваться не только для одновременного обслуживания нескольких подключений в клиент-серверных
Строка 281: Строка 279:
 
Здесь мы проводим два теста – последовательная обработка (1) и
 
Здесь мы проводим два теста – последовательная обработка (1) и
 
использование потоков с эксклюзивным доступом к диску (2). В строках
 
использование потоков с эксклюзивным доступом к диску (2). В строках
16–22 мы создаем подкласс класса Thread, в котором переопределяем
+
16–21 мы создаем подкласс класса Thread, в котором переопределяем
метод run(). Запуск потока выполняется в строке 37. Обратите внимание
+
метод run(). Запуск потока выполняется в строке 36. Обратите внимание
на список running (строки 34, 38, 39–40). С его помощью мы отслеживаем активность потоков – метод join() заставляет ждать, пока поток не
+
на список running (строки 33, 37–39). С его помощью мы отслеживаем активность потоков – метод join() заставляет ждать, пока поток не
 
завершит свою работу. Дальнейшая работа основного сценария продолжится только после того, как отработают все порожденные потоки.
 
завершит свою работу. Дальнейшая работа основного сценария продолжится только после того, как отработают все порожденные потоки.
  
Строка 332: Строка 330:
 
print code, msg
 
print code, msg
 
if code == 200:
 
if code == 200:
print host.getfile().read()
+
    print host.getfile().read()
 
</source>
 
</source>
 
|Ширина=310px}}
 
|Ширина=310px}}

Текущая версия на 12:44, 24 февраля 2010

Содержание

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

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

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

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

Наиболее распространенным способом взаимодействия двух приложений через сеть являются уже знакомые нам сокеты. Модуль 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

[править] Всегда к вашим услугам

В качестве примера сервера, на этот раз работающего по протоколу 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.

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

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

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

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

В стандартной поставке 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 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 к вашим услугам модуль 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-сайтов.

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