day21-階段總結

死不悔改奇男子發表於2024-04-23

1. 知識點補充

1.1 併發程式設計 & 網路程式設計

從知識點的角度來看,本身兩者其實沒有什麼關係:

  • 網路程式設計,基於網路基礎知識、socket模組實現網路的資料傳輸。

  • 併發程式設計,基於多程序、多執行緒等 來提升程式的執行效率。

但是,在很多 “框架” 的內部其實會讓兩者結合起來,使用多程序、多執行緒等手段來提高網路程式設計的處理效率。

案例1:多執行緒socket服務端

基於多執行緒實現socket服務端,實現同時處理多個客戶端的請求。

  • 服務端

    import socket
    import threading
    
    
    def task(conn):
        while True:
            client_data = conn.recv(1024)
            data = client_data.decode('utf-8')
            print("收到客戶端發來的訊息:", data)
            if data.upper() == "Q":
                break
            conn.sendall("收到收到".encode('utf-8'))
        conn.close()
    
    
    def run():
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('127.0.0.1', 8001))
        sock.listen(5)
        while True:
            # 等待客戶端來連線(主執行緒)
            conn, addr = sock.accept()
            # 建立子執行緒
            t = threading.Thread(target=task, args=(conn,))
            t.start()
            
        sock.close()
    
    
    if __name__ == '__main__':
        run()
    
    
  • 客戶端

    import socket
    
    # 1. 向指定IP傳送連線請求
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8001))
    
    while True:
        txt = input(">>>")
        client.sendall(txt.encode('utf-8'))
        if txt.upper() == 'Q':
            break
        reply = client.recv(1024)
        print(reply.decode("utf-8"))
    
    # 關閉連線,關閉連線時會向服務端傳送空資料。
    client.close()
    

案例2:多程序socket服務端

基於多程序實現socket服務端,實現同時處理多個客戶端的請求。

  • 服務端

    import socket
    import multiprocessing
    
    
    def task(conn):
        while True:
            client_data = conn.recv(1024)
            data = client_data.decode('utf-8')
            print("收到客戶端發來的訊息:", data)
            if data.upper() == "Q":
                break
            conn.sendall("收到收到".encode('utf-8'))
        conn.close()
    
    
    def run():
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.bind(('127.0.0.1', 8001))
        sock.listen(5)
        while True:
            # 等待客戶端來連線
            conn, addr = sock.accept()
            
            # 建立了子程序(至少有個執行緒)
            t = multiprocessing.Process(target=task, args=(conn,))
            t.start()
            
        sock.close()
    
    
    if __name__ == '__main__':
        run()
    
  • 客戶端

    import socket
    
    # 1. 向指定IP傳送連線請求
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8001))
    
    while True:
        txt = input(">>>")
        client.sendall(txt.encode('utf-8'))
        if txt.upper() == 'Q':
            break
        reply = client.recv(1024)
        print(reply.decode("utf-8"))
    
    # 關閉連線,關閉連線時會向服務端傳送空資料。
    client.close()
    

1.2 併發和並行

如何來理解這些概念呢?

  • 序列,多個任務排隊按照先後順序逐一去執行。

  • 併發,假設有多個任務,只有一個CPU,那麼在同一時刻只能處理一個任務,為了避免序列,可以讓將任務切換執行(每個任務執行一點,然後再切換),達到併發效果(看似都在同時執行)。

    併發在Python程式碼中體現:協程、多執行緒(由CPython的GIL鎖限制,多個執行緒無法被CPU排程)。
    
  • 並行,假設有多個任務,有多個CPU,那麼同一時刻每個CPU都是執行一個任務,任務就可以真正的同時執行。

    並行在Python程式碼中的體現:多程序。
    

1.3 單例模式

