Python的Socket編程

前言

  • 先將使用過程需要說明的要點記錄下:
    • 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。在每條消息前面追加一個頭信息,頭信息中包括消息的長度和其它需要的字段。這樣做的話我們只需要追蹤頭信息,當讀到頭信息時,就可以查到消息的長度並且讀出所有字節然後使用它。

單客戶端/單服務器模式

在這裏插入圖片描述

# client.py
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))
  • 單服務器
# server.py
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)

多客戶端/多服務器模式

# mult-client.py
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)  # Should be ready to read
        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)  # Should be ready to write
            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)
        # Check for a socket being monitored to continue.
        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()  # Should be ready to read
    print("accepted connection from", addr)
    # 調用 sock.accept() 後立即再立即調 conn.setblocking(False) 來讓 socket 進入非阻塞模式
    conn.setblocking(False)
    # 使用了 types.SimpleNamespace 類創建了一個對象用來保存我們想要的 socket 和數據
    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):
    # key 包含了 socket 對象「fileobj」和數據對象;
    sock = key.fileobj
    data = key.data
    # mask 包含了就緒的事件
    if mask & selectors.EVENT_READ:
        '''
        如果 socket 就緒而且可以被讀取,mask & selectors.EVENT_READ 就爲真,sock.recv() 會被調用。
        所有讀取到的數據都會被追加到 data.outb 裏面
        '''
        recv_data = sock.recv(1024)  # Should be ready to read
        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)
            # 任何接收並被 data.outb 存儲的數據都將使用 sock.send() 方法打印出來
            sent = sock.send(data.outb)  # Should be ready to write
            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)    # 配置 socket 爲非阻塞模式
# 使用 selectors.EVENT_READ 讀取到事件
# 註冊 socket 監控
# data 來跟蹤 socket 上發送或者接收的東西
sel.register(lsock, selectors.EVENT_READ, data=None)

try:
    while True:
        events = sel.select(timeout=None)   # 阻塞直到 socket I/O 就緒
        """
        sel.select(timeout=None)返回一個(key, events) 元組;
        key 包含了 socket 對象「fileobj」和數據對象;
        mask 包含了就緒的事件
        """
        for key, mask in events:
            # 新的客戶端, 利用accept_wrapper()來接受新的 socket 對象並註冊到 selector 上
            if key.data is None:
                accept_wrapper(key.fileobj) #key.fileobj=socket對象
            # 如果 key.data 不爲空,舊的被接受的客戶端,我們需要爲它服務,接着 service_connection() 會傳入 key 和 mask 參數並調用
            else:
                service_connection(key, mask)
except KeyboardInterrupt:
    print("caught keyboard interrupt, exiting")
finally:
    sel.close()

參考鏈接

Python網絡編程(第3版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章