[走近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文件中。

發佈了4 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章