справочник по Python. мм isbn 9785932861578 9 785932 861578
Скачать 4.21 Mb.
|
Глава 21 . Работа с сетью и сокеты В этой главе описываются модули, используемые для низкоуровневой реализации сетевых серверов и клиентов. Стандартная библиотека языка Python обеспечивает широкую поддержку сетевых операций, начиная от операций с сокетами и заканчивая функциями для работы с прикладными протоколами высокого уровня, такими как HTTP. Для начала будет дано краткое (действительно, очень краткое) введение в разработку сетевых приложений. За дополнительной информацией читателям рекомендуется обращаться к специализированным книгам, таким как «UNIX Network Programming, Volume 1: Networking APIs: Sockets and XTI» У. Ричарда Стивенса (W. Richard Stevens) (издательство Prentice Hall, 1997, ISBN 0-13- 490012-X). 1 Модули, предназначенные для работы с протоколами приклад- ного уровня, описываются в главе 22 «Разработка интернет-приложений». Основы разработки сетевых приложений Модули, входящие в стандартную библиотеку Python и предназначенные для разработки сетевых приложений, главным образом поддерживают два протокола Интернета: TCP и UDP. Протокол TCP – это надежный протокол с созданием логического соединения, используемый для создания между компьютерами двустороннего канала обмена данными. Протокол UDP – это низкоуровневый протокол, обеспечивающий возможность обмена па- кетами, с помощью которого компьютеры могут отправлять и получать информацию в виде отдельных пакетов, без создания логического соедине- ния. В отличие от TCP, взаимодействия по протоколу UDP не отличаются надежностью, что усложняет управление ими в приложениях, в которых необходимо гарантировать надежность обмена информацией. По этой при- чине большинство интернет-приложений используют протокол TCP. 1 Стивенс У. «UNIX. Разработка сетевых приложений». – Пер. с англ. – СПб.: Пи- тер, 2003. 562 Глава 21. Работа с сетью и сокеты Работа с обоими протоколами осуществляется с помощью программной аб- стракции, известной как сокет. Сокет – это объект, напоминающий файл, позволяющий программе принимать входящие соединения, устанавливать исходящие соединения, а также отправлять и принимать данные. Прежде чем два компьютера смогут обмениваться информацией, на каждом из них должен быть создан объект сокета. Компьютер, принимающий соединение, (сервер) должен присвоить свое- му объекту сокета определенный номер порта. Порт – это 16-битное число в диапазоне 0 – 65 535, которое используется клиентами для уникальной идентификации серверов. Порты с номерами 0 – 1023 зарезервированы для нужд системы и используются наиболее распространенными сетевыми протоколами. Ниже перечислены некоторые распространенные протоко- лы с присвоенными им номерами портов (более полный список можно най- ти по адресу: http://www.iana.org/assignments/port-numbers): Служба Номер порта FTP-Data 20 FTP-Control 21 SSH 22 Telnet 23 SMTP (электронная почта) 25 HTTP (WWW) 80 IMAP 143 HTTPS (безопасная WWW) 443 Процедура установки TCP-соединения между клиентом и сервером опреде- TCP-соединения между клиентом и сервером опреде- -соединения между клиентом и сервером опреде- ляется точной последовательностью операций, как показано на рис. 21.1. На стороне сервера, работающего по протоколу TCP, объект сокета, исполь- TCP, объект сокета, исполь- , объект сокета, исполь- зуемый для приема запросов на соединение, – это не тот же самый сокет, что в дальнейшем используется для обмена данными с клиентом. В част- ности, системный вызов accept() возвращает новый объект сокета, который фактически будет использоваться для обслуживания соединения. Это по- зволяет серверу одновременно обслуживать соединения с большим количе- ством клиентов. Взаимодействия по протоколу UDP выполняются похожим способом, за исключением того, что клиенты и серверы не устанавливают логическое соединение друг с другом, как показано на рис. 21.2. Следующий пример иллюстрирует применение протокола TCP клиентом и сервером, использующими модуль socket. В этом примере сервер просто возвращает клиенту текущее время в виде строки. # Программа сервера времени from socket import * import time ёё Основы разработки сетевых приложений 563 Сервер socket() socket() listen() ожидание запросов на соединение обработка запроса установить соединение запрос Клиент connect() write() ответ bind() read() accept() read() write() Рис. 21.1. Процедура установки TCP-соединения Сервер socket() socket() recvfrom() ожидание получения данных обработка запроса запрос Клиент sendto() ответ bind() recvfrom() sendto() bind() Рис. 21.2. Обмен данными по протоколу UDP 564 Глава 21. Работа с сетью и сокеты s = socket(AF_INET, SOCK_STREAM) # Создает сокет TCP s.bind((‘’,8888)) # Присваивает порт 8888 s.listen(5) # Переходит в режим ожидания запросов; # одновременно обслуживает не более # 5 запросов. while True: client,addr = s.accept() # Принять запрос на соединение print(“Получен запрос на соединение с %s” % str(addr)) timestr = time.ctime(time.time()) + “\r\n” client.send(timestr.encode(‘ascii’)) client.close() Н иже приводится клиентская программа: # Программа клиента, запрашивающего текущее время from socket import * s = socket(AF_INET,SOCK_STREAM) # Создать сокет TCP s.connect((‘localhost’, 8888)) # Соединиться с сервером tm = s.recv(1024) # Принять не более 1024 байтов данных s.close() print(“Текущее время: %s” % tm.decode(‘ascii’)) Пример реализации обмена данными по протоколу UDP приводится в раз- UDP приводится в раз- приводится в раз- деле с описанием модуля socket ниже, в этой главе. В сетевых протоколах обмен данными часто выполняется в текстовой фор- ме. Поэтому особое внимание необходимо уделять кодировке текста. В Py- Py- thon 3 все строки состоят из символов Юникода, что влечет за собой не- обходимость кодировать строки, передаваемые через сеть. Именно по этой причине в программе сервера к отправляемым данным применяется метод encode(‘ascii’) . Точно так же, когда клиент принимает данные из сети, эти данные поступают в виде простой последовательности кодированных бай- тов. Если вывести эту последовательность на экран или попробовать ин- терпретировать ее как текст, результат, скорее всего, получится совсем не тот, какого вы ожидали. Поэтому прежде чем работать с данными, их необ- ходимо декодировать. Для этого в программе клиента применяется метод decode(‘ascii’) к принимаемым данным. В оставшейся части главы описываются модули, имеющие отношение к программированию с применением сокетов. В главе 22 описываются вы- сокоуровневые модули, обеспечивающие поддержку различных интернет- приложений, таких как электронная почта и Веб. Модуль asynchat Модуль asynchat упрощает реализацию приложений, в которых использу- ются асинхронные сетевые операции, поддерживаемые модулем asyncore. Это достигается за счет обертывания низкоуровневых операций ввода- вывода, реализованных в модуле asyncore, высокоуровневым программ- ным интерфейсом, предназначенным для работы с сетевыми протокола- ми, основанными на простых механизмах типа «запрос-ответ» (например, HTTP). Модуль asynchat 565 Для работы с этим модулем необходимо определить класс, производный от класса async_chat. Внутри этого класса необходимо определить два мето- да: collect_incoming_data() и found_terminator(). Первый метод вызывается всякий раз, когда через сетевое соединение поступают какие-либо данные. Обычно этот метод просто принимает данные и сохраняет их. Метод found_ terminator() вызывается, когда будет обнаружен конец запроса. Например, при использовании протокола HTTP признаком конца запроса является пустая строка. Для вывода данных объекты класса async_chat поддерживают выходную очередь FIFO. Если программе потребуется отправить данные, ей достаточ- FIFO. Если программе потребуется отправить данные, ей достаточ- . Если программе потребуется отправить данные, ей достаточ- но просто добавить их в очередь. Когда операция записи в сетевое соедине- ние станет возможной, данные из этой очереди будут отправлены автома- тически. async_chat([sock]) Базовый класс. Используется для создания новых обработчиков. Класс async_chat является производным от класса asyncore.dispatcher и предостав- ляет те же методы. В аргументе sock передается объект сокета, который будет использоваться для обмена данными. Экземпляр a класса async_chat обладает следующими методами помимо тех, что уже предоставляются базовым классом asyncore.dispatcher: a.close_when_done() Сообщает об окончании последовательности исходящих данных, добавляя None в выходную очередь FIFO. Когда процедура записи обнаружит это зна- FIFO. Когда процедура записи обнаружит это зна- . Когда процедура записи обнаружит это зна- чение, она закроет канал. a.collect_incoming_data(data) Вызывается всякий раз при поступлении очередной порции данных. В ар- гументе data передаются полученные данные, которые обычно сохраняют- ся для последующего использования. Этот метод должен быть реализован пользователем. a.discard_buffers() Уничтожает все данные, хранящиеся в буферах ввода-вывода и в выходной очереди FIFO. a.found_terminator() Вызывается, когда обнаруживается признак конца данных, установлен- ный методом set_terminator(). Этот метод должен быть реализован пользо- метод должен быть реализован пользо- метод должен быть реализован пользо- должен быть реализован пользо- должен быть реализован пользо- быть реализован пользо- быть реализован пользо- реализован пользо- реализован пользо- пользо- пользо- вателем. Обычно в этом методе выполняется обработка данных, собранных методом collect_incoming_data(). a.get_terminator() Возвращает признак окончания последовательности данных. a.push(data) Добавляет данные в выходную очередь FIFO. В аргументе data передается строка с исходящими данными. 566 Глава 21. Работа с сетью и сокеты a.push_with_producer(producer) Добавляет объект поставщика producer в выходную очередь FIFO. Объект producer может быть любым объектом, имеющим метод more(). Метод more() должен возвращать строку при каждом вызове. Пустая строка служит при- знаком окончания данных. За кулисами объект класса async_chat в цикле будет вызывать метод more(), чтобы получить данные для записи в канал вывода. В очередь FIFO можно добавить несколько объектов поставщиков, многократно вызвав метод push_with_producer(). s.set_terminator(term) Устанавливает признак окончания данных. Аргумент term может быть строкой, целым числом или объектом None. Если в аргументе term переда- ется строка, всякий раз, когда она будет встречаться во входных данных, будет вызываться метод found_terminator(). Если в аргументе term переда- ется целое число, оно интерпретируется, как счетчик байтов. Всякий раз, когда будет принято указанное количество байтов, будет вызываться метод found_terminator() . Если в аргументе term передается None, прием данных бу- дет осуществляться до бесконечности. В модуле имеется класс, который может передаваться методу a.push_with_ producer() для воспроизводства данных. simple_producer(data [, buffer_size]) Создает простой объект поставщика, который делит строку байтов data на фрагменты. Аргумент buffer_size определяет размер фрагмента и по умол- чанию принимает значение 512. Модуль asynchat всегда используется совместно с модулем asyncore. Напри- мер, модуль asyncore может использоваться для создания сервера, при- нимающего входящие соединения, а модуль asynchat – для реализации обработчиков соединений. В следующем примере демонстрируется, как реализовать минимально возможный веб-сервер, обрабатывающий запро- сы GET. В примере почти полностью отсутствуют проверки на наличие ошибок и другие особенности, но его должно быть достаточно для начала. Сравните этот пример с примером, который приводится ниже, в разделе с описанием модуля asyncore. # Асинхронный сервер HTTP, реализованный на основе модуля asynchat import asynchat, asyncore, socket import os import mimetypes try: from http.client import responses # Python 3 except ImportError: from httplib import responses # Python 2 ёё # Следующий класс включает модуль asyncore # и просто принимает входящие соединения class async_http(asyncore.dispatcher): def __init__(self,port): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET,socket.SOCK_STREAM) Модуль asynchat 567 self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.bind((‘’,port)) self.listen(5) ёё def handle_accept(self): client,addr = self.accept() return async_http_handler(client) ёё # Следующий класс обслуживает асинхронные запросы HTTP. class async_http_handler(asynchat.async_chat): def __init__(self,conn=None): asynchat.async_chat.__init__(self,conn) self.data = [] self.got_header = False self.set_terminator(b”\r\n\r\n”) ёё # Принимает входящие данные и добавляет их в буфер def collect_incoming_data(self,data): if not self.got_header: self.data.append(data) ёё # Обрабатывает признак конца данных (пустая строка) def found_terminator(self): self.got_header = True header_data = b””.join(self.data) # Преобразовать данные заголовка (двоичные) в текст # для последующей обработки header_text = header_data.decode(‘latin-1’) header_lines = header_text.splitlines() request = header_lines[0].split() op = request[0] url = request[1][1:] self.process_request(op,url) ёё # Добавляет текст в исходящий поток, предварительно кодируя его def push_text(self,text): self.push(text.encode(‘latin-1’)) ёё # Обрабатывает запрос def process_request(self, op, url): if op == “GET”: if not os.path.exists(url): self.send_error(404,”File %s not found\r\n”) else: type, encoding = mimetypes.guess_type(url) size = os.path.getsize(url) self.push_text(“HTTP/1.0 200 OK\r\n”) self.push_text(“Content-length: %s\r\n” % size) self.push_text(“Content-type: %s\r\n” % type) self.push_text(“\r\n”) self.push_with_producer(file_producer(url)) else: self.send_error(501,”%s method not implemented” % op) self.close_when_done() ёё 568 Глава 21. Работа с сетью и сокеты # Обработка ошибок def send_error(self,code,message): self.push_text(“HTTP/1.0 %s %s\r\n” % (code, responses[code])) self.push_text(“Content-type: text/plain\r\n”) self.push_text(“\r\n”) self.push_text(message) ёё class file_producer(object): def __init__(self,filename,buffer_size=512): self.f = open(filename,”rb”) self.buffer_size = buffer_size def more(self): data = self.f.read(self.buffer_size) if not data: self.f.close() return data ёё a = async_http(8080) asyncore.loop() Чтобы опробовать этот пример, необходимо указывать адреса URL, соот- URL, соот- , соот- ветствующие файлам, находящимся в том же каталоге, где будет запущен сервер. Модуль asyncore Модуль asyncore используется для разработки сетевых приложений, в ко- торых события, связанные с сетью, обрабатываются асинхронно, как по- следовательность событий, рассылаемых циклом событий, построенным на основе системного вызова select(). Такой подход удобно использовать в сетевых программах, реализующих многозадачность без использования потоков управления или процессов. Этот прием способен обеспечить высо- кую производительность при коротких операциях. Все функциональные возможности этого модуля представлены классом dispatcher, который яв- ляется тонкой оберткой вокруг обычного объекта сокета. dispatcher([sock]) Базовый класс, определяющий неблокирующий объект сокета, управ- ляемый событиями. В аргументе sock передается существующий объект сокета. Если конструктор вызывается без аргумента, позднее необходимо будет создать сокет вызовом метода create_socket() (описывается ниже). После его создания сетевые события будут обрабатываться специальными методами-обработчиками. Кроме того, все созданные объекты класса dis- patcher сохраняются во внутреннем списке, который используется некото- рыми функциями опроса. Для обработки сетевых событий вызываются следующие методы объектов класса dispatcher. Они должны быть определены в классах, производных от класса dispatcher. d.handle_accept() Вызывается объектом сокета, принимающим соединения, когда поступает новый запрос на соединение. Модуль asyncore 569 d.handle_close() Вызывается при закрытии сокета. d.handle_connect() Вызывается после того, как соединение будет установлено. d.handle_error() Вызывается, когда появляется необработанное исключение. d.handle_expt() Вызывается, когда сокет получает срочные данные. d.handle_read() Вызывается, когда появляются новые данные, доступные для чтения из со- кета. d.handle_write() Вызывается, когда выполняется операция записи данных. d.readable() Этот метод используется циклом select(), чтобы посмотреть, готов ли объ- ект записывать данные. Возвращает True, если готов, и False – в противном случае. Этот метод вызывается, чтобы определить, должен ли вызываться метод handle_read(), чтобы сообщить о получении новых данных. d.writable() Вызывается циклом select(), чтобы посмотреть, готов ли объект записы- вать данные. Возвращает True, если готов, и False – в противном случае. Этот метод всегда вызывается, чтобы определить, должен ли вызываться метод handle_write(), чтобы произвести операцию записи. Вдобавок к предыдущим методам для выполнения низкоуровневых опера- ций над сокетом могут использоваться следующие методы. Они похожи на методы объекта сокета. |