在python開發和原始碼中關於單例模式有兩種最常見的編寫方式,分別是:

  • 基於__new__方法實現

    import threading
    import time
    
    class Singleton:
        instance = None
        lock = threading.RLock()
    
        def __init__(self):
            self.name = "武沛齊"
            
        def __new__(cls, *args, **kwargs):
    
            if cls.instance:
                return cls.instance
            with cls.lock:
                if cls.instance:
                    return cls.instance
                # time.sleep(0.1)
                cls.instance = object.__new__(cls)
            return cls.instance
        
        
    obj1 = Singleton()
    obj2 = Singleton()
    
    print(obj1 is obj2) # True
    
  • 基於模組匯入方式

    # utils.py
    
    class Singleton:
        
        def __init__(self):
            self.name = "武沛齊"
            
        ...
            
    single = Singleton()
    
    from xx import single
    
    print(single)
    
    from xx import single
    print(single)
    

2. 階段總結

image

3.考試題

  1. 簡述物件導向的三大特性
繼承,將多個子類中相同的方法放在父類中,子類可以繼承父類中的方法(提升重用性)
封裝,將多個資料封裝到一個物件; 將同類的方法編寫(封裝)在一個型別中。
多型,天然支援多型,崇尚鴨子模型,不會對型別進行限制,只要具備相應的屬性即可,例如:
    def func(arg):
        arg.send()
    不管arg是什麼型別,只要具備send方法即可。
  1. super的作用?
根據mro的順序,向上尋找類。
  1. 例項變數和類變數的區別?
例項變數,屬於物件,每個物件中都各自儲存各自的例項變數。
類變數,屬於類,在類中儲存。
  1. @staticmethod 和 @classmethod的區別?
@staticmethod,靜態方法;  定義時:可以有任意個引數;    執行時:類和物件均可以觸發執行。
@classmethod,類方法;     定義時:至少有一個cls引數;  執行時:類和物件均可以觸發執行,自動把當前類當做引數傳遞給cls。
  1. 簡述 __new____init__的區別?
__new__,構造方法,用於建立物件。
__init__,初始化方法,用於在物件中初始化值。
  1. 在Python中如何定義私有成員?
用兩個下劃線開頭。
  1. 請基於__new__ 實現一個單例類(加鎖)。
import threading


class SingleTon:
    instance = None
    lock = threading.RLock()  # 定義鎖

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __new__(cls, *args, **kwargs):
        if cls.instance:
            return cls.instance
        with cls.lock:
            if cls.instance:
                return cls.instance
            cls.instance = super().__new__(cls)
        return cls.instance


obj1 = SingleTon("武沛齊", 18)
print(obj1)

obj2 = SingleTon("alex", 23)
print(obj2)

  1. 比較以下兩段程式碼的區別

    class F1(object):
        def func(self,num):
            print("F1.func",num)
            
    class F2(F1):
        def func(self,num):
            print("F2.func",num)
            
    class F3(F2):
        def run(self):
            F1.func(self,1)
            
    obj = F3()
    obj.run()
    
    # 直接用F1類呼叫func方法,執行的就是F1類中的func方法,輸出為:F1.func 1
    
    class F1(object):
        def func(self,num):
            print("F1.func",num)
            
    class F2(F1):
        def func(self,num):
            print("F2.func",num)
            
    class F3(F2):
        def run(self):
            super().func(1)
            
    obj = F3()
    obj.run()
    
    # 用super呼叫func方法,會去找父類中的func方法,執行的就是F2類中的func方法,輸出為:F2.func 1
    
  2. 補充程式碼實現

    class Context:
        pass
    
    with Context() as ctx:
        ctx.do_something()
        
    # 請在Context類下新增程式碼完成該類的實現
    
     class Context:
         def __enter__(self):
             return self
         
         def __exit__(self, exc_type, exc_val, exc_tb):
             pass
         
         def do_something(self):
             print("啦啦啦啦啦啦")
     
     
     with Context() as ctx:
         ctx.do_something()
    
  3. 簡述 迭代器、可迭代物件 的區別?

迭代器,
    1.當類中定義了 __iter__ 和 __next__ 兩個方法。
    2.__iter__ 方法需要返回物件本身,即:self
    3. __next__ 方法,返回下一個資料,如果沒有資料了,則需要丟擲一個StopIteration的異常。

