`
searun
  • 浏览: 173993 次
  • 性别: Icon_minigender_1
  • 来自: 合肥
社区版块
存档分类
最新评论

[走近Python 3编程] 10. 网络

阅读更多

即使程序运行在不同的机器上,网络也可以使得它们互相通信。对于某些程序如 Web 浏览器,这是一个基本要求。除此之外,还可以有更多的功能,如远程操作或者记录获取或者提供数据给其它机器。当前大部分的网络应用都是运行在 P2P 模式(不同的机器上运行着同样的程序)或者更普遍的客户端 / 服务器端模式(客户端发送请求给服务器)。

在这一章中,我们将创建一个基本的客户机/服务器应用程序。这类应用通常都实现成两个单独的应用:一台服务器的等待和响应要求,一个或多个客户端发送请求到服务器,并作出对服务器的响应。

要做到这一点,客户端必须知道如何连接到服务器,也就是服务器的IP (互联网协议)地址和端口号(机器当然可以使用其他协议,例如使用 bonjour API 。可以从 pypi.python.org/pypi 中找到合适的模块)。另外,客户端和服务器端必须发送和接收数据规定好的双方都能理解的协议数据。

Python 的低级 socket 模块( Python 的高级网络模块都是基于此模块构建的)支持 IPv4 IPv6 地址。同样的,也支持许多广泛使用的协议,包括 UDP User Datagram Protocol ,一个轻量级的但是并不可靠的非连接协议,采用数据报传递数据,并不保证数据的可靠性)和 TCP Transmission Control Protocol ,一个可靠的面向连接的基于流的协议)。在 TCP 中,任意大小的数据都可以得到可靠传输—— socket 可以将数据分解成足够传输的大小,而在另一段对其进行重建。

还有一个需要做的决定是发送和接收数据的时候是采用文本传输还是采用二进制数据传输,如果是后者的话,则要决定采用什么样的形式。在本章中我们采用块结构,其中前四个字节(无符号整数)为接下来数据的长度,而接下来的数据则被封装成二进制 pickle 。这种方案的好处是任何的应用都可以使用同样的发送和接收代码,因为几乎所有的数据都可以保存在 pickle 中。而缺点就是客户端和服务器端都需要知道 pickle ,所以它们必须使用 Python 书写,或者是能够通过 Python 访问。例如,在 Java 中使用 Jython ,或者是在 C++ 中使用 Boost.Python 。当然, pickle 也需要考虑安全性。

这里我们将要使用的例子是汽车注册程序。服务器端包含有注册的详细信息。而客户端则可以获取汽车的详细信息,并修改汽车的所有者等,甚至可以创建一个新的注册。服务器端支持任意多的客户端,即使是同时访问也不会拒绝任何请求。这是因为服务器将每个请求分配给不同的线程(我们也可以看到如何使用不同的进程来完成)。

为了示例,服务器端和客户端将运行在同一台机器上。这意味着我们可以使用“ localhost ”作为 IP 地址(当服务器端运行在其它机器上时当然可以使用其 IP 地址,如果没有防火墙的话,应该可以操作)。同样的,我们选择了一个端口号 9653 。端口号最好大于 1023 ,一般位于 5001 32767 之间,尽管到 65535 之间的端口号都是合法的。

服务器的可以接受五种类型的请求:GET_CAR_DETAILS, HANGE_MILEAGE, CHANGE_OWNER, NEW_REGISTRATION SHUTDOWN,每种类型都有一个特定的回应。回应一般是请求数据或者是请求的响应,或者表示一个错误。

 

建立 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 接受默认值。这里用户请求一个特定汽车的详细信息并更新了其里程。由于可能会有许多客户端运行,当用户退出后,服务器端不应该受到影响。如果服务器端停止,客户端也会停止,而其他的客户端则会收到一个“ 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 列表是一个全局数据,每一项包含有两项,如["localhost", 9653],表示 IP 地址和端口号。这里的 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: {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()函数并不知道或者关心它所发送和接收的数据,仅仅是提供了网络服务。

在这个汽车注册程序中,我们使用的协议是总是将操作名称作为第一个参数,然后是相关的参数(在这里就是执照信息)。而服务器端总是返回一个响应的二元组,其中第一个参数是布尔值,表明成功或失败。如果此标志位为 False ,则第二个参数为错误信息。如果为 True ,则第二个数据要么为一个确认信息,要么是一个包含有请求响应数据的多元组。

所以,当执照信息不可识别的时候, ok 值为 False ,并且打印出 data[0] 中的错误信息,同时返回上次未改变的执照信息。否则,我们将得到执照信息(同时这也成为了上一次执照信息),以及由数据(包含座位数、里程数和所有者)组成的 CarTuple

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()是类似的,除了在最前面会更新某部分详细信息。实际上,这里有两个网络调用,
r etrieve_car_details() 调用handle_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 变量名没有任何实际的作用,而仅仅表示这里将有参数会被忽略。

如果用户选择停止服务器,我们可以使用handle_request()通知服务器端,并指明不需要返回值。当数据发送后,handle_request() 函数将不用等待响应而直接返回,并使用 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)

这个函数提供了客户端的所有网络处理功能。此函数首先创建了一个 struct.Struct 结构,按照网络字节顺序保存了一个无符号整型数,然后创建了一个 pickle ,包含有所有的数据。函数不知道或者关心这些数据是什么。需要注意的是,这里我们明确制定了 pickle 的版本号为 3 。这保证了客户端和服务器端都采用同样的 pickle 版本,即使是两端使用不同的 Python 版本。

如果希望我们的协议在将来也可以继续用,可以对其使用版本号(就像我们对二进制磁盘格式那样)。这可以在网络层完成,也可以在数据层完成。在网络层上,我们可以传递两个无符号整数,分别表示长度和版本号。在数据层上,可以讲 pickle 数据转换成一个列表(字典),从而其第一个元素(或者“ version ”元素)包含着版本号。在练习中你将试着完成此任务。

SocketManager 是一个自定义的上下文管理器,提供了可以使用的 socket ,具体将在后面进行介绍。socket.socket.sendall()方法将发送所有的数据,如果需要的话将在后台生成多个socket.socket.send()调用。我们总是会发送两个元素: pickle 的长度及其自身。如果wait_for_reply argument变量为 False ,则不需要等待响应而直接返回。上下文管理器将保证在函数返回之前关闭 socket

在发送数据(需要接收响应)之后,我们调用socket.socket.recv()方法来获取响应。此方法将在收到报文之前阻塞。在第一次调用时,将请求四个字节的数据,表明接下来的 pickle 长度。我们使用struct.Struct将此字节转换成整数。接着创建一个字节数组,并接收传递过来的 pickle ,最多可以接收 4000 字节数。当读取了数据(或者数据流结束)后,则跳出循环使用pickle.loads()函数(使用字节或者字节数组对象)接封装数据,并将其返回。在这里,通过和服务器端的协议,我们知道数据总是一个元组,但是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 (布尔值判断为 False ),任何的异常都会被传播。这使得我们可以在handle_request()函数中放入一个合适的异常处理块来处理异常。

 

建立 TCP 服务器端

由于创建服务器的过程基本上的流程都是一样的,所以我们这里没有采用低级的 socket 模块,而采用了提供有相关功能的高级模块。我们所要做的就是通过 handle() 函数提供一个请求处理器,读取请求并返回响应。socketserver模块提供了通信功能,为每个请求服务,连续的处理请求或者将请求交给每个单独的线程或进程,而其本身对用户透明,这使得我们不用为底层的处理所困扰。

这个应用中的服务器端为car_registration_server.py(服务器端首次运行的时候, Windows 可能会弹出一个对话框,点击“取消阻止”让其继续运行)。此程序中保存了有个简单的 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 对象。一般的,服务器在启动和结束的时候不会打印任何信息,而是运行在后台,所以一般需要通过写记录文件来报告(如使用 logging 模块)。这里我们选择在启动和退出的时候打印一条信息,使得我们的测试要容易一些。

我们创建的请求处理类需要能够访问 cars 字典对象,但是却不能将此对象传递给某个实例。这是因为服务器端是需要处理所有请求的。所以这里我们设置字典对象为RequestHandler.Cars类变量名,因为它是可以访问到所有实例所。

我们使用服务器端将工作的地址和端口号以及RequestHandler类对象(不是一个实例)创建了一个 server 实例对象。这里的地址使用了一个空字符串,用来表示任何可以访问的 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)

加载的代码很简单,这是因为我们使用 Python 标准库中的 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:

save() 函数(这里没有给出)和 load() 函数是类似的。唯一不同的是我们使用写模式和二进制打开,并使用pickle.dump()保存数据,而不返回任何值。

class CarRegistrationServer(socketserver.ThreadingMixIn,
                            socketserver.TCPServer): pass

这是一个完整的自定义服务器类。如果我们需要使用创建进程而不是线程的方式,则可以将继承于socketserver.ThreadingMixIn类改为继承自socketserver.ForkingMixIn类。术语 mixin 一般用来描述设计成可以多种继承的类。 socketserver 模块中的类可以用来创建一系列的自定义服务器端类,包括 UDP TCP 服务器。所需要做的仅仅是继承自相应的基类。

需要注意的是 socketserver 混合类需要首先被继承。这保证了当两个类中含有相同函数的时候,混合类中的函数优先被调用。这是因为 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)),
            CHANGE_MILEAGE=(
                    lambda self, *args: self.change_mileage(*args)),
            CHANGE_OWNER=(
                    lambda self, *args: self.change_owner(*args)),
            NEW_REGISTRATION=(
                    lambda self, *args: self.new_registration(*args)),
            SHUTDOWN=lambda self, *args: self.shutdown(*args))

因为我们使用的是 TCP 服务器,这里我们创建了一个socketserver.StreamRequestHandler子类。如果是 UDP 服务器的话,则可以使用socketserver.DatagramRequestHandler,或者是通过使用server.BaseRequestHandler类进行低层次的访问。

RequestHandler.Cars字典是我们加在 main() 函数中的一个类变量,包含了所有的注册数据。为此对象增加属性(如类和实例)可以在类之外完成(如这里的 main() 函数),而不拘于其形式(对象中含有 __dict__ ),同时这也是非常简单的。我们知道类依赖于这个变量后,可以通过添加 Cars=None 来将此变量出现的地方注释掉。

尽管所有请求处理器都需要访问 Cars 数据,我们需要保证此数据不会同时被两个线程中的方法所调用。否则,这个字典数据有可能会损坏,甚至有可能崩溃。为了避免这种问题,我们使用了锁的类变量,用来保证在同一时间只会有一个线程访问 Cars 字典数据。( GIL 全局锁变量保证了对 Cars 字典对象的访问是同步的,但是在前面解释过,在 CPython 的实现中我们还不能利用这点)。关于线程和锁的用法,请参见第 9 章。

这里的 Call 字典对象是另一个类变量。键名为服务器可以采取的动作,键值则为具体执行此动作的函数名。我们不能像在客户端的用户菜单中那样直接使用方法,因为在类级别上没有 self 变量。我们的解决方案是提供包裹函数,这样当它们被调用的时候能够获取到 self 变量,然后依次调用给定 self 和其他参数的方法。另外一种方案是在所有的方法后创建 Call 字典对象。这将创建一些如GET_CAR_DETAILS=get_car_details的条目,由于在方法定义后字典数据才创建,所以 Python 可以根据此找到get_car_details()方法。我们采用了第一种方法,因为这种方法更直观,而且不需要计较字典创建的先后问题。

尽管 Call 字典对象是在类建立后才访问,但是因为它是 mutable 的数据,所以我们为了安全也为它创建了一个锁,用来保证同一时间不会有两个线程进行访问(同样的,因为 GIL 的原因,在 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对象。这两个对象都是由 socketserver 提供的,已经打开并且可以使用。

struct.Struct用于处理整数字节数,这在客户端和服务器端之间的“长度和封装数据”格式中是需要的。

我们首先读取前四个字节(有符号整型数)信息,从而知道发送的 pickle 的字节数。然后读取相应的字节数并将其解封装成数据。读取过程将阻塞知道读到数据。这里我们知道数据总是一个元组,其中第一个元素为请求动作,而另外一个则为其参数。这就是我们在前面和客户端之间建立的协议。

Try 块中我们可以获取特定请求的 lambda 函数。这里我们使用了锁来保护对 Call 字典对象的访问,尽管这看起来有点过于小心了。和以前一样,在锁范围中我们尽可能的少做事情,在这里我们仅仅做了一个字典查找来获取函数引用。然后我们调用此函数, self 作为第一个参数,而元组中剩下的数据作为其他的参数。这里我们使用了函数调用,所以并没有传递 self 。因为在 lambda 函数中传递了 self ,并使用普通方式调用了函数,所以这里没有传递 self 也没有关系。

如果动作为关闭, shutdown() 方法中的一个自定义 Finish 异常将会触发。因为我们知道客户端获取不到任何响应,所以这里直接返回。但是对于其他动作,我们封装调用方法的结果(使用 pickle 协议版本 3 ),然后写入 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 car 马上被拷贝被退出 with 语句。这保证了锁在尽可能短的时间内。尽管读取不会改变读取的信息,因为我们使用的是一个可能会在其它线程中改变的字典数据,所以这里使用锁来防止意外。在锁控制范围之外我们得到了 car 对象的拷贝(或者为 None ),我们可以对其进行处理而不用阻塞其它线程。

就像所有的汽车注册动作响应方法一样,我们返回一个元组,其中第一个元素表示成功或者失败,而其他参数则根据第一个元素有所不同。这些方法不关心甚至是不知道数据是如何返回给客户端的(也就知道第一个元素是布尔值的元组),因为网络处理都被封装在了 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 语句并返回一个错误元组。

好像在客户端进行检查能够完全的避免某些网络流量,例如客户端可以在负数里程数的时候给出一个错误信息,或者简单的阻止此值。尽管客户端应该这样做,但是我们还是要在服务器端对数据进行检查,而不能假设客户端是没有 BUG 的。尽管客户端获取汽车的里程数作为默认值,我们也不能假设用户的输入是合法的(即使是大于现在的里程数),因为可能会有些客户端已经增加了里程数值。所以服务器端在一个锁范围内对数据进行了显示的检查。

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中检查执照的存在性和向字典中增加一辆新的汽车信息之间使用相同的执照信息来增加一辆汽车。

    def shutdown(self, *ignore):
        self.server.shutdown()
        raise Finish()

如果动作为关闭则调用服务器端的 shutdown() 方法,此方法将阻止接受任何请求,而已有的请求则会继续服务。然后我们触发一个自定义异常,通知 handler() 函数服务器端处理已经结束,这将使得 handler() 函数不向客户端发送响应而直接返回。

 

小结

本章演示了如何通过使用 Python 的标准库中的网络模块以及 struct pickle 模块创建网络客户端和服务器端。

在第一节中我们开发了一个客户端程序,并使用了一个函数handle_request(),使用“长度和封装数据”的格式来从服务器端接收和发送数据。在第二节中则演示了如何使用 socketserver 模块中的类创建一个服务器子类,以及怎样实现一个服务器处理器来处理客户端的请求。这里的网络交互核心是一个函数 handle() ,可以从客户端接收和发送数据。

本章介绍的 socket 以及socketserver模块,以及标准库中的其他网络模块,如 asyncore asynchat ssl 等,提供了我们这里没有使用的更多功能。但是如果觉得标准库提供的网络接口并不够,或者是不够高级,这时候可以考虑看看第三方的库,如 Twisted 网络框架( http://www.twistedtrix.com )。

 

练习

1. 练习中包含了对本章中客户端和服务器端的修改。改动并不是很大,但是还是需要花费一些时间来写对的。

 

拷贝 c ar_registration_server.py 和car_registration.py文件,然后修改它们使得其通过网络层的版本来交换数据。这可以通过在 struct 数据中包含两个证书来实现。

这将在客户端程序中的handle_request()函数中增加和修改大概十行代码,以及服务器端程序中 handle() 方法的大概十六行代码(包括对版本号不匹配时候的处理)。

这个和下面练习的解答包含在car_registration_ans.py 和car_registration_server_ans.py文件中。

 

 

2. 拷贝一下car_registration_server.py代码,并在其增加一个动作——GET_LICENSES_STARTING_WITH。这个动作将接受一个字符串的参数。 此参数返回一个二元组(布尔值 True ,执照列表)。需要注意的是,这里没有错误( False )的情况,因为不匹配的情况不是错误返回空列表即可。

在锁的范围内获取执照信息(RequestHandler.Cars字典的键),而将其他工作放入锁外面,以便尽可能的减小阻塞时间。找到匹配执照的高效率方法是对键值进行排序,然后可以使用 bisect 模块来找到第一个匹配执照并从那里开始重复。另一种可能的方法是对执照信息循环,然后选取特定字符串开始的执照信息,可能需要使用列表推导(list comprehension)。

除了导入部分, Call 字典数据需要为此动作增加一些代码。而动作的具体实现可以在十行代码中完成。这并不难,但需要细心。一种使用 bisect 的方法在car_registration_server _ans.py文件中提供。

 

 

3. 拷贝car_registration.py 代码,使得其增加对新服务器(car_registration_server_ans.py)的支持。这意味着对retrieve_car_details()函数的修改,使得用户在输入非法执照信息的时候,能够获取一个执照信息列表。下面是一个新函数的操作示例(服务器已经运行,并且菜单也做了部分调整,另外,用户的输入采用加黑):

(C)ar  (M)ileage  (O)wner  (N)ew car  (S)top server  (Q)uit [c]:
License: da 4020
License: DA 4020
Seats:   2
Mileage: 97181
Owner:   Jonathan Lynn
(C)ar  (M)ileage  (O)wner  (N)ew car  (S)top server  (Q)uit [c]:
License [DA 4020]: z
This license is not registered Start of license: z
No licence starts with Z Start of license: a
(1) A04 4HE
(2) A37 4791
(3) ABK3035
Enter choice (0 to cancel): 3
License: ABK3035
Seats:   5
Mileage: 17719
Owner:   Anthony Jay

这里的修改将删除 1 行,并增加大概二十多行。这里有点棘手,因为用户必须可以跳出或者继续下一步骤。确保你的新函数在所有条件下都适用(没有执照信息,一个执照信息或者是更多的执照信息)。一个实现的方案在car_registration_ans.py文件中。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics