Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
Клиент TCP Программаклиент находится в файле car_registration.py. Ниже при водится пример сеанса взаимодействия (с уже запущенным сервером и слегка отредактированным меню, чтобы уместить его по ширине книжной страницы): (C)ar (M)ileage (O)wner (N)ew car (S)top server (Q)uit [c]: License: 024 hyr License: 024 HYR Seats: 2 Mileage: 97543 Owner: Jack Lemon (C)ar (M)ileage (O)wner (N)ew car (S)top server (Q)uit [c]: m License [024 HYR]: Mileage [97543]: 103491 Mileage successfully changed Здесь жирнымшрифтом выделены данные, которые были введены пользователем. Там, где ввод пользователя отсутствует, подразумева ется, что пользователь нажал клавишу Enter, принимая значение по Клиент TCP 491 умолчанию. В данном случае пользователь запросил информацию о конкретном автомобиле и затем обновил величину пробега. С сервером могут работать столько клиентов, сколько мы пожелаем, и когда пользователь завершает работу клиентской программы, это никак не сказывается на работе сервера. Но если остановить сервер, то клиент, действиями которого была выполнена остановка, завершит свою работу, а все остальные клиенты при следующей попытке под ключиться к серверу получат сообщение «Connection refused» (попыт ка соединения отвергнута) и их работа будет завершена. В более слож ных приложениях возможность останавливать сервер может предос тавляться только определенным пользователям и, возможно, с опреде ленных машин, но мы включили такую возможность в клиентскую программу, чтобы показать, как это можно реализовать. Теперь приступим к изучению программного кода, начав с функции main() и пользовательского интерфейса, а реализацию сетевых взаимо действий рассмотрим в конце примера. def main(): if len(sys.argv) > 1: Address[0] = sys.argv[1] call = dict(c=get_car_details, m=change_mileage, o=change_owner, n=new_registration, s=stop_server, q=quit) menu = ("(C)ar Edit (M)ileage Edit (O)wner (N)ew car " "(S)top server (Q)uit") valid = frozenset("cmonsq") previous_license = None while True: action = Console.get_menu_choice(menu, valid, "c", True) previous_license = call[action](previous_license) Глобальный список Address хранит IPадрес и номер пор та в двух элементах ["localhost", 9653], где IPадрес мо жет быть переопределен, если он передается в виде аргу мента командной строки. Словарь call отображает пунк ты меню на соответствующие им функции. Модуль Console – один из тех, что поставляются вместе с книгой; он со держит некоторые полезные функции получения значений, вводимых пользователем в консоли, такие как Console.get_string() и Console.get_ integer() , – они похожи на функции, разработанные в первых главах, и были объединены в модуль, чтобы их было проще использовать в разных программах. Для удобства пользователей мы запоминаем последний введенный но мерной знак, чтобы его можно было использовать в качестве значения по умолчанию, потому что выполнение большинства команд начина ется с запроса номерного знака соответствующего автомобиля. Как только пользователь сделает свой выбор, вызывается соответствую щая функция, которой передается номерной знак, и ожидается, что Ветвление с использова нием слова рей, стр. 395 492 Глава 10. Сети каждая функция будет возвращать использовавшийся номер. Так как цикл выполняется бесконечно, программа должна завершаться одной из функций; это будет показано ниже. def get_car_details(previous_license): license, car = retrieve_car_details(previous_license) if car is not None: print("License: {0}\nSeats: {1[0]}\nMileage: {1[1]}\n" "Owner: {1[2]}".format(license, car)) return license Эта функция используется для получения информации о конкретном автомобиле. Поскольку большинству функций требуется запросить номер автомобиля у пользователя и часто необходимо получить неко торые сведения об автомобиле, мы вынесли эту функциональность в отдельную функцию retrieve_car_details() – она возвращает кортеж из двух элементов, содержащий номер автомобиля, введенный пользо вателем, и именованный кортеж CarTuple, хранящий количество поса дочных мест, пробег и владельца (или предыдущий номер автомобиля и значение None, если был введен неопознанный номер). Здесь функция просто выводит полученную информацию и возвращает использовав шийся номер автомобиля, который будет применяться в качестве зна чения по умолчанию при вызове следующей функции, которой потре буется этот номер. def retrieve_car_details(previous_license): license = Console.get_string("License", "license", previous_license) if not license: return previous_license, None license = license.upper() ok, *data = handle_request("GET_CAR_DETAILS", license) if not ok: print(data[0]) return previous_license, None return license, CarTuple(*data) Это первая функция, реализующая взаимодействие по сети. Она вызы вает функцию handle_request(), которую мы рассмотрим немного ни же. Функция принимает handle_request() любые данные, полученные в качестве аргументов, отправляет их серверу и возвращает все, что было получено от сервера. Функция handle_request() ничего не знает об отправляемых и получаемых данных, она просто реализует сетевую службу. В приложении регистрации автомобилей, в соответствии с принятым протоколом запросов, первым аргументом всегда посылается имя ко манды, которая должна быть выполнена сервером, а в остальных аргу ментах – параметры команды, в данном случае это номер автомобиля. Согласно протоколу ответов сервер всегда возвращает кортеж, первым элементом которого является логический флаг, свидетельствующий Клиент TCP 493 об успехе операции. Если флаг имеет значение False, то во втором эле менте кортежа содержится текст сообщения об ошибке. Если флаг имеет значение True, то кортеж будет состоять либо из двух элементов, где во втором элементе содержится текст подтверждения, либо из n элементов, где второй и все последующие элементы хранят запро шенные данные. Далее, если номер автомобиля не будет опознан, переменная ok получит значение False, и тогда функция просто выведет текст сообщения об ошибке из data[0], а вызывающей программе будет возвращен преды дущий номер автомобиля. В противном случае будет возвращен теку щий номер (который теперь становится предыдущим номером) и кор теж CarTuple, созданный из списка data (число мест, пробег, владелец). def change_mileage(previous_license): license, car = retrieve_car_details(previous_license) if car is None: return previous_license mileage = Console.get_integer("Mileage", "mileage", car.mileage, 0) if mileage == 0: return license ok, *data = handle_request("CHANGE_MILEAGE", license, mileage) if not ok: print(data[0]) else: print("Mileage successfully changed") return license Эта функция следует той же схеме, что и get_car_details(), за исклю чением того, что после получения информации она обновляет один из элементов. Здесь фактически выполняется два сетевых запроса, по скольку функция retrieve_car_details() вызывает функцию handle_re quest() для получения информации об автомобиле. Здесь нам необхо димо убедиться в корректности номера автомобиля и получить теку щее значение пробега, которое будет использоваться как значение по умолчанию. В данном случае ответ сервера всегда будет представлять собой кортеж из двух элементов, во втором элементе которого будет со держаться либо текст сообщения об ошибке, либо None. Мы не будем рассматривать функцию change_owner(), поскольку струк турно она полностью повторяет функцию change_mileage(). Мы также не будем рассматривать функцию new_registration(), которая отлича ется лишь тем, что не пытается получить информацию об автомобиле вначале (так как вводится информация о новом автомобиле) и запра шивает у пользователя полный набор сведений. Для нас в этих функ циях нет ничего нового и ничего, что было бы связано с программиро ванием сетевых взаимодействий. def quit(*ignore): sys.exit() 494 Глава 10. Сети def stop_server(*ignore): handle_request("SHUTDOWN", wait_for_reply=False) sys.exit() Если пользователь выбирает команду завершения программы, мы производим выход вызовом функции sys.exit(). Каждая функция, со ответствующая некоторому пункту меню, получает в виде аргумента предыдущий номер автомобиля, но в данном случае в нем нет никакой необходимости. Мы не можем просто написать def quit():, потому что в результате будет создана функция, не ожидающая аргументов, и при вызове такой функции с предыдущим номером автомобиля будет воз буждено исключение TypeError, свидетельствующее о том, что функ ция получила аргумент, который она не ожидала получить. Поэтому мы просто определили параметр *ignore, который способен принять любое число позиционных аргументов. Само имя ignore ничего не зна чит для интерпретатора и используется исключительно для того, что бы показать тому, кто будет сопровождать программу, что аргументы игнорируются функцией. Если пользователь выбирает команду остановки сервера, мы с помо щью функции handle_request() извещаем сервер и указываем при этом, что не ждем ответа. Сразу после отправки данных функция handle_re quest() возвращает управление, не ожидая ответа, и мы завершаем ра боту программы вызовом функции sys.exit(). def handle_request(*items, wait_for_reply=True): SizeStruct = struct.Struct("!I") data = pickle.dumps(items, 3) try: with SocketManager(tuple(Address)) as sock: sock.sendall(SizeStruct.pack(len(data))) sock.sendall(data) if not wait_for_reply: return size_data = sock.recv(SizeStruct.size) size = SizeStruct.unpack(size_data)[0] result = bytearray() while True: data = sock.recv(4000) if not data: break result.extend(data) if len(result) >= size: break return pickle.loads(result) except socket.error as err: print("{0}: is the server running?".format(err)) sys.exit(1) Клиент TCP 495 Эта функция реализует в программе все сетевые взаимодействия. Она начинается с создания объекта struct.Struct, который хранит одно це лое число без знака с сетевым порядком следования байтов, после чего создается законсервированный объект, содержащий все полученные элементы данных. Функция ничего не знает и не делает никаких пред положений об этих элементах данных. Обратите внимание, что здесь явно определяется протокол консервирования 3, чтобы гарантиро вать, что сервер и клиент будут использовать одну и ту же версию про токола, даже если на стороне сервера или на стороне клиента была об новлена версия интерпретатора Python. Если бы нам было необходимо обеспечить возможность дальнейшего развития протокола обмена, мы могли бы предусмотреть передачу его номера версии (как мы делали это с двоичными форматами сохране ния данных на диск). Это можно реализовать как на сетевом уровне, так и на уровне данных. На сетевом уровне мы могли бы предусмот реть передачу номера версии в виде второго целого числа без знака, то есть передавать длину блока данных и номер версии протокола. На уровне данных мы могли бы следовать соглашению, согласно которо му законсервированный объект всегда является списком (или слова рем), в котором первый элемент (или элемент с ключом «version») со держит номер версии. (Вы получите шанс добавить версию протокола, когда будете решать упражнения.) Класс SocketManager – это наш собственный менеджер контекста, пред ставляющий собой сокет, готовый к использованию, – мы рассмотрим его чуть ниже. Метод socket.socket.sendall() отправляет все передан ные ему данные, выполняя за кулисами столько вызовов socket.soc ket.send() , сколько потребуется. Программа всегда передает серверу два элемента данных: длину законсервированного объекта и сам объ ект. Если аргумент wait_for_reply имеет значение False, функция не ждет получения ответа от сервера и немедленно возвращает управле ние – менеджер контекста гарантирует, что сокет будет закрыт до то го, как функция фактически вернет управление. После отправки данных (когда требуется получить ответ) вызывается метод socket.socket.recv(), который принимает ответ. Этот метод бло кирует выполнение программы, пока не примет данные. Первым вы зовом принимаются четыре байта – столько отводится под целое число со значением размера законсервированного объекта, следующего за числом. Здесь с помощью объекта struct.Struct выполняется распако вывание байтов в целое число size. Затем создается пустой объект bytearray и производится попытка получить входящий законсервиро ванный объект блоками размером до 4 000 байтов. Как только будет прочитано size байтов (или если данные были исчерпаны до этого мо мента), производится выход из цикла и распаковывание данных с по мощью функции pickle.loads() (которая принимает объект bytes или bytearray ), после чего данные возвращаются вызывающей программе. 496 Глава 10. Сети В данном случае известно, что данные всегда имеют форму кортежа, так как это определяется протоколом взаимодействия с сервером при ложения регистрации автомобилей, но функция handle_request() ниче го не знает и не делает никаких предположений о типе получаемых данных. Если при попытке выполнить сетевое взаимодействие чтото пойдет не так, например, сервер окажется не запущен или по какимлибо причи нам соединение будет разорвано, будет возбуждено исключение sock et.error . В этом случае исключение будет перехвачено, клиентская программа выведет сообщение об ошибке и завершит работу. class SocketManager: def __init__(self, address): self.address = address def __enter__(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect(self.address) return self.sock def __exit__(self, *ignore): self.sock.close() Объект address – это кортеж из двух элементов (IPадрес и номер пор та); он определяется при создании менеджера контекста. Как только инструкция with задействует менеджер контекста, он создаст сокет и попытается установить соединение, при этом выполнение программы будет заблокировано, пока соединение не будет установлено или пока сокет не возбудит исключение. Первый аргумент функции инициали зации объекта socket.socket() определяет семейство адресов – в данном случае используется семейство socket.AF_INET (IPv4), однако имеются и другие семейства – например, socket.AF_INET6 (IPv6), socket.AF_UNIX и socket.AF_NETLINK. Второй аргумент – это обычно либо socket.SOCK_ STREAM (TCP), как в данном случае, либо socket.SOCK_DGRAM (UDP). Когда поток управления покидает область видимости инструкции with , вызывается метод __exit__() объекта контекста. Мы не заботимся о том, возникло исключение или нет (поэтому мы игнорируем аргу менты с информацией об исключении), и просто закрываем сокет. По скольку метод возвращает None (False – в логическом контексте), любое возникшее исключение продолжит свое распространение – на этот случай мы предусмотрели соответствующий блок except в функции handle_request() , который обработает любое исключение, возникшее в сокете. Сервер TCP Поскольку в большинстве случаев программный код реализации серве ров следует одному и тому же шаблону, вместо низкоуровневого моду Сервер TCP 497 ля socket мы будем использовать высокоуровневый модуль socketser ver , который выполнит всю рутинную работу за нас. Все, что от нас тре буется, это реализовать класс обработчика запросов с методом handle(), который будет использоваться для чтения запросов и записи ответов. Модуль socketserver сам выполняет все необходимые взаимодействия, обслуживая запросы на соединение либо по очереди, либо передавая их своим отдельным потокам или процессам, причем все это делается абсолютно прозрачно, что избавляет нас от необходимости погружать ся в детали низкоуровневой обработки. Программасервер для данного приложения находится в файле car_re gistration_server.py 1 Эта программа содержит определение очень про стого класса Car, который хранит информацию о количестве мест, про беге и владельце в виде свойств (первое из них доступно только для чтения). Класс не хранит информацию о номерном знаке, потому что номер хранится в словаре, где он используется в качестве ключа. Начнем с того, что рассмотрим функцию main(), затем коротко позна комимся с тем, как сервер загружает данные, далее рассмотрим созда ние класса сервера и, наконец, реализацию класса обработчика запро сов, обрабатывающего запросы клиентов. def main(): filename = os.path.join(os.path.dirname(__file__), "car_registrations.dat") cars = load(filename) print("Loaded {0} car registrations".format(len(cars))) RequestHandler.Cars = cars server = None try: server = CarRegistrationServer(("", 9653), RequestHandler) server.serve_forever() except Exception as err: print("ERROR", err) finally: if server is not None: server.shutdown() save(filename, cars) print("Saved {0} car registrations".format(len(cars))) Регистрационная информация об автомобилях хранится в том же ка талоге, что и сама программа. В объект cars записывается ссылка на словарь, ключами которого являются строки с номерами автомоби лей, а значениями – объекты типа Car. Обычно серверы ничего не вы водят на экран, потому что обычно они запускаются и останавливают 1 При первом запуске сервера в операционной системе Windows может по явиться диалог брандмауэра, сообщающий о том, что Python заблокиро ван – щелкните на кнопке Разблокировать (Unblock), чтобы дать серверу воз можность работать. 498 Глава 10. Сети ся автоматически, а выполняются в фоновом режиме. По этой причине они, как правило, сообщают о своем состоянии посредством записи со общений в файлы журналов (например, с помощью модуля logging). Здесь мы решили выводить на экран сообщения при запуске и останов ке, чтобы упростить тестирование и экспериментирование. Наш класс обработчика запросов должен иметь возможность обра щаться к словарю cars, но мы не можем передавать словарь экземпля рам этого класса, так как они будут создаваться сервером без нашего участия – по одному обработчику на каждый запрос. Поэтому мы за писываем ссылку на словарь в атрибут класса RequestHandler.Cars, ко торый обеспечит доступ к словарю всем экземплярам класса. При создании экземпляра сервера ему передаются адрес и порт для вы полнения сетевых взаимодействий, а также класс RequestHandler – сам класс, а не его экземпляр. Пустая строка адреса соответствует любому доступному адресу IPv4 (включая текущий сетевой адрес машины и локальный адрес localhost). Затем серверу сообщается, что он должен выполнять обслуживание запросов без остановки. Когда сервер оста навливается (как это происходит, мы увидим немного ниже), он сохра няет словарь cars, поскольку информация в нем могла быть изменена клиентами. def load(filename): try: with contextlib.closing(gzip.open(filename, "rb")) as fh: return pickle.load(fh) except (EnvironmentError, pickle.UnpicklingError) as err: print("server cannot load data: {1}".format(err)) sys.exit(1) Загрузка выполняется очень просто благодаря тому что здесь исполь зуется менеджер контекста из модуля contextlib, входящего в состав стандартной библиотеки, который гарантирует закрытие файла неза висимо от того, произошло исключение или нет. Другой способ до биться того же эффекта состоит в том, чтобы использовать собствен ный менеджер контекста. Например: class GzipManager: def __init__(self, filename, mode): self.filename = filename self.mode = mode def __enter__(self): self.fh = gzip.open(self.filename, self.mode) return self.fh def __exit__(self, *ignore): self.fh.close() При использовании собственного менеджера GzipManager инструкция with приобретает вид: |