可迭代物件,在類中定義了 __iter__ 方法並返回一個迭代器物件。	
  1. 什麼是反射?反射的作用?
反射,透過字串的形式去操作物件中的成員。例如:getattr/setattr/delattr/hashattr
  1. 簡述OSI七層模型。
OSI七層模型分為:應用層、表示層、會話層、傳輸層、網路層、資料連路程、物理層,其實就是讓各層各司其職,完成各自的事。

以Http協議為例,簡述各層的職責:
	應用層,規定資料傳格式。
		"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n"
	表示層,對應用層資料的編碼、壓縮(解壓縮)、分塊、加密(解密)等任務。
		"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
	會話層,負責與目標建立、中斷連線。
	傳輸層,建立埠到埠的通訊,其實就確定雙方的埠資訊。
		資料:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
        埠:
            - 目標:80
            - 本地:6784
	網路層,標記IP資訊。
		資料:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
        埠:
            - 目標:80
            - 本地:6784
        IP:
            - 目標IP:110.242.68.3
            - 本地IP:192.168.10.1
	資料連路程,設定MAC地址資訊
		資料:"POST /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
        埠:
            - 目標:80
            - 本地:6784
        IP:
            - 目標IP:110.242.68.3(百度)
            - 本地IP:192.168.10.1
        MAC:
            - 目標MAC:FF-FF-FF-FF-FF-FF 
            - 本機MAC:11-9d-d8-1a-dd-cd
	物理層,將二進位制資料在物理媒體上傳輸。
  1. UDP和TCP的區別。
- UDP,UDP不提供可靠性,它只是把應⽤程式傳給IP層的資料包傳送出去, 但是並不能保證它們能到達⽬的地。 由於UDP在傳輸資料包前不⽤在客戶和伺服器之間建⽴⼀個連線, 且沒有超時重發等機制, 故⽽傳輸速度很快。常見的有:語音通話、視訊通話、實時遊戲畫面 等。
- TCP,在收發資料前,必須和對方建立可靠的連線,然後再進行收發資料。常見有:網站、手機APP資料獲取等。
  1. 簡述TCP三次握手和四次揮手的過程。
建立連線,三次握手:
    第一次:客戶端向服務端請求,傳送:seq=100(隨機值);  
    第二次:服務端接收請求,然後給客戶端傳送:seq=300(隨機值)、ack=101(原來客戶埠發來請求的seq+1)
    第三次:客戶接接收請求,然後給服務端傳送:seq=101(第2次返回的值)、 ack=301(第2次seq+1)
    
    第1、2次過程,客戶端傳送了資料seq,服務端也回給了seq+1,證明:客戶端可以正常收發資料。此時,服務端不知客戶端是否正常接收到了。
    第2、3次過程,服務端傳送了資料seq,客戶端也返回了seq+1,證明:服務端可以正常收發資料。

斷開連線,四次揮手:(任意一方都可以發起)
    第一次:客戶端向服務端發請求,傳送:<seq=100><ack=300>       (我要與你斷開連線)
    第二次:服務端接收請求,然後給客戶端傳送:<seq=300><ack=101>  (已收到,可能還有資料未處理,等等)
    第三次:服務端接收請求,然後給客戶端傳送:<seq=300><ack=101>  (可以斷開連線了)
    第四次:客戶端接收請求,然後給服務端傳送:<seq=101><ack=301>  (好的,可以斷開了)
  1. 簡述你理解的IP和子網掩碼。
子網掩碼用於給IP劃分網段,IP分為:網路地址 + 主機地址。
簡而言之,子網掩碼掩蓋的IP部分就是網路地址,未掩蓋就是主機部分。

例如:
    IP:192.168.1.199      11000000.10101000.00000001.11000111
    子網掩碼:255.255.255.0     11111111.11111111.11111111.00000000
此時,網路地址就是前24位 + 主機地址是後8位。你可能見過有些IP這樣寫 192.168.1.199/24,意思也是前24位是網路地址。
  1. 埠的作用?
