前言
- 先將使用過程需要說明的要點記錄下:
- socket.socket() 創建了一個 socket 對象,並且支持 context manager type,使用 with 語句,這樣就不用再手動調用 s.close() 來關閉 socket
- 1024 是緩衝區數據大小限制最大值參數 bufsize
- 標準庫中的selectors 模塊提供了高層且高效的 I/O 多路複用,基於原始的 select 模塊構建
- asyncio 使用單線程來處理多任務,使用事件循環來管理任務。通過使用 select(),可以創建自己的事件循環,更簡單且同步化。當使用多線程時,要處理併發的情況,不得不面臨使用 CPython 或者 PyPy 中的「全局解析器鎖 GIL」,這有效地限制了可以並行完成的工作量。
- 從 Python 3.3 開始,與 socket 或地址語義相關的錯誤會引發 OSError 或其子類之一的異常引用
- 需要有不同的網絡緩衝區,使用 recv() 方法不斷的從緩衝區中讀取數據,直到你的應用確定讀取到了足夠的數據
- 一個通用的方案,很多協議都會用到它,包括 HTTP。在每條消息前面追加一個頭信息,頭信息中包括消息的長度和其它需要的字段。這樣做的話我們只需要追蹤頭信息,當讀到頭信息時,就可以查到消息的長度並且讀出所有字節然後使用它。
單客戶端/單服務器模式
import socket
host="127.0.0.1"
port=65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as sock:
sock.connect((host,port))
sock.sendall(b"hello world!")
data = sock.recv(1024)
print("Recieved ",repr(data))
import socket
host="127.0.0.1"
port=65432
with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as sock:
sock.bind((host,port))
sock.listen()
conn,addr = sock.accept()
with conn:
print("Connected by ",addr)
while True:
data = conn.recv(1024)
print(data)
if not data:
break
conn.sendall(data)
多客戶端/多服務器模式
import sys
import socket
import selectors
import types
sel = selectors.DefaultSelector()
messages = [b"Message 1 from client.", b"Message 2 from client."]
def start_connections(host, port, num_conns):
server_addr = (host, port)
for i in range(0, num_conns):
connid = i + 1
print("starting connection", connid, "to", server_addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = types.SimpleNamespace(
connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=list(messages),
outb=b"",
)
sel.register(sock, events, data=data)
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024)
if recv_data:
print("received", repr(recv_data), "from connection", data.connid)
data.recv_total += len(recv_data)
if not recv_data or data.recv_total == data.msg_total:
print("closing connection", data.connid)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if not data.outb and data.messages:
data.outb = data.messages.pop(0)
if data.outb:
print("sending", repr(data.outb), "to connection", data.connid)
sent = sock.send(data.outb)
data.outb = data.outb[sent:]
if len(sys.argv) != 4:
print("usage:", sys.argv[0], "<host> <port> <num_connections>")
sys.exit(1)
host, port, num_conns = sys.argv[1:4]
start_connections(host, int(port), int(num_conns))
try:
while True:
events = sel.select(timeout=1)
if events:
for key, mask in events:
service_connection(key, mask)
if not sel.get_map():
break
except KeyboardInterrupt:
print("caught keyboard interrupt, exiting")
finally:
sel.close()
import sys
import socket
import selectors
import types
sel = selectors.DefaultSelector()
def accept_wrapper(sock):
conn, addr = sock.accept()
print("accepted connection from", addr)
conn.setblocking(False)
data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(conn, events, data=data)
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
'''
如果 socket 就緒而且可以被讀取,mask & selectors.EVENT_READ 就爲真,sock.recv() 會被調用。
所有讀取到的數據都會被追加到 data.outb 裏面
'''
recv_data = sock.recv(1024)
if recv_data:
data.outb += recv_data
else:
'''
如果沒有收到任何數據,表示客戶端關閉了它的 socket 連接,這時服務端也應該關閉自己的連接。
先調用 sel.unregister() 來撤銷 select() 的監控
'''
print("closing connection to", data.addr)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print("echoing", repr(data.outb), "to", data.addr)
sent = sock.send(data.outb)
data.outb = data.outb[sent:]
if len(sys.argv) != 3:
print("usage:", sys.argv[0], "<host> <port>")
sys.exit(1)
host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print("listening on", (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
try:
while True:
events = sel.select(timeout=None)
"""
sel.select(timeout=None)返回一個(key, events) 元組;
key 包含了 socket 對象「fileobj」和數據對象;
mask 包含了就緒的事件
"""
for key, mask in events:
if key.data is None:
accept_wrapper(key.fileobj)
else:
service_connection(key, mask)
except KeyboardInterrupt:
print("caught keyboard interrupt, exiting")
finally:
sel.close()
參考鏈接
Python網絡編程(第3版)