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. 階段總結
3.考試題
- 簡述物件導向的三大特性
繼承,將多個子類中相同的方法放在父類中,子類可以繼承父類中的方法(提升重用性)
封裝,將多個資料封裝到一個物件; 將同類的方法編寫(封裝)在一個型別中。
多型,天然支援多型,崇尚鴨子模型,不會對型別進行限制,只要具備相應的屬性即可,例如:
def func(arg):
arg.send()
不管arg是什麼型別,只要具備send方法即可。
- super的作用?
根據mro的順序,向上尋找類。
- 例項變數和類變數的區別?
例項變數,屬於物件,每個物件中都各自儲存各自的例項變數。
類變數,屬於類,在類中儲存。
- @staticmethod 和 @classmethod的區別?
@staticmethod,靜態方法; 定義時:可以有任意個引數; 執行時:類和物件均可以觸發執行。
@classmethod,類方法; 定義時:至少有一個cls引數; 執行時:類和物件均可以觸發執行,自動把當前類當做引數傳遞給cls。
- 簡述
__new__
和__init__
的區別?
__new__,構造方法,用於建立物件。
__init__,初始化方法,用於在物件中初始化值。
- 在Python中如何定義私有成員?
用兩個下劃線開頭。
- 請基於
__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)
-
比較以下兩段程式碼的區別
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
-
補充程式碼實現
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()
-
簡述 迭代器、可迭代物件 的區別?
迭代器,
1.當類中定義了 __iter__ 和 __next__ 兩個方法。
2.__iter__ 方法需要返回物件本身,即:self
3. __next__ 方法,返回下一個資料,如果沒有資料了,則需要丟擲一個StopIteration的異常。
可迭代物件,在類中定義了 __iter__ 方法並返回一個迭代器物件。
- 什麼是反射?反射的作用?
反射,透過字串的形式去操作物件中的成員。例如:getattr/setattr/delattr/hashattr
- 簡述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
物理層,將二進位制資料在物理媒體上傳輸。
- UDP和TCP的區別。
- UDP,UDP不提供可靠性,它只是把應⽤程式傳給IP層的資料包傳送出去, 但是並不能保證它們能到達⽬的地。 由於UDP在傳輸資料包前不⽤在客戶和伺服器之間建⽴⼀個連線, 且沒有超時重發等機制, 故⽽傳輸速度很快。常見的有:語音通話、視訊通話、實時遊戲畫面 等。
- TCP,在收發資料前,必須和對方建立可靠的連線,然後再進行收發資料。常見有:網站、手機APP資料獲取等。
- 簡述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> (好的,可以斷開了)
- 簡述你理解的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位是網路地址。
- 埠的作用?
在網路程式設計中,IP代指的是計算機,而埠則代指計算機中的某個程式。以方便對計算機中的多個程式程序區分。
- 什麼是粘包?如何解決粘包?
兩臺電腦在進行收發資料時,其實不是直接將資料傳輸給對方。
- 對於傳送者,執行 `sendall/send` 傳送訊息時,是將資料先傳送至自己網路卡的 寫緩衝區 ,再由緩衝區將資料傳送給到對方網路卡的讀緩衝區。
- 對於接受者,執行 `recv` 接收訊息時,是從自己網路卡的讀緩衝區獲取資料。
所以,如果傳送者連續快速的傳送了2條資訊,接收者在讀取時會認為這是1條資訊,即:2個資料包粘在了一起。
解決思路:
雙方約定好規則,在每次收發資料時,都按照固定的 資料頭 + 資料 來進行處理資料包,在資料頭中設定好資料的長度。
- IO多路複用的作用是什麼?
可以監聽多個 IO物件 的變化(可讀、可寫、異常)。
在網路程式設計中一般與非阻塞的socket物件配合使用,以監聽 socket服務端、客戶端是否 (可讀、可寫、異常)
IO多路複用有三種模式:
- select,限制1024個 & 輪訓的機制監測。
- poll,無限制 & 輪訓的機制監測。
- epoll,無限制 & 採用回撥的機制(邊緣觸發)。
- 簡述程序、執行緒、協程的區別。
執行緒,是計算機中可以被cpu排程的最小單元。
程序,是計算機資源分配的最小單元(程序為執行緒提供資源)。
一個程序中可以有多個執行緒,同一個程序中的執行緒可以共享此程序中的資源。
由於CPython中GIL的存在:
- 執行緒,適用於IO密集型操作。
- 程序,適用於計算密集型操作。
協程,協程也可以被稱為微執行緒,是一種使用者態內的上下文切換技術,在開發中結合遇到IO自動切換,就可以透過一個執行緒實現併發操作。
所以,在處理IO操作時,協程比執行緒更加節省開銷(協程的開發難度大一些)。
- 什麼是GIL鎖?其作用是什麼?
GIL, 全域性直譯器鎖(Global Interpreter Lock),是CPython直譯器特有一個玩意,讓一個程序中同一個時刻只能有一個執行緒可以被CPU呼叫。
- 程序之間如何實現資料的共享?
multiprocessing.Value 或 multiprocessing.Array
multiprocessing.Manager
multiprocessing.Queue
multiprocessing.Pipe
-
已知一個訂單物件(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)
-
基於物件導向的知識構造一個連結串列。
注意:每個連結串列都是一個物件,物件內部均儲存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
-
讀原始碼,分析程式碼的執行過程。
-
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()
-
請自己基於socket模組和threading模組實現 門票預訂 平臺。(無需考慮粘包)
-
使用者作為socket客戶端
- 輸入
景區名稱
,用來查詢景區的餘票。 - 輸入
景區名稱-預訂者-8
,用於預定門票。
- 輸入
-
socket服務端,可以支援併發多人同時查詢和購買。(為每個客戶度建立一個執行緒)。
-
服務端資料儲存結構如下:
db ├── tickets │ ├── 歡樂谷.txt # 內部儲存放票數量 │ ├── 迪士尼.txt │ └── 長城.txt └── users ├── alex.txt # 內部儲存次使用者預定記錄 └── 武沛齊.txt # 注意:當使用者預定門票時,放票量要減去相應的數量(變為0之後,則不再接受預定)。
-
-
參考程式碼
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()