在網路程式設計中,IP代指的是計算機,而埠則代指計算機中的某個程式。以方便對計算機中的多個程式程序區分。
  1. 什麼是粘包?如何解決粘包?
兩臺電腦在進行收發資料時,其實不是直接將資料傳輸給對方。

- 對於傳送者,執行 `sendall/send` 傳送訊息時,是將資料先傳送至自己網路卡的 寫緩衝區 ,再由緩衝區將資料傳送給到對方網路卡的讀緩衝區。
- 對於接受者,執行 `recv` 接收訊息時,是從自己網路卡的讀緩衝區獲取資料。

所以,如果傳送者連續快速的傳送了2條資訊,接收者在讀取時會認為這是1條資訊,即:2個資料包粘在了一起。

解決思路:
    雙方約定好規則,在每次收發資料時,都按照固定的 資料頭 + 資料 來進行處理資料包,在資料頭中設定好資料的長度。
  1. IO多路複用的作用是什麼?
可以監聽多個 IO物件 的變化(可讀、可寫、異常)。

在網路程式設計中一般與非阻塞的socket物件配合使用,以監聽 socket服務端、客戶端是否 (可讀、可寫、異常)

IO多路複用有三種模式:
    - select,限制1024個 & 輪訓的機制監測。
    - poll,無限制 & 輪訓的機制監測。
    - epoll,無限制 & 採用回撥的機制(邊緣觸發)。
  1. 簡述程序、執行緒、協程的區別。
執行緒,是計算機中可以被cpu排程的最小單元。
程序,是計算機資源分配的最小單元(程序為執行緒提供資源)。
一個程序中可以有多個執行緒,同一個程序中的執行緒可以共享此程序中的資源。

由於CPython中GIL的存在:
    - 執行緒,適用於IO密集型操作。
    - 程序,適用於計算密集型操作。

協程,協程也可以被稱為微執行緒,是一種使用者態內的上下文切換技術,在開發中結合遇到IO自動切換,就可以透過一個執行緒實現併發操作。


所以,在處理IO操作時,協程比執行緒更加節省開銷(協程的開發難度大一些)。
  1. 什麼是GIL鎖?其作用是什麼?
GIL, 全域性直譯器鎖(Global Interpreter Lock),是CPython直譯器特有一個玩意,讓一個程序中同一個時刻只能有一個執行緒可以被CPU呼叫。
  1. 程序之間如何實現資料的共享?
multiprocessing.Value 或 multiprocessing.Array
multiprocessing.Manager
multiprocessing.Queue
multiprocessing.Pipe
  1. 已知一個訂單物件(tradeOrder)有如下欄位:

    欄位英文名 中文名 欄位型別 取值舉例
    nid ID int 123456789
    name 姓名 str 張三
    items 商品列表 list 可以存放多個訂單物件
    is_member 是否是會員 bool True

    商品物件有如下欄位:

    欄位英文名稱 中文名 欄位型別 取值
    id 主鍵 int 987654321
    name 商品名稱 str 手機

    請根據要求實現如下功能:

    • 編寫相關類。
    • 建立訂單物件並根據關係關聯多個商品物件。
    • 用json模組將物件進行序列化為JSON格式(提示:需自定義JSONEncoder)。
import json


class ObjectJsonEncoder(json.JSONEncoder):
    def default(self, o):
        if type(o) in {Order, Goods}:
            return o.__dict__
        else:
            return o


class Order:
    def __init__(self, nid, name, is_member):
        self.nid = nid
        self.name = name
        self.items = []
        self.is_member = is_member


class Goods:
    def __init__(self, id, name):
        self.id = id
        self.name = name


od = Order(666, "武沛齊", True)
od.items.append(Goods(1, '汽車'))
od.items.append(Goods(2, '美女'))
od.items.append(Goods(3, '遊艇'))

data = json.dumps(od, cls=ObjectJsonEncoder)
print(data)
  1. 基於物件導向的知識構造一個連結串列。

    image

    注意:每個連結串列都是一個物件,物件內部均儲存2個值,分為是:當前值、下一個物件 。

