Программирование на Python 3. Руководство издательство СимволПлюс
![]()
|
499 with GzipManager(filename, "rb") as fh: Функция save() (здесь она не показана) по своей структуре очень похо жа на функцию load(), только она открывает файл в двоичном режиме для записи, сохраняет данные с помощью функции pickle.dump() и ни чего не возвращает. class CarRegistrationServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass Это полное определение нашего собственного класса сер вера. Если появится необходимость создать сервер, кото рый будет обслуживать запросы в отдельных процессах, а не в потоках, достаточно будет лишь унаследовать класс socketserver.ForkingMixIn вместо класса socketser ver.ThreadingMixIn . Термин mixin (смесь) часто использу ется для описания классов, специально предназначен ных для множественного наследования. Классы из моду ля socketserver могут использоваться для создания са мых разнообразных серверов, включая серверы UDP и серверы UDP и TCP в операционной системе UNIX, по средством наследования соответствующей пары базо вых классов. Обратите внимание, что класссмесь из модуля socketserver всегда дол жен наследоваться первым. Это гарантирует, что методы классасмеси будут пользоваться преимуществом перед методами второго наследуе мого класса при наличии одноименных методов в обоих классах, по скольку интерпретатор Python выполняет поиск методов в базовых классах в том порядке, в каком они перечислены в определении клас са, и использует первый найденный метод. Для обработки каждого запроса сервер создает обработчик запросов (используя переданный ему класс). Наш собственный класс Request Handler реализует методы для обработки каждого типа запросов плюс обязательный метод handle(), который вызывается классом сервера. Но прежде чем перейти к рассмотрению этих методов, познакомимся с объявлением класса и с атрибутами класса. class RequestHandler(socketserver.StreamRequestHandler): CarsLock = threading.Lock() CallLock = threading.Lock() Call = dict( GET_CAR_DETAILS=( lambda self, *args: self.get_car_details(*args)), CHANGE_MILEAGE=( lambda self, *args: self.change_mileage(*args)), CHANGE_OWNER=( lambda self, *args: self.change_owner(*args)), NEW_REGISTRATION=( Множествен ное наследо вание, стр. 449 500 Глава 10. Сети lambda self, *args: self.new_registration(*args)), SHUTDOWN=lambda self, *args: self.shutdown(*args)) Мы наследуем класс socketserver.StreamRequestHandler, потому что бу дем использовать потоковый (TCP) сервер. Для создания серверов UDP можно использовать класс socketserver.DatagramRequestHandler, а для обеспечения низкоуровневого доступа можно было бы наследовать класс socketserver.BaseRequestHandler. Словарь RequestHandler.Cars – это атрибут класса, который добавляется в функции main(). Он хранит все регистрационные данные. Добавле ние дополнительных атрибутов в объекты (такие как классы или эк земпляры) может производиться за пределами объявления класса (в данном случае – в функции main()) без какихлибо ограничений (при условии, что объект имеет атрибут __dict__), что может быть очень удобно. Так как заранее известно, что класс зависит от этого атрибута, можно было бы добавить строку Cars = None в объявление, чтобы за фиксировать существование переменной. Почти каждый метод, обрабатывающий запросы, должен иметь дос туп к словарю Cars, но нам следует гарантировать, что обращение к не му никогда не будет происходить из двух методов (из двух разных по токов) одновременно, в противном случае это может привести к повре ждению словаря или к аварийному завершению программы. Чтобы избежать этих неприятностей, мы предусмотрели блокировку, оформ ленную в виде атрибута класса. С ее помощью мы сможем гарантиро вать, что в каждый конкретный момент времени только один поток будет иметь доступ к словарю. 1 (Принципы разработки многопоточ ных программ, включая использование блокировок, рассматриваются в главе 9.) Словарь Call – это еще один атрибут класса. Каждый ключ это словаря является именем операции, которую может выполнять сервер, а каж дое значение – это функция, выполняющая соответствующую опера цию. Мы не можем использовать методы непосредственно, как это бы ло сделано с функциями в словаре, обслуживающем меню в клиент ской программе, потому что на уровне класса отсутствует ссылка self. Чтобы решить эту проблему, мы использовали функцииобертки, ко торые получают значение ссылки self в момент вызова и затем вызы вают соответствующий метод, передавая ему ссылку self и любые дру гие аргументы. Как вариант, словарь Call можно было бы создать по сле определения всех методов. Это позволило бы создать такие записи, как GET_CAR_DETAILS=get_car_details, и интерпретатор Python сумел бы 1 Глобальная блокировка интерпретатора (Global Interpreter Lock, GIL) гарантирует синхронизированный доступ к сло варю Cars, но, как отмечалось ранее, мы не можем полагать ся на нее, так как она является особенностью реализации CPython. Глобальная блокировка GIL, стр. 478 Сервер TCP 501 отыскать метод get_car_details(), так как словарь создается после то го, как все методы были определены. Но мы предпочли использовать первый вариант, так как он более явный и не зависит от того, в каком месте класса находится определение словаря. Несмотря на то, что после создания класса словарь Call всегда будет использоваться только для чтения, тем не менее, учитывая, что словарь является изменяемым объектом, мы решили обеспечить дополнительный уро вень безопасности и определили блокировку, которая га рантирует, что в каждый конкретный момент времени доступ к нему будет иметь только один поток. (Она так же не является обязательной изза глобальной блокиров ки, присутствующей в реализации CPython.) def handle(self): SizeStruct = struct.Struct("!I") size_data = self.rfile.read(SizeStruct.size) size = SizeStruct.unpack(size_data)[0] data = pickle.loads(self.rfile.read(size)) try: with self.CallLock: function = self.Call[data[0]] reply = function(self, *data[1:]) except Finish: return data = pickle.dumps(reply, 3) self.wfile.write(SizeStruct.pack(len(data))) self.wfile.write(data) Всякий раз, когда клиент выполняет запрос, создается новый поток с новым экземпляром класса RequestHandler, после чего вызывается ме тод handle() экземпляра. Внутри этого метода данные, полученные от клиента, можно прочитать с помощью объекта файла self.rfile, а от правка данных клиенту может быть выполнена с помощью объекта файла self.wfile. Оба эти объекта предоставляются модулем socket server уже открытыми и готовыми к использованию. Объект типа struct.Struct – это целочисленный счетчик байтов, кото рый необходим, чтобы прочитать формат «длина плюс данные», ис пользуемый при обмене данными между клиентами и сервером. Сначала выполняется чтение первых четырех байтов и распаковыва ние их в целое число size, чтобы узнать размер законсервированного объекта, отправленного клиентом. Затем выполняется чтение size байтов и распаковывание их в переменную data. Операция чтения бло кирует дальнейшее выполнение, пока данные не будут прочитаны. В данном случае известно, что данные всегда поступают в виде корте жа, первый элемент которого определяет выполняемую операцию, Глобальная блокировка GIL, стр. 478 502 Глава 10. Сети а остальные элементы являются параметрами этой операции, потому что это определено протоколом, установленным для клиентов. Внутри блока try мы получаем лямбдафункцию, соответствующую запрошенной операции. Доступ к словарю Call выполняется под защи той блокировки, хотя вполне возможно, что мы проявляем излишнюю осторожность. Как обычно, мы стараемся выполнять как можно мень ше действий в области видимости блокировки – в данном случае мы всего лишь отыскиваем в словаре ссылку на требуемую функцию. Как только функция будет получена, она тут же вызывается и ей переда ются в виде первого аргумента – ссылка self и в виде последующих ар гументов – остальное содержимое кортежа data. Здесь производится вызов функции, поэтому ссылка self не передается ей интерпретато ром. Это не имеет большого значения, так как мы сами передаем ссыл ку self, а внутри лямбдафункции полученная ссылка self использу ется для вызова метода обычным способом. В результате производится вызов метода self. method (*data[1:]) , где method – это метод, соответст вующий операции, указанной в data[0]. Если выбрана операция остановки сервера, в методе shutdown() возбуж дается наше собственное исключение Finish – в этой ситуации извест но, что клиент не ожидает ответа, поэтому мы просто можем вернуть управление. Но для остальных операций выполняется консервирова ние (с использованием протокола 3) объекта с результатами работы метода, соответствующего запрошенной операции, и запись – сначала размера законсервированного объекта, а затем самого объекта с дан ными. def get_car_details(self, license): with self.CarsLock: car = copy.copy(self.Cars.get(license, None)) if car is not None: return (True, car.seats, car.mileage, car.owner) return (False, "This license is not registered") Этот метод начинается с того, что пытается получить блокировку для доступа к данным и блокируется, пока не получит ее. Затем с помо щью метода dict.get(), которому вторым аргументом передается зна чение None, он получает сведения об автомобиле с указанным номером или None. Информация тут же копируется, и инструкция with завер шается. Этим обеспечивается максимально короткое время, в тече ние которого блокировка будет занята. Операция чтения не изменяет словарь, но, так как мы имеем дело с изменяемой коллекцией, впол не возможно, что какойнибудь другой поток в то же самое время по пробует изменить словарь – использование блокировки устраняет та кую опасность. Теперь, когда мы находимся вне области действия блокировки, у нас имеется объект с копией информации об автомоби ле (или None), с которой можно продолжить работу, не блокируя дру гие потоки. Сервер TCP 503 Все методы, выполняющие операции с регистрационной информаци ей, возвращают кортеж, первым элементом которого является логиче ский флаг – признак успешного выполнения операции, а количество и значения остальных элементов зависят от конкретного метода. Ни один из этих методов даже понятия не имеет и не делает никаких пред положений о данных, возвращаемых клиенту, кроме того, что эти дан ные представляют собой «кортеж, первым элементом которого являет ся логический флаг», так как все сетевые взаимодействия сосредото чены в методе handle(). def change_mileage(self, license, mileage): if mileage < 0: return (False, "Cannot set a negative mileage") with self.CarsLock: car = self.Cars.get(license, None) if car is not None: if car.mileage < mileage: car.mileage = mileage return (True, None) return (False, "Cannot wind the odometer back") return (False, "This license is not registered") В этом методе мы можем выполнить одну проверку, не прибегая к ис пользованию блокировки. Но если величина пробега неотрицательна, необходимо приобрести блокировку и получить ссылку на объект с ин формацией о соответствующем автомобиле, и если требуемый объект существует (например, если указан верный номер автомобиля), необ ходимо остаться в области действия блокировки, чтобы изменить ве личину пробега в соответствии с запросом клиента, в противном слу чае вернуть кортеж с сообщением об ошибке. Если для запрошенного номера автомобиля в словаре отсутствует информация (car == None), метод выходит из области видимости инструкции with и возвращает кортеж с сообщением об ошибке. На первый взгляд, проверку величины пробега можно выполнить на стороне клиента, чтобы уменьшить объем сетевого трафика – напри мер, клиент мог бы получать сообщение об ошибке при вводе отрица тельной величины пробега (или такая возможность просто предотвра щалась бы). Но даже если мы обяжем сторону клиента реализовать та кую проверку, мы попрежнему должны проверять данные на стороне сервера, так как нельзя быть полностью уверенным, что клиентская программа не содержит ошибок. И хотя программаклиент получает величину пробега автомобиля, чтобы использовать ее в качестве значе ния по умолчанию, мы не должны считать величину пробега, указан ную пользователем, верной (даже если она больше текущей величи ны), потому что какойнибудь другой клиент мог увеличить это значе ние к текущему моменту времени. Из всего этого следует, что оконча тельная проверка данных может выполняться только на стороне сервера и только под защитой блокировки. 504 Глава 10. Сети Метод change_owner() очень похож на предыдущий, поэтому мы не бу дем приводить его здесь. def new_registration(self, license, seats, mileage, owner): if not license: return (False, "Cannot set an empty license") if seats not in {2, 4, 5, 6, 7, 8, 9}: return (False, "Cannot register car with invalid seats") if mileage < 0: return (False, "Cannot set a negative mileage") if not owner: return (False, "Cannot set an empty owner") with self.CarsLock: if license not in self.Cars: self.Cars[license] = Car(seats, mileage, owner) return (True, None) return (False, "Cannot register duplicate license") И снова, прежде чем обратиться к словарю с регистрационными дан ными, мы можем выполнить множество проверок. Если все данные имеют допустимые значения, необходимо получить блокировку. Если номер автомобиля отсутствует в словаре RequestHandler.Cars (а он дол жен отсутствовать, так как при создании новой регистрационной за писи должен указываться незарегистрированный номер), создается новый объект Car и сохраняется в словаре. Все это должно выполнять ся под защитой блокировки, потому что мы не должны допускать воз можность добавления одного и того же номера автомобиля в момент времени между проверкой присутствия номера автомобиля в словаре RequestHandler.Cars и добавлением нового объекта Car в словарь. def shutdown(self, *ignore): self.server.shutdown() raise Finish() Если запрошена операция остановки сервера, вызывается метод shut down() сервера – это приведет к прекращению приема последующих за просов, но сам сервер продолжит обработку уже принятых запросов. После этого мы возбуждаем собственное исключение, чтобы известить метод handler(), что работу сервера следует прекратить. Это приведет к тому, что метод handler() вернет управление вызывающей програм ме, не посылая ответ клиенту. В заключение В этой главе было показано, что сетевые клиенты и серверы реализу ются довольно просто благодаря наличию в стандартной библиотеке языка Python сетевых модулей и модулей struct и pickle. В первом разделе мы разработали клиентскую программу и создали в ней единственную функцию handle_request(), способную отправлять Упражнения 505 на сервер и получать от сервера произвольные законсервированные объекты с данными в универсальном формате «длина плюс законсер вированный объект». Во втором разделе мы увидели, как можно соз дать подкласс сервера, используя классы из модуля socketserver, и как реализовать класс обработчика запросов для обслуживания запросов клиентов. Здесь также все сетевые взаимодействия были сосредоточе ны в единственном методе handle(), который принимает от клиентов и отправляет клиентам произвольные законсервированные объекты с данными. Модули socket и socketserver, а также многие другие модули из стан дартной библиотеки, такие как asyncore, asynchat и ssl, предоставляют намного больше функциональных возможностей, чем мы использова ли здесь. Но если окажется, что средств сетевых взаимодействий, имеющихся в стандартной библиотеке, недостаточно или они недоста точно высокоуровневые, то стоит обратить внимание сетевую платфор му Twisted, созданную сторонними разработчиками (www.twistedmat rix.com ), как на возможную альтернативу. Упражнения В упражнения предлагается модифицировать программы клиента и сервера, описанные в этой главе. Модификации не требуют ввода больших объемов программного кода, но требуют проявить долю вни мания, чтобы найти правильное решение. 1. Скопируйте программы car_registration_server.py и car_registrati on.py и измените их так, чтобы они использовали версию протокола обмена на сетевом уровне. Реализовать это можно, например, пере давая два целых числа (длина, версия протокола) вместо одного. Для этого потребуется добавить или изменить порядка десяти строк в функции handle_request() в клиентской программе, а также доба вить или изменить порядка шестнадцати строк в методе handle() в серверной программе, включая программный код, обрабатываю щий ситуацию, когда номер версии протокола не соответствует ожидаемому. 2. Скопируйте программу car_registration_server.py (или используйте ту, что была разработана в упражнении 1) и модифицируйте ее так, чтобы она выполняла новую операцию GET_LICENSES_STARTING_WITH. Операция должна принимать один строковый параметр. Метод, реализующий операцию, должен всегда возвращать кортеж из двух элементов (True, список номеров). Ситуация ошибки в данном слу чае невозможна, так как отсутствие совпадений не является ошиб кой, и в этом случае клиенту должны возвращаться значение True и пустой список. Извлечение номеров автомобилей (ключей словаря RequestHand ler.Cars ) должно выполняться под защитой блокировки, но все ос 506 Глава 10. Сети тальные действия должны выполняться за пределами блокировки, чтобы минимизировать время блокировки словаря. Один из эффек тивных способов поиска соответствующих номеров автомобилей за ключается в том, чтобы отсортировать список ключей, затем с по мощью модуля bisect отыскать первое совпадение и потом выпол нять итерации, начиная с найденного элемента. Другое возможное решение состоит в том, чтобы выполнить итерации по списку номе ров, выбирая те, что начинаются с указанной строки, возможно, с применением генератора списков. Помимо дополнительной инструкции import, необходимо будет до бавить пару строк программного кода, записывающего ссылку на реализацию операции в словарь Call. Размер метода реализации операции не будет превышать десятка строк. Это совсем несложно, но потребует проявить определенное внимание. Решение, исполь зующее модуль bisect, приводится в файле car_registration_server_ ans.py 3. Скопируйте программу car_registration.py (или используйте ту, что была разработана в упражнении 1) и модифицируйте ее так, чтобы задействовать достоинства нового сервера (car_registration_server_ ans.py ). Это подразумевает внесение изменений в функцию retrie ve_car_details() , чтобы в случае ввода неправильного номера авто мобиля запросить у пользователя начальные символы номерного знака и предложить ему выбрать номер из полученного списка. Ни же приводится пример сеанса взаимодействия пользователя с про граммой с использованием новой функции (сервер уже запущен, ме ню немного отредактировано, чтобы уместить его по ширине книж ной страницы, и ввод пользователя выделен жирным шрифтом): (C)ar (M)ileage (O)wner (N)ew car (S)top server (Q)uit [c]: License: |