后端编程Python3-网络编程(python做后端怎么样)
off999 2024-10-19 07:14 14 浏览 0 评论
本节是第五讲的第二十一小节,本节主要介绍Python的网络编程(包括客户端与服务端)。
网络(Networking)
借助于网络化,计算机程序可以彼此通信——即便运行在不同的机器上。对诸如 Web浏览器等程序,实际上这就是其功能实质所在;对其他程序,网络化则扩展了其功能,比如,远程操作或登录,或从其他机器取回数据(或为其提供数据)。大多数网络化程序的工作模式或者是点对点(相同程序运行在不同机器上)的,或者是更常见的客户端/服务器(客户端程序向服务器提交请求)模式。 在本章中,我们将创建一个基本的客户端/服务器模式的应用程序。这样的应用程序通常实现为两个分离的程序:服务器,等待来自客户端的请求并对其进行响应;一个或多个客户端,向服务器提交请求并处理服务器的响应信息。为保证这一模式顺利运作,客户端必须知道要连接的服务器在哪里,也就是服务器的IP (Internet协议)地 址与端口号*。此外,客户端与服务器在发送与接收数据时,都必须使用议定的协议, 协议中要使用双方都可以正确理解与处理的数据格式。 Python的底层socket模块(Python的所有高层网络功能模块都以此模块为基础) 同时支持IPv4地址与IPv6地址,也支持大多数通常使用的网络协议,包括UDP (用户数据报协议)与TCP (传输控制协议)。UDP是一个轻量级(但不是很可靠)的无连接协议,在这一协议中,数据是以离散的数据包(数据报)形式发送的,但并不能保证一定可以发送到目的地;TCP是一个可靠的、有连接的、面向流的协议,借助于 TCP,任意数量的数据都可以发送或接收——在发送端,socket负责将数据分解为合 适大小的数据块,以便可以正确发送;在接收端,socket负责将数据进行重组。 UDP通常用于对提供连续读数的仪器进行监控,这种情境下偶尔的数据丢失不会有很大的影响,比如,在音频流或视频流的传送中可以使用UDP——偶尔的帧丢失是可以接受的。FTP与HTTP都建立在TCP之上,客户端/服务器模式的应用程序通常也使用TCP,这是因为都需要面向连接的通信以及TCP提供的可靠性。在本章中,我们将开发一个客户端/服务器应用程序,因此也需要使用TCP。 另一个需要确定的问题是如何发送与接收数据,是以文本行的形式还是以二进制数据块的形式发送。如果是后者,具体采用的又是哪种数据格式?在本章中,我们使用的数据格式为二进制数据块,其中,最前面的4个字节用于表示后面数据的长度(编码为无符号整数,使用struct模块)其后跟随的数据为一个二进制pickle。 这种方法的好处在于,pickle中几乎可以存储任意数据,因此,对任意应用程序, 都可以使用相同的数据发送、接收代码;不足之处在于,客户端与服务器必须都能正确理解pickle,因此,都必须釆用Python进行编写或必须可以访问Python,比如, 在Java中使用Jython,在C++中使用Boost.Python。当然,通常的安全性衡量也适用于pickle的使用。
我们将使用的实例是一个汽车注册程序。在该程序中,服务器存放汽车注册的详细资料(汽车牌照、座位、里程、所有人),客户端可以取回汽车详细资料、改变汽车的里程数或所有人,或进行新的汽车注册。任意数量的客户端都可以使用,并且彼此不会发生阻塞——即便两个客户端同时访问服务器,这是因为服务器将每一个客户端请求分别分配给不同的线程进行处理。(我们将看到,使用不同的进程进行分别处理也一样方便。) 对本实例,为方便起见,我们将服务器与客户端在同一台机器上运行,这意味着我们使用“本地主机"作为IP地址(如果服务器运行在其他主机上,那么可以在命令行中将其地址传送给客户端;如果没有防火墙机制,那么也可以正常工作),端口号的选择也是任意的,这里选取的是9653。端口号应孩大于1023,通常在5001与32767之间 ——尽管数值上在65535之下的端口号数值都是有效的。 服务器可以接受5种类型的请求:GET_CAR_DETAILS、CHANGE_MILEAGE、 CHANGE_OWNER、NEW_REGISTRATION 与 SHUTDOWN,并对每种请求给出对应的响应信息,响应信息可以是被请求的数据、对请求操作的确认,或指明出现某种错误。
创建TCP客户端(Creating a TCP Client)
客户端程序名为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键接受默认信息。在上面的实例中,用户要求査看某特定汽车的详细资料,并更新自身的里程数。 同时运行的客户端可以有任意多个,在某用户退出其客户端程序时,服务器并不会受影响。如果服务器被终止运行,那么终止服务器的客户端将退出,所有其他客户端将收到“连接拒绝”错误消息,并在下次尝试访问服务器时结束。在更复杂的应用程序中,只有某些特定用户才可以终止服务器的运行(或许还需要在特定的机器上), 这里为了展示这种机制是如何实现的,我们在客户端程序中包含了这一功能。
#下面看一下实现代码,代码以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(),这与本书前面章节中开发的函数是类似的,将其部署在一个模块中是为了便于在不同的程序中重用。
由于大多数命令都以询问相关汽车的汽车牌照开始,因此为方便用户,程序将记录用户最近一次输入的汽车牌照,并将其作为默认值。用户做出选择之后,我们将调用相关函数传递汽车牌照,并期望每个函数返回其使用的汽车牌照。另外,由于循环是无限的,因此,程序必须由某个函数进行终止。下面我们进一步查看相关函数与代码。
def get_car_details(previous_license):
license, car = retrieve_car_details(previous_license)
if car is not None:
print("License: {0}\nSeats: {seats}\nMileage: {mileage}\n"
"Owner: {owner}".format(license, **car._asdict()))
return license
上面给出的这一函数用于获取某特定汽车的信息。由于大多数函数都需要从用户处请求汽车牌照,通常还需要一些汽车相关的其他数据以便于正常工作,因此我们提炼出这一功能,将其实现在retrieve_car_details()函数中---该函数返回一个二元组,其中包含用户输入的汽车牌照与一个指定的元组CarTuple, CarTuple用于存储汽车的座位、里程数与所有人(或者前一个汽车牌照,若输入的是无法识别的汽车牌照,则为None)。这里,我们只打印取回的信息,并返回汽车牌照作为默认值,以便下一个需要汽车牌照的函数使用。
def retrieve_car_detaiis(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()函数不知道、不关心其发送、接收的具体数据,只是纯粹地实现发送、接收数据的功能。
考虑这样一种情况,在汽车注册时,协议要求将需要服务器执行的操作名称作为第一个参数来发送,其后跟随的是任意相关的参数——这里只包括汽车牌照。服务器进行应答时,总是返回一个元组,其中第一个项目是一个布尔型标识success/failure。 如果该标志为False,那么返回的是一个二元组,其中第二个项目为一条错误消息;如果该标志为True,那么返回的可以是二元组(其中第二个项目为一条确认消息),也可以是n元组,其中第二个以及后续的项目为被请求的数据。 这里,如果汽车牌照无法识别,ok取值为False,就在data[0]中打印错误消息,并返回前一个未改变的汽车牌照;否则,就返回该汽车牌照(该汽车牌照现在将变为"前一个”汽车牌照)与来自列表data的CarTupIe (包括座位、里程数、所有人等信息)。
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() 需要调用handIe_request()来获取汽车的详细资料。这样做一方面是为了确认汽车牌照的有效与否,另一方面是为了获取当前的里程数并将其作为默认值。这里,返回的应答消息总是一个二元组,其中第二个项目或者是一条错误消息,或者为None。
这里没有给出函数change_owner()的代码,因为该函数在结构上与change_mileage() 是相同的;也不给出函数new_registration()的代码,因为该函数的不同之处仅在于开始处不取回汽车的详细资料(该函数处理的是一个新输入的汽车),另外不同的是要求用户输 入汽车的所有详细资料而不是仅仅改变某一项——所有这些都已有展示,并且与网络程序设计都没有关系。
def quit(*ignore):
sys.exit()
def stop_server(*ignore):
handle.request("SHUTDOWN", wait_for_reply=False)
sys.exit()
如果用户选择退出程序,就调用sys.exit()以便“干净地”终止程序。每一个菜单功能都将被前一个驾照调用,但这里我们不关心具体参数。我们不能编写一个def quit() ——这种做法将创建一个函数,该函数不需要参数,以前一个驾照对其进行调用时, 就会产生一个TypeError异常,并声称遇到了本不需要提供的参数。从上面的代码中可以看到,我们指定了一个*ignore参数,该参数可以接受任意数量的位置参数。ignore 这个名称对Python没有影响,只是用来通知maintainers参数被忽略。
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)
如果用户选择终止服务器,那么可以使用handle_request()来通知服务器,并规定不需要应答信息。数据发送之后,handle_request()会自动返回,而不会等待应答信息, 使用sys.exit()则保证“干净地”终止程序。 这一函数实现了客户端程序的所有网络处理功能。从上面的代码可以看到,首先创建了 struct.Struct,该变量的作用是以网络字节顺序存放一个无符号整数,之后创建了一个pickle,用于存放要通过网络传送的任意数据,函数本身不知道也不关心其中存放的是什么数据。要注意的是,上面代码中将pickle协议版本设置为3,这是为了确保客户端与服务器使用的是相同的pickle版本——即便客户端或服务器已经升级为运行不同的Python版本。 如果需要协议不会过时,就可以为其指定版本(就像处理二进制磁盘格式一样), 这可以在网络层或数据层实现。在网络层,可以传送两个无符号整数而不是一个,也就是长度与协议版本号。在数据层,可以遵循这样一个约定:pickle总是一个列表(或总是一个字典),首项目(或“版本”项目) 包含版本号。SocketManager 是一个自定义的上下文管理器,作用是提供一个socket 稍后将对其进行描述。socket.socket.sendall()方法的作用是发送所有收到的数据——如果必要, 可以在幕后进行多个socket.socket.send()调用。程序中发送的数据总是包含两个项目, 即pickle的长度以及pickle本身。如果wait_for_reply参数取值为False,就不需要等待应答消息并直接返回——上下文管理器可以确保socket在函数真正返回之前关闭。发送数据(并且需要应答信息)之后,调用socket.socket.recv()方法来获取应答信息。该方法一直阻塞,直至获取应答。在第一次调用的时候,请求的应答信息为4个字节,这4个字节代表的整数用来存储其后的应答pickle的大小。
我们使用struct.Struct 将这些字节拆分为size整数,之后创建一个空的bytearray,并取回最多4000字节到来的pickle。读入size字节后(或数据用完),我们跳出循环,并使用pickle.loads()函数(该函数接受一个bytes或bytearray对象)对数据进行unpickle,并返回处理后的数据。这种情况下,我们知道数据总是一个元组,因为这是与汽车注册服务器之间建立的协议,但handle_request()函数不知道也不关心数据是什么。 如果网络连接出错,比如,服务器没有运行或者连接由于某种原因失败,就会产生一个socket.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.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()__方法。 我们不必关心是否有意外产生(所以我们忽略意外参数),而只是关闭该socket。由于该方法返回None (在Boolean上下文中为False),因此任何异常将被传播(我们在 handle_request()中设计了适当的except块,因此,这种机制将正常工作)。
创建TCP服务器(Creating a TCP Server)
创建服务器的代码通常遵循同样的设计模式,因而,我们不必使用低层的socket 模块,可以使用高层的socket-server模块,该模块可以完成创建服务器所需要的对象。 我们所需要做的就是提供一个请求句柄类以及一个handle()方法,以便读取请求并进行应答。socket-server模块负责处理网络连接的相关操作,对来自客户端的每个请求进行响应,或者是串行处理,或者是将不同的请求分配给单独的线程或进程(这一切操作都是透明的,用户不需要为低层的细节而困扰)。 对于本应用程序而言,要创建的服务器为car_registration_server.py。该程序包含一个非常简单的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(C, 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对象。通常,服务器不会打印数据,这是因为,典型情况下,服务器是自动启动与终止的,并在后台运行,所以,服务器通常只是将其状态写入日志(使用logging模块)。这里,为了使实验测试更加清晰简单,我们在服务器启动与终止时打印了一条消息。 请求句柄类需要具备访问cars字典的能力,但我们不能将字典传递给某个实例, 这是因为服务器会为我们创建实例——以便处理每个请求,因此,我们将字典设置为 RequestHandler.Cars类变量,以便所有实例都可以对其进行存取。
我们创建了一个服务器实例,并向其传递了需要使用的IP地址与端口号,还创建了 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: {0}".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语句变为如下格式: with GzipManager(filename, "rb”) as fh: 该上下文管理器可以在任意Python 3.x环境下使用。但如果我们只是关心Python 3.1或后续版本,也可以简单地写为with gzip.open(...)as fh,因为从Python 3.1开始, gzip.open()函数支持上下文管理协议。save()函数(这里没有展示)在结构上与load()函数相同,不同之处在于此函数以二进制写模式打开文件,使用pickle.dump()保存数据,并且不返回任何对象。 class CarRegistrationServer(socketserver.ThreadingMixIn,socketserver.TCPServer): pass 上面给出的是完整的自定义服务器类。如果我们需要创建一个使用进程而不是线程的服务器,那么唯一需要做的改变是继承socketserver.ForkingMixIn类,而非 socketserver.ThreadingMixIn类。术语mixin通常用于描述那些专门设计为多继承的类。 socketserver模块的类可用于创建各种类型的自定义服务器,包括UDP服务器以及 UNIX TCP、UDP服务器,这是通过继承适当的基类对实现的。
要注意的是,我们使用的socketserver mixin类必须总是首先被继承,这是为了保证mixin 类的方法比第二个类的方法优先使用(对两个类都可以提供同一方法的情况),因为Python在基类中搜索方法时,其顺序是基类被指定的顺序,并使用首先找到的适当方法。 socket服务器创建一个请求处理程序(使用其被给定的类)来处理每一个请求。 我们自定义的RequestHandler类为每种可以处理的请求提供了一个方法,另外还必须有一个handle()方法,因为这是socket服务器使用的唯一方法。在查看这一方法之前, 我们先查看类声明以及类变量。
class RequestHandler(socketserver.StreamRequestHandler):
CarsLock = threading.Lock()
CallLock = threading.Lock()
Call = dict(
GET_CAR_DETAILS=(lambda self, *args: self.get_car_details(*args)),
CHANG E_MILEAGE=(lambda self, *args: self.change_mileage(*args)),
CHANG E_OWNER=(lambda self, *args: self.change_owner(*args)),
NEW_REGISTRATION=(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数据,但我们必须保证该数据不会同时被两个方法(来自两个不同的线程)存取,如果发生这种情况,那么字典将被损坏或者程序崩溃。为避免这种情况,我们使用一个锁类变量,以便确保在同一时刻只有一个线程存取Cars字典* (线程化,包括锁的使用,都在第10章中进行了讲述)。 Call字典是另一个类变量,该字典的每个键是服务器可以执行的操作名,值则为执行该操作的函数。我们不能直接使用方法,就像对客户端的菜单字典中的函数一样, 因为类这一层没有可用的self。我们使用的解决方案是,提供wrapper函数,在self 被调用时对其进行包裹,并依次调用适当的方法(带有给定的self与任何其他参数)。 一种替代的解决方案是在所有方法之后创建Call字典,这使得我们可以创建类似于 GET_CAR_DETAILS=get_car_details 这样的条目,并且 Python 有能力找到 get_car_ details()方法,因为Call字典是在所有方法定义之后创建的。我们使用了第一种方法, 因为这种方法更加显式,也不会对方法与字典创建的顺序进行约束。
Call字典只有在类创建后才会被读取,由于该字典是可变的,因此我们釆用双保险机制,并为其创建一个锁,以保证不会有两个线程同时对其进行存取。(同样地,由于GIL的作用,对CPython而言,这里的锁机制也并不是必需的。)
def handle(self):
SizeStruct = struct.Struct("!|")
size_data = self.rfile.read(SizeStruct.size)
size = SizeStruct.unpack(size_data)[0]
data = pickle.Ioads(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.wfiie.write(data)
在客户端提交请求的任意时刻都会创建一个线程与RequestHandler类的一个新实例,之后调用该实例的handle()方法。在该方法内部,来自于客户端的数据将从文件对象self.rfile读取,向客户端返回数据则可以通过写self.wfile对象实现——这两个对象都是由socketserver提供的,并打开以备使用。 struct.Struct用于整数字节计数,整数字节计数用于"length plus pickle”格式,我们使用这种格式在客户端与服务器之间交换数据。 我们首先读入4个字节,并将其拆分为size整数,以便获知发送给我们的pickle大小。之后读入size字节,并将其unpickle为data变量。读操作将一直阻塞,直至数据被读入。这里,我们知道data总是一个元组,其中第一项是被请求的操作,其他项则为参数,因为这是与汽车注册客户端之间建立的协议。 在try代码块内部,我们获取适合于执行请求操作的lambda函数。我们使用锁机制来保护对Call字典的存取,尽管这种做法可能过于谨慎。与以往一样,我们始终坚持在锁的作用范围之内做尽可能少的工作一这里仅做一个字典查询,以便获取对函数的引用。获取该函数后,就对其进行调用,将self作为第一个参数,data元组的余下部分作为其他参数。这里我们在进行函数调用,因此Python没有传递self。这并不会导致问题,因为我们会自己传递self。在lambda函数内部,传入的self用于以通常方式调用方法。也就是说, 执行调用self.method(*data[1:])。这里,method是与data[0]中给定的操作相对应的方法。
如果该操作是关机,那么shutdown()方法内将产生一个自定义的Finish异常。如果出现这种情况,我们就可以知道客户端无法获取应答信息,因此只是简单地返回。 但对于任何其他操作,我们对调用操作相应的方法(使用pickle协议3)产生的结果进行pickle,并写入pickle大小以pickle数据本身。
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),我们可以在适当的时间对其进 行处理,而不会阻塞任何其他线程。
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")
像所有汽车注册操作处理方法一样,这里也返回一个元组,其第一项是一个布尔型的成功/失败标记,其他项则是可变的。除“第一项是布尔型数据的元组"外,这些方法都不需要担心(甚至不需要知道)其数据是如何返回给客户端的,因为所有网络交互都封装在handle()方法中。 在这一方法中,我们可以进行检测,而不需要请求锁。如果mileage是一个非负值,我们就必须请求锁,并获取相关的汽车对象,如果获取了汽车对象(也就是说, 驾照是有效的),我们就必须在锁的作用范围内按照要求改变mileage—或返回一个错误元组。如果没有哪个汽车对象使用给定的驾照(car为None),我们就退出with语句,并返回一个错误元组。
好像在客户端进行了验证,就可以避免一些网络流量开销,比如,对里程数为负数的情况,客户端可以给出一条错误消息(或简单地阻止)。即使客户端应该进行验证, 我们仍然必须在服务器端进行检测,因为我们不能假定客户端肯定没有错误。并且, 尽管客户端获取汽车的mileage并将其用作默认的里程数,我们也不能假定用户输入 的里程数(即使大于当前里程数)是有效的,因为某些其他客户端可能同时提高了里程数。因此,我们只能在服务器端进行权威性的验证,并只在锁的作用范围内进行。
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() 如果操作是关机,我们就调用服务器的shutdown()方法—这将阻止服务器接受任何进一步的请求(如果服务器还在为现有的请求服务,则仍然可以运行)。之后,产生一个自定义异常,以便告知handler()已经结束这将使handler()返回,而不向客户端发送任何应答。
以上内容部分摘自视频课程05后端编程Python21网络编程,更多实操示例请参照视频讲解。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。
相关推荐
- 每天一个 Python 库:datetime 模块全攻略,时间操作太丝滑!
-
在日常开发中,时间处理是绕不开的一块,比如:生成时间戳比较两个时间差转换为可读格式接口传参/前端展示/日志记录今天我们就用一个案例+代码+思维导图,带你完全搞定datetime模块的用法!...
- 字节跳动!2023全套Python入门笔记合集
-
学完python出来,已经工作3年啦,最近有很多小伙伴问我,学习python有什么用其实能做的有很多可以提高工作效率增强逻辑思维还能做爬虫网站数据分析等等!!最近也是整理了很多适合零基...
- 为什么你觉得Matplotlib用起来困难?因为你还没看过这个思维导图
-
前言Matplotlib是一个流行的Python库,可以很容易地用于创建数据可视化。然而,设置数据、参数、图形和绘图在每次执行新项目时都可能变得非常混乱和繁琐。而且由于应用不同,我们不知道选择哪一个图...
- Python新手必看!30分钟搞懂break/continue(附5个实战案例)
-
一、跳转语句的使命当程序需要提前结束循环或跳过特定迭代时,break和continue就是你的代码急刹按钮和跳步指令。就像在迷宫探险中:break=发现出口立即离开continue=跳过陷阱继续前进二...
- 刘心向学(24)Python中的数据类(python中5种简单的数据类型)
-
分享兴趣,传播快乐,增长见闻,留下美好!亲爱的您,这里是LearningYard新学苑。今天小编为大家带来文章“刘心向学(24)Python中的数据类”欢迎您的访问。Shareinterest,...
- 刘心向学(25)Python中的虚拟环境(python虚拟环境安装和配置)
-
分享兴趣,传播快乐,增长见闻,留下美好!亲爱的您,这里是LearningYard新学苑。今天小编为大家带来文章“刘心向学(25)Python中的虚拟环境”欢迎您的访问。Shareinte...
- 栋察宇宙(八):Python 中的 wordcloud 库学习介绍
-
分享乐趣,传播快乐,增长见识,留下美好。亲爱的您,这里是LearingYard学苑!今天小编为大家带来“Python中的wordcloud库学习介绍”欢迎您的访问!Sharethefun,...
- AI在用|ChatGPT、Claude 3助攻,1分钟GET高颜值思维导图
-
机器之能报道编辑:Cardinal以大模型、AIGC为代表的人工智能浪潮已经在悄然改变着我们生活及工作方式,但绝大部分人依然不知道该如何使用。因此,我们推出了「AI在用」专栏,通过直观、有趣且简洁的人...
- 使用DeepSeek + Python开发AI思维导图应用,非常强!
-
最近基于Deepseek+PythonWeb技术开发了一个AI对话自动生成思维导图的应用,用来展示下如何基于低门槛的Python相关技术栈,高效结合deepseek实现从应用场景到实际应用的快速落地...
- 10幅思维导图告诉你 - Python 核心知识体系
-
首先,按顺序依次展示了以下内容的一系列思维导图:基础知识,数据类型(数字,字符串,列表,元组,字典,集合),条件&循环,文件对象,错误&异常,函数,模块,面向对象编程;接着,结合这些思维导图主要参考的...
- Python基础核心思维导图,让你轻松入门
-
Python基础核心思维导图【高清图文末获取】学习路线图就给大家看到这里了,需要的小伙伴下方获取获取方式看下方图片...
- Python基础核心思维导图,学会事半功倍
-
Python基础核心思维导图【高清图文末获取】学习路线图就给大家看到这里了,需要的小伙伴下方获取获取方式看下方图片...
- 硬核!288页Python核心知识笔记(附思维导图,建议收藏)
-
今天就给大家分享一份288页Python核心知识笔记,相较于部分朋友乱糟糟的笔记,这份笔记更够系统地总结相关知识,巩固Python知识体系。文末获取完整版PDF该笔记学习思维导图:目录内容展示【领取方...
- Python学习知识思维导图(高效学习)
-
Python学习知识思维导图python基础知识python数据类型条件循环列表元组字典集合字符串序列函数面向对象编程模块错误异常文件对象#python##python自学##编程#...
- 别找了!288页Python核心知识笔记(附思维导图,建议收藏)
-
今天就给大家分享一份288页Python核心知识笔记,相较于部分朋友乱糟糟的笔记,这份笔记更够系统地总结相关知识,巩固Python知识体系。文末获取完整版PDF该笔记学习思维导图:目录内容展示【领取方...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 每天一个 Python 库:datetime 模块全攻略,时间操作太丝滑!
- 字节跳动!2023全套Python入门笔记合集
- 为什么你觉得Matplotlib用起来困难?因为你还没看过这个思维导图
- Python新手必看!30分钟搞懂break/continue(附5个实战案例)
- 刘心向学(24)Python中的数据类(python中5种简单的数据类型)
- 刘心向学(25)Python中的虚拟环境(python虚拟环境安装和配置)
- 栋察宇宙(八):Python 中的 wordcloud 库学习介绍
- AI在用|ChatGPT、Claude 3助攻,1分钟GET高颜值思维导图
- 使用DeepSeek + Python开发AI思维导图应用,非常强!
- 10幅思维导图告诉你 - Python 核心知识体系
- 标签列表
-
- python计时 (54)
- python安装路径 (54)
- python类型转换 (75)
- python进度条 (54)
- python的for循环 (56)
- python串口编程 (60)
- python写入txt (51)
- python读取文件夹下所有文件 (59)
- java调用python脚本 (56)
- python操作mysql数据库 (66)
- python字典增加键值对 (53)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python qt (52)
- python人脸识别 (54)
- python斐波那契数列 (51)
- python多态 (60)
- python命令行参数 (53)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- centos7安装python (53)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)