class Node:
    def __init__(self, current, _next):
        self.current = current
        self.next = _next


# 手動建立連結串列
v4 = Node("火蜥蜴", None)
v3 = Node("女神", v4)
v2 = Node("武沛齊", v3)
v1 = Node("alex", v2)

# 根據列表建立連結串列
data_list = ["alex", "武沛齊", "女神", "火蜥蜴"]
root = None
for index in range(len(data_list) - 1, -1, -1):  # 從連結串列尾開始構建
    node = Node(data_list[index], root)
    root = node
  1. 讀原始碼,分析程式碼的執行過程。

    • socket服務端

      import socket
      import threading
      
      
      class BaseServer:
      
          def __init__(self, server_address, request_handler_class):
              self.server_address = server_address
              self.request_handler_class = request_handler_class
      
          def serve_forever(self):
              while True:
                  request, client_address = self.get_request()
                  self.process_request(request, client_address)
      
          def finish_request(self, request, client_address):
              self.request_handler_class(request, client_address, self)()
      
          def process_request(self, request, client_address):
              pass
      
          def get_request(self):
              return "傻兒子", "Alex"
      
      
      class TCPServer(BaseServer):
          address_family = socket.AF_INET
      
          socket_type = socket.SOCK_STREAM
      
          request_queue_size = 5
      
          allow_reuse_address = False
      
          def __init__(self, server_address, request_handler_class, bind_and_activate=True):
              BaseServer.__init__(self, server_address, request_handler_class)
              self.socket = socket.socket(self.address_family, self.socket_type)
              self.server_bind()
              self.server_activate()
      
          def server_bind(self):
              self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
              self.socket.bind(self.server_address)
              self.server_address = self.socket.getsockname()
      
          def server_activate(self):
              self.socket.listen(self.request_queue_size)
      
          def get_request(self):
              return self.socket.accept()
      
          def close_request(self, request):
              request.close()
      
      
      class ThreadingMixIn:
          def process_request_thread(self, request, client_address):
              self.finish_request(request, client_address)
              self.close_request(request)
      
          def process_request(self, request, client_address):
              t = threading.Thread(target=self.process_request_thread, args=(request, client_address))
              t.start()
      
      
      class ThreadingTCPServer(ThreadingMixIn, TCPServer):
          pass
      
      
      class BaseRequestHandler:
          def __init__(self, request, client_address, server):
              self.request = request
              self.client_address = client_address
              self.server = server
              self.setup()
      
          def __call__(self, *args, **kwargs):
              try:
                  self.handle()
              finally:
                  self.finish()
      
          def setup(self):
              pass
      
          def handle(self):
              pass
      
          def finish(self):
              pass
      
      
      class MyHandler(BaseRequestHandler):
          def handle(self):
              print(self.request)
              self.request.sendall(b'hahahahah...')
      
      
      server = ThreadingTCPServer(("127.0.0.1", 8000), MyHandler)
      server.serve_forever()
      
      
  • socket客戶端

      import socket
    
      # 1. 向指定IP傳送連線請求
      client = socket.socket()
      client.connect(('127.0.0.1', 8000)) # 向服務端發起連線(阻塞)10s
    
      # 2. 連線成功之後,傳送訊息
      client.sendall('hello'.encode('utf-8'))
    
      # 3. 等待,訊息的回覆(阻塞)
      reply = client.recv(1024)
      print(reply)
    
      # 4. 關閉連線
      client.close()
    

    image

  1. 請自己基於socket模組和threading模組實現 門票預訂 平臺。(無需考慮粘包)

    • 使用者作為socket客戶端

      • 輸入景區名稱,用來查詢景區的餘票。
      • 輸入景區名稱-預訂者-8,用於預定門票。
    • socket服務端,可以支援併發多人同時查詢和購買。(為每個客戶度建立一個執行緒)。

      • 服務端資料儲存結構如下:

        db
        ├── tickets
        │   ├── 歡樂谷.txt # 內部儲存放票數量
        │   ├── 迪士尼.txt
        │   └── 長城.txt
        └── users
            ├── alex.txt # 內部儲存次使用者預定記錄
            └── 武沛齊.txt
            
        # 注意:當使用者預定門票時,放票量要減去相應的數量(變為0之後,則不再接受預定)。
        

        image

        image

參考程式碼
server.py

import os
import socket
import threading
import datetime
import time

BOOKING_LOCK = threading.RLock()

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TICKETS_PATH = os.path.join(BASE_DIR, "db", 'tickets')
USERS_PATH = os.path.join(BASE_DIR, "db", 'users')


def search(conn, name):
    """
    查詢
    :param conn: 客戶端連線物件
    :param name: 景區名稱
    """
    file_name = "{}.txt".format(name)
    file_path = os.path.join(TICKETS_PATH, file_name)

    if not os.path.exists(file_path):
        conn.sendall("暫不支援此景區{}的預定。".format(name).encode('utf-8'))
        return

    with open(file_path, mode='r', encoding='utf-8') as file_object:
        count = int(file_object.read().strip())

    conn.sendall("景區{}剩餘票數為:{}。".format(name, count).encode('utf-8'))


def booking(conn, name, user, count):
    """
    預定
    :param conn: 客戶端連線物件
    :param name: 景區名稱
    :param user: 預訂者
    :param count: 預定數量
    """
    file_name = "{}.txt".format(name)
    file_path = os.path.join(TICKETS_PATH, file_name)
    if not os.path.exists(file_path):
        conn.sendall("暫不支援此景區{}的預定。".format(name).encode('utf-8'))
        return

    if not count.isdecimal():
        conn.sendall("預定數量必須是整型。".encode('utf-8'))
        return

    booking_count = int(count)
    if booking_count < 1:
        conn.sendall("預定數量至少1張。".encode('utf-8'))
        return

    # 執行緒鎖
    BOOKING_LOCK.acquire()

    with open(file_path, mode='r', encoding='utf-8') as file_object:
        count = int(file_object.read().strip())

    if booking_count > count:
        conn.sendall("預定失敗,景區{}剩餘票數為:{}。".format(name, count).encode('utf-8'))
        return

    count = count - booking_count
    with open(file_path, mode='w', encoding='utf-8') as file_object:
        file_object.write(str(count))

    user_file_name = "{}.txt".format(user)
    user_path = os.path.join(USERS_PATH, user_file_name)
    with open(user_path, mode='a', encoding='utf-8') as file_object:
        line = "{},{},{}\n".format(datetime.datetime.now().strftime("%Y-%m-%d"), name, booking_count)
        file_object.write(line)

    # 人為讓預定時間久一點
    time.sleep(5)

    conn.sendall("預定成功".encode('utf-8'))
    BOOKING_LOCK.release()


def task(conn):
    """ 當使用者連線成功後,處理客戶端的請求 """
    while True:
        client_data = conn.recv(1024)
        if not client_data:
            print("客戶端失去連線")
            break

        data = client_data.decode('utf-8')
        if data.upper() == "Q":
            print("客戶端退出")
            break

        data_list = data.split("-")

        if len(data_list) == 1:
            search(conn, *data_list)
        elif len(data_list) == 3:
            booking(conn, *data_list)
        else:
            conn.sendall("輸入格式錯誤".encode('utf-8'))
    conn.close()


def initial_path():
    """ 初始化檔案路徑 """
    path_list = [TICKETS_PATH, USERS_PATH]
    for path in path_list:
        if os.path.exists(path):
            continue
        os.makedirs(path)


def run():
    """ 主函式 """

    initial_path()

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
        # 等待客戶端來連線
        conn, addr = sock.accept()

        t = threading.Thread(target=task, args=(conn,))
        t.start()

    sock.close()


if __name__ == '__main__':
    run()

client.py

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8001))
print("門票預定系統")
while True:
    txt = input(">>>")  # 景區  or  景區-使用者-asd
    client.sendall(txt.encode('utf-8'))
    if txt.upper() == 'Q':
        break
    reply = client.recv(1024)
    print(reply.decode("utf-8"))

client.close()

相關文章