每週一個 Python 模組 | socket

yongxinz發表於2019-01-03

專欄地址:每週一個 Python 模組

Socket 提供了標準的 BSD Socket API,以便使用 BSD 套接字介面通過網路進行通訊。它包括用於處理實際資料通道的類,還包括與網路相關的功能,例如將伺服器的名稱轉換為地址以及格式化要通過網路傳送的資料。

定址、協議族和套接字型別

socket 是由程式用來傳遞資料或通過網際網路通訊的通道的一個端點。套接字有兩個主要屬性來控制它們傳送資料的方式: 地址族控制所使用的 OSI 網路層協議, 以及套接字型別控制傳輸層協議。

Python 支援三種地址族。最常見的是 AF_INET,用於 IPv4 網路定址。IPv4 地址長度為四個位元組,通常表示為四個數字的序列,每八位元組一個,用點分隔(例如,10.1.1.5127.0.0.1)。這些值通常被稱為 IP 地址。

AF_INET6用於IPv6 網路定址。IPv6 是網路協議的下一代版本,支援 IPv4 下不可用的 128 位地址,流量整型和路由功能。IPv6 的採用率持續增長,特別是隨著雲端計算的發展,以及由於物聯網專案而新增到網路中的額外裝置的激增。

AF_UNIX是 Unix 域套接字(UDS)的地址族,它是 POSIX 相容系統上可用的程式間通訊協議。UDS 的實現允許作業系統將資料直接從一個程式傳遞到另一個程式,而無需通過網路堆疊。這比使用 AF_INET 更高效,但由於檔案系統用作定址的名稱空間,因此 UDS 僅限於同一系統上的程式。使用 UDS 而不是其他 IPC 機制(如命名管道或共享記憶體)的吸引力在於程式設計介面與 IP 網路相同,因此應用程式可以在單個主機上執行時高效通訊。

注意:AF_UNIX常量僅定義在支援 UDS 的系統上。

套接字型別通常用 SOCK_DGRAM 處理面向訊息的資料包傳輸,用 SOCK_STREAM 處理面向位元組流的傳輸。資料包套接字通常與 UDP(使用者資料包協議)相關聯 ,它們提供不可靠的單個訊息傳遞。面向流的套接字與 TCP(傳輸控制協議)相關聯 。它們在客戶端和伺服器之間提供位元組流,通過超時管理,重傳和其他功能確保訊息傳遞或故障通知。

大多數提供大量資料的應用程式協議(如 HTTP)都是基於 TCP 構建的,因為它可以在自動處理訊息排序和傳遞時更輕鬆地建立複雜的應用程式。UDP 通常用於訊息不太重要的協議(例如通過 DNS 查詢名稱),或者用於多播(將相同資料傳送到多個主機)。UDP 和 TCP 都可以與 IPv4 或 IPv6 定址一起使用。

注意:Python 的socket模組還支援其他套接字型別,但不太常用,因此這裡不做介紹。有關更多詳細資訊,請參閱標準庫文件。

在網路上查詢主機

socket 包含與網路域名服務介面相關的功能,因此程式可以將伺服器主機名轉換為其數字網路地址。雖然程式在使用它們連線伺服器之前不需要顯式轉換地址,但在報錯時,包含數字地址以及使用的名稱會很有用。

要查詢當前主機的正式名稱,可以使用 gethostname()

import socket

print(socket.gethostname())	# apu.hellfly.net
複製程式碼

返回的名稱取決於當前系統的網路設定,如果位於不同的網路(例如連線到無線 LAN 的膝上型電腦),則可能會更改。

使用 gethostbyname() 將伺服器名稱轉換為它的數字地址。

import socket

HOSTS = [
    'apu',
    'pymotw.com',
    'www.python.org',
    'nosuchname',
]

for host in HOSTS:
    try:
        print('{} : {}'.format(host, socket.gethostbyname(host)))
    except socket.error as msg:
        print('{} : {}'.format(host, msg))
        
# output
# apu : 10.9.0.10
# pymotw.com : 66.33.211.242
# www.python.org : 151.101.32.223
# nosuchname : [Errno 8] nodename nor servname provided, or not known
複製程式碼

如果當前系統的 DNS 配置在搜尋中包含一個或多個域,則 name 引數不必要是完整的名稱(即,它不需要包括域名以及基本主機名)。如果找不到該名稱,則會引發 socket.error 型別異常。

要訪問伺服器的更多命名資訊,請使用 gethostbyname_ex()。它返回伺服器的規範主機名,別名以及可用於訪問它的所有可用 IP 地址。

import socket

HOSTS = [
    'apu',
    'pymotw.com',
    'www.python.org',
    'nosuchname',
]

for host in HOSTS:
    print(host)
    try:
        name, aliases, addresses = socket.gethostbyname_ex(host)
        print('  Hostname:', name)
        print('  Aliases :', aliases)
        print(' Addresses:', addresses)
    except socket.error as msg:
        print('ERROR:', msg)
    print()
    
# output
# apu
#   Hostname: apu.hellfly.net
#   Aliases : ['apu']
#  Addresses: ['10.9.0.10']
# 
# pymotw.com
#   Hostname: pymotw.com
#   Aliases : []
#  Addresses: ['66.33.211.242']
# 
# www.python.org
#   Hostname: prod.python.map.fastlylb.net
#   Aliases : ['www.python.org', 'python.map.fastly.net']
#  Addresses: ['151.101.32.223']
# 
# nosuchname
# ERROR: [Errno 8] nodename nor servname provided, or not known
複製程式碼

擁有伺服器的所有已知 IP 地址後,客戶端可以實現自己的負載均衡或故障轉移演算法。

使用getfqdn() 將部分名稱轉換為一個完整的域名。

import socket

for host in ['apu', 'pymotw.com']:
    print('{:>10} : {}'.format(host, socket.getfqdn(host)))
    
# output
#        apu : apu.hellfly.net
# pymotw.com : apache2-echo.catalina.dreamhost.com
複製程式碼

如果輸入是別名,則返回的名稱不一定與輸入引數匹配,例如此處的 www

當伺服器地址可用時,使用gethostbyaddr() 對名稱執行“反向”查詢。

import socket

hostname, aliases, addresses = socket.gethostbyaddr('10.9.0.10')

print('Hostname :', hostname)	# Hostname : apu.hellfly.net
print('Aliases  :', aliases)	# Aliases  : ['apu']
print('Addresses:', addresses)	# Addresses: ['10.9.0.10']
複製程式碼

返回值是一個元組,包含完整主機名,別名以及與該名稱關聯的所有 IP 地址。

查詢服務資訊

除 IP 地址外,每個套接字地址還包括一個整數埠號。許多應用程式可以在同一主機上執行,監聽單個 IP 地址,但一次只能有一個套接字可以使用該地址的埠。IP 地址,協議和埠號的組合唯一地標識通訊通道,並確保通過套接字傳送的訊息到達正確的目的地。

某些埠號是為特定協議預先分配的。例如,使用 SMTP 的電子郵件伺服器之間的通訊,使用 TCP 在埠號 25 上進行,而 Web 客戶端和伺服器使用埠 80 進行 HTTP 通訊。可以使用 getservbyname() 查詢具有標準化名稱的網路服務的埠號。

import socket
from urllib.parse import urlparse

URLS = [
    'http://www.python.org',
    'https://www.mybank.com',
    'ftp://prep.ai.mit.edu',
    'gopher://gopher.micro.umn.edu',
    'smtp://mail.example.com',
    'imap://mail.example.com',
    'imaps://mail.example.com',
    'pop3://pop.example.com',
    'pop3s://pop.example.com',
]

for url in URLS:
    parsed_url = urlparse(url)
    port = socket.getservbyname(parsed_url.scheme)
    print('{:>6} : {}'.format(parsed_url.scheme, port))
    
# output
#   http : 80
#  https : 443
#    ftp : 21
# gopher : 70
#   smtp : 25
#   imap : 143
#  imaps : 993
#   pop3 : 110
#  pop3s : 995
複製程式碼

雖然標準化服務不太可能改變埠,但是在未來新增新服務時,通過系統呼叫來查詢而不是硬編碼會更靈活。

要反轉服務埠查詢,使用getservbyport()

import socket
from urllib.parse import urlunparse

for port in [80, 443, 21, 70, 25, 143, 993, 110, 995]:
    url = '{}://example.com/'.format(socket.getservbyport(port))
    print(url)
    
# output
# http://example.com/
# https://example.com/
# ftp://example.com/
# gopher://example.com/
# smtp://example.com/
# imap://example.com/
# imaps://example.com/
# pop3://example.com/
# pop3s://example.com/
複製程式碼

反向查詢對於從任意地址構造服務的 URL 非常有用。

可以使用 getprotobyname() 檢索分配給傳輸協議的編號。

import socket


def get_constants(prefix):
    """Create a dictionary mapping socket module
    constants to their names.
    """
    return {
        getattr(socket, n): n for n in dir(socket) if n.startswith(prefix)
    }


protocols = get_constants('IPPROTO_')

for name in ['icmp', 'udp', 'tcp']:
    proto_num = socket.getprotobyname(name)
    const_name = protocols[proto_num]
    print('{:>4} -> {:2d} (socket.{:<12} = {:2d})'.format(
        name, proto_num, const_name,
        getattr(socket, const_name)))
    
# output
# icmp ->  1 (socket.IPPROTO_ICMP =  1)
#  udp -> 17 (socket.IPPROTO_UDP  = 17)
#  tcp ->  6 (socket.IPPROTO_TCP  =  6)
複製程式碼

協議號的值是標準化的,用字首 IPPROTO_ 定義為常量。

查詢伺服器地址

getaddrinfo() 將服務的基本地址轉換為元組列表,其中包含建立連線所需的所有資訊。每個元組的內容會有所不同,包含不同的網路地址族或協議。

import socket


def get_constants(prefix):
    """Create a dictionary mapping socket module
    constants to their names.
    """
    return {
        getattr(socket, n): n for n in dir(socket) if n.startswith(prefix)
    }


families = get_constants('AF_')
types = get_constants('SOCK_')
protocols = get_constants('IPPROTO_')

for response in socket.getaddrinfo('www.python.org', 'http'):

    # Unpack the response tuple
    family, socktype, proto, canonname, sockaddr = response

    print('Family        :', families[family])
    print('Type          :', types[socktype])
    print('Protocol      :', protocols[proto])
    print('Canonical name:', canonname)
    print('Socket address:', sockaddr)
    print()
    
# output
# Family        : AF_INET
# Type          : SOCK_DGRAM
# Protocol      : IPPROTO_UDP
# Canonical name:
# Socket address: ('151.101.32.223', 80)
# 
# Family        : AF_INET
# Type          : SOCK_STREAM
# Protocol      : IPPROTO_TCP
# Canonical name:
# Socket address: ('151.101.32.223', 80)
# 
# Family        : AF_INET6
# Type          : SOCK_DGRAM
# Protocol      : IPPROTO_UDP
# Canonical name:
# Socket address: ('2a04:4e42:8::223', 80, 0, 0)
# 
# Family        : AF_INET6
# Type          : SOCK_STREAM
# Protocol      : IPPROTO_TCP
# Canonical name:
# Socket address: ('2a04:4e42:8::223', 80, 0, 0)
複製程式碼

該程式演示瞭如何查詢 www.python.org 的連線資訊。

getaddrinfo()採用幾個引數來過濾結果列表。hostport是必傳引數。可選的引數是familysocktypeproto,和flags。可選值應該是0或由 socket 定義的常量之一。

import socket


def get_constants(prefix):
    """Create a dictionary mapping socket module
    constants to their names.
    """
    return {
        getattr(socket, n): n for n in dir(socket) if n.startswith(prefix)
    }


families = get_constants('AF_')
types = get_constants('SOCK_')
protocols = get_constants('IPPROTO_')

responses = socket.getaddrinfo(
    host='www.python.org',
    port='http',
    family=socket.AF_INET,
    type=socket.SOCK_STREAM,
    proto=socket.IPPROTO_TCP,
    flags=socket.AI_CANONNAME,
)

for response in responses:
    # Unpack the response tuple
    family, socktype, proto, canonname, sockaddr = response

    print('Family        :', families[family])
    print('Type          :', types[socktype])
    print('Protocol      :', protocols[proto])
    print('Canonical name:', canonname)
    print('Socket address:', sockaddr)
    print()
    
# output
# Family        : AF_INET
# Type          : SOCK_STREAM
# Protocol      : IPPROTO_TCP
# Canonical name: prod.python.map.fastlylb.net
# Socket address: ('151.101.32.223', 80)
複製程式碼

由於flags包含AI_CANONNAME,伺服器的規範名稱(可能與主機具有別名時用於查詢的值不同)包含在結果中。如果沒有該標誌,則規範名稱將保留為空。

IP 地址表示

用 C 編寫的網路程式使用資料型別將 IP 地址表示為二進位制值(而不是通常在 Python 程式中的字串地址)。要在 Python 表示和 C 表示之間轉換 IPv4 地址,請使用structsockaddrinet_aton()inet_ntoa()

import binascii
import socket
import struct
import sys

for string_address in ['192.168.1.1', '127.0.0.1']:
    packed = socket.inet_aton(string_address)
    print('Original:', string_address)
    print('Packed  :', binascii.hexlify(packed))
    print('Unpacked:', socket.inet_ntoa(packed))
    print()
    
# output
# Original: 192.168.1.1
# Packed  : b'c0a80101'
# Unpacked: 192.168.1.1
# 
# Original: 127.0.0.1
# Packed  : b'7f000001'
# Unpacked: 127.0.0.1
複製程式碼

打包格式中的四個位元組可以傳遞給 C 庫,通過網路安全傳輸,或者緊湊地儲存到資料庫中。

相關函式 inet_pton()inet_ntop() 支援 IPv4 和 IPv6,根據傳入的地址族引數生成適當的格式。

import binascii
import socket
import struct
import sys

string_address = '2002:ac10:10a:1234:21e:52ff:fe74:40e'
packed = socket.inet_pton(socket.AF_INET6, string_address)

print('Original:', string_address)
print('Packed  :', binascii.hexlify(packed))
print('Unpacked:', socket.inet_ntop(socket.AF_INET6, packed))

# output
# Original: 2002:ac10:10a:1234:21e:52ff:fe74:40e
# Packed  : b'2002ac10010a1234021e52fffe74040e'
# Unpacked: 2002:ac10:10a:1234:21e:52ff:fe74:40e
複製程式碼

IPv6 地址已經是十六進位制值,因此將打包版本轉換為一系列十六進位制數字會生成類似於原始值的字串。

TCP/IP 客戶端和服務端

Sockets 可以作為服務端並監聽傳入訊息,或作為客戶端連線其他應用程式。連線 TCP/IP 套接字的兩端後,通訊是雙向的。

服務端

此示例程式基於標準庫文件中的示例程式,接收傳入的訊息並將它們回送給傳送方。它首先建立一個 TCP/IP 套接字,然後用 bind() 將套接字與伺服器地址相關聯。地址是localhost指當前伺服器,埠號是 10000。

# socket_echo_server.py
import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the port
server_address = ('localhost', 10000)
print('starting up on {} port {}'.format(*server_address))
sock.bind(server_address)

# Listen for incoming connections
sock.listen(1)

while True:
    # Wait for a connection
    print('waiting for a connection')
    connection, client_address = sock.accept()
    try:
        print('connection from', client_address)

        # Receive the data in small chunks and retransmit it
        while True:
            data = connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                print('sending data back to the client')
                connection.sendall(data)
            else:
                print('no data from', client_address)
                break

    finally:
        # Clean up the connection
        connection.close()
複製程式碼

呼叫listen()將套接字置於伺服器模式,並用 accept()等待傳入連線。整數參數列示後臺排隊的連線數,當連線數超出時,系統會拒絕。此示例僅期望一次使用一個連線。

accept()返回伺服器和客戶端之間的開放連線以及客戶端的地址。該連線實際上是另一個埠上的不同套接字(由核心分配)。從連線中用 recv() 讀取資料並用 sendall() 傳輸資料。

與客戶端通訊完成後,需要使用 close() 清理連線。此示例使用 try:finally塊來確保close()始終呼叫,即使出現錯誤也是如此。

客戶端

客戶端程式的 socket 設定與服務端的不同。它不是繫結到埠並監聽,而是用於connect()將套接字直接連線到遠端地址。

# socket_echo_client.py
import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect the socket to the port where the server is listening
server_address = ('localhost', 10000)
print('connecting to {} port {}'.format(*server_address))
sock.connect(server_address)

try:

    # Send data
    message = b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    # Look for the response
    amount_received = 0
    amount_expected = len(message)

    while amount_received < amount_expected:
        data = sock.recv(16)
        amount_received += len(data)
        print('received {!r}'.format(data))

finally:
    print('closing socket')
    sock.close()
複製程式碼

建立連線後,資料可以通過 sendall() 傳送 recv() 接收。傳送整個訊息並收到同樣的回覆後,關閉套接字以釋放埠。

執行客戶端和服務端

客戶端和服務端應該在單獨的終端視窗中執行,以便它們可以相互通訊。服務端輸出顯示傳入的連線和資料,以及傳送回客戶端的響應。

$ python3 socket_echo_server.py
starting up on localhost port 10000
waiting for a connection
connection from ('127.0.0.1', 65141)
received b'This is the mess'
sending data back to the client
received b'age.  It will be'
sending data back to the client
received b' repeated.'
sending data back to the client
received b''
no data from ('127.0.0.1', 65141)
waiting for a connection
複製程式碼

客戶端輸出顯示傳出訊息和來自服務端的響應。

$ python3 socket_echo_client.py
connecting to localhost port 10000
sending b'This is the message.  It will be repeated.'
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
closing socket
複製程式碼

簡易客戶端連線

通過使用便捷功能create_connection()連線到服務端,TCP/IP 客戶端可以節省一些步驟 。該函式接受一個引數,一個包含伺服器地址的雙值元組,並派生出用於連線的最佳地址。

import socket
import sys


def get_constants(prefix):
    """Create a dictionary mapping socket module
    constants to their names.
    """
    return {
        getattr(socket, n): n for n in dir(socket) if n.startswith(prefix)
    }


families = get_constants('AF_')
types = get_constants('SOCK_')
protocols = get_constants('IPPROTO_')

# Create a TCP/IP socket
sock = socket.create_connection(('localhost', 10000))

print('Family  :', families[sock.family])
print('Type    :', types[sock.type])
print('Protocol:', protocols[sock.proto])
print()

try:

    # Send data
    message = b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    amount_received = 0
    amount_expected = len(message)

    while amount_received < amount_expected:
        data = sock.recv(16)
        amount_received += len(data)
        print('received {!r}'.format(data))

finally:
    print('closing socket')
    sock.close()
    
# output
# Family  : AF_INET
# Type    : SOCK_STREAM
# Protocol: IPPROTO_TCP
# 
# sending b'This is the message.  It will be repeated.'
# received b'This is the mess'
# received b'age.  It will be'
# received b' repeated.'
# closing socket
複製程式碼

create_connection()getaddrinfo() 方法獲得可選引數,並socket使用建立成功連線的第一個配置返回已開啟的連線引數。familytypeproto屬性可以用來檢查返回的型別是 socket 型別。

選擇監聽地址

將服務端繫結到正確的地址非常重要,以便客戶端可以與之通訊。前面的示例都用 'localhost' 作為 IP 地址,但這樣有一個限制,只有在同一伺服器上執行的客戶端才能連線。使用伺服器的公共地址(例如 gethostname() 的返回值)來允許其他主機進行連線。修改上面的例子,讓服務端監聽通過命令列引數指定的地址。

# socket_echo_server_explicit.py
import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the address given on the command line
server_name = sys.argv[1]
server_address = (server_name, 10000)
print('starting up on {} port {}'.format(*server_address))
sock.bind(server_address)
sock.listen(1)

while True:
    print('waiting for a connection')
    connection, client_address = sock.accept()
    try:
        print('client connected:', client_address)
        while True:
            data = connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                connection.sendall(data)
            else:
                break
    finally:
        connection.close()
複製程式碼

對客戶端程式進行類似的修改。

# socket_echo_client_explicit.py
import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect the socket to the port on the server
# given by the caller
server_address = (sys.argv[1], 10000)
print('connecting to {} port {}'.format(*server_address))
sock.connect(server_address)

try:

    message = b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    amount_received = 0
    amount_expected = len(message)
    while amount_received < amount_expected:
        data = sock.recv(16)
        amount_received += len(data)
        print('received {!r}'.format(data))

finally:
    sock.close()
複製程式碼

使用引數 hubert 啟動服務端, netstat命令顯示它正在監聽指定主機的地址。

$ host hubert.hellfly.net

hubert.hellfly.net has address 10.9.0.6

$ netstat -an | grep 10000

Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
...
tcp4       0      0  10.9.0.6.10000         *.*                    LISTEN
...
複製程式碼

在另一臺主機上執行客戶端,hubert.hellfly.net 作為引數:

$ hostname

apu

$ python3 ./socket_echo_client_explicit.py hubert.hellfly.net
connecting to hubert.hellfly.net port 10000
sending b'This is the message.  It will be repeated.'
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
複製程式碼

服務端輸出是:

$ python3 socket_echo_server_explicit.py hubert.hellfly.net
starting up on hubert.hellfly.net port 10000
waiting for a connection
client connected: ('10.9.0.10', 33139)
received b''
waiting for a connection
client connected: ('10.9.0.10', 33140)
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
received b''
waiting for a connection
複製程式碼

許多服務端具有多個網路介面,因此也會有多個 IP 地址連線。為每個 IP 地址執行服務端肯定是不明智的,可以使用特殊地址INADDR_ANY 同時監聽所有地址。儘管 socketINADDR_ANY 定義了一個常量,但它是一個整數,必須先將其轉換為點符號分隔的字串地址才能傳遞給 bind()。作為更方便的方式,使用“ 0.0.0.0”或空字串('')就可以了,而不是進行轉換。

import socket
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the address given on the command line
server_address = ('', 10000)
sock.bind(server_address)
print('starting up on {} port {}'.format(*sock.getsockname()))
sock.listen(1)

while True:
    print('waiting for a connection')
    connection, client_address = sock.accept()
    try:
        print('client connected:', client_address)
        while True:
            data = connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                connection.sendall(data)
            else:
                break
    finally:
        connection.close()
複製程式碼

要檢視套接字使用的實際地址,可以使用 getsockname() 方法。啟動服務後,再次執行 netstat 會顯示它正在監聽任何地址上的傳入連線。

$ netstat -an

Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address    Foreign Address  (state)
...
tcp4       0      0  *.10000          *.*              LISTEN
...
複製程式碼

使用者資料包客戶端和服務端

使用者資料包協議(UDP)與 TCP/IP 的工作方式不同。TCP 是面向位元組流的,所有資料以正確的順序傳輸,UDP 是面向訊息的協議。UDP 不需要長連線,因此設定 UDP 套接字更簡單一些。另一方面,UDP 訊息必須適合單個資料包(對於 IPv4,這意味著它們只能容納 65,507 個位元組,因為 65,535 位元組的資料包也包含頭資訊)並且不能保證傳送與 TCP 一樣可靠。

服務端

由於沒有連線本身,伺服器不需要監聽和接收連線。它只需要 bind() 地址和埠,然後等待單個訊息。

# socket_echo_server_dgram.py 
import socket
import sys

# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Bind the socket to the port
server_address = ('localhost', 10000)
print('starting up on {} port {}'.format(*server_address))
sock.bind(server_address)

while True:
    print('\nwaiting to receive message')
    data, address = sock.recvfrom(4096)

    print('received {} bytes from {}'.format(len(data), address))
    print(data)

    if data:
        sent = sock.sendto(data, address)
        print('sent {} bytes back to {}'.format(sent, address))
複製程式碼

使用 recvfrom() 從套接字讀取訊息,然後按照客戶端地址返回資料。

客戶端

UDP 客戶端與服務端類似,但不需要 bind()。它用 sendto()將訊息直接傳送到服務算,並用 recvfrom()接收響應。

# socket_echo_client_dgram.py 
import socket
import sys

# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

server_address = ('localhost', 10000)
message = b'This is the message.  It will be repeated.'

try:

    # Send data
    print('sending {!r}'.format(message))
    sent = sock.sendto(message, server_address)

    # Receive response
    print('waiting to receive')
    data, server = sock.recvfrom(4096)
    print('received {!r}'.format(data))

finally:
    print('closing socket')
    sock.close()
複製程式碼

執行客戶端和服務端

執行服務端會產生:

$ python3 socket_echo_server_dgram.py
starting up on localhost port 10000

waiting to receive message
received 42 bytes from ('127.0.0.1', 57870)
b'This is the message.  It will be repeated.'
sent 42 bytes back to ('127.0.0.1', 57870)

waiting to receive message
複製程式碼

客戶端輸出是:

$ python3 socket_echo_client_dgram.py
sending b'This is the message.  It will be repeated.'
waiting to receive
received b'This is the message.  It will be repeated.'
closing socket
複製程式碼

Unix 域套接字

從程式設計師的角度來看,使用 Unix 域套接字和 TCP/IP 套接字有兩個本質區別。首先,套接字的地址是檔案系統上的路徑,而不是包含伺服器名稱和埠的元組。其次,在套接字關閉後,在檔案系統中建立的表示套接字的節點仍然存在,並且每次服務端啟動時都需要刪除。通過在設定部分進行一些更改,使之前的服務端程式支援 UDS。

建立 socket 時使用地址族 AF_UNIX。繫結套接字和管理傳入連線方式與 TCP/IP 套接字相同。

# socket_echo_server_uds.py 
import socket
import sys
import os

server_address = './uds_socket'

# Make sure the socket does not already exist
try:
    os.unlink(server_address)
except OSError:
    if os.path.exists(server_address):
        raise

# Create a UDS socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

# Bind the socket to the address
print('starting up on {}'.format(server_address))
sock.bind(server_address)

# Listen for incoming connections
sock.listen(1)

while True:
    # Wait for a connection
    print('waiting for a connection')
    connection, client_address = sock.accept()
    try:
        print('connection from', client_address)

        # Receive the data in small chunks and retransmit it
        while True:
            data = connection.recv(16)
            print('received {!r}'.format(data))
            if data:
                print('sending data back to the client')
                connection.sendall(data)
            else:
                print('no data from', client_address)
                break

    finally:
        # Clean up the connection
        connection.close()
複製程式碼

還需要修改客戶端設定以使用 UDS。它應該假定套接字的檔案系統節點存在,因為服務端通過繫結到該地址來建立它。傳送和接收資料在 UDS 客戶端中的工作方式與之前的 TCP/IP 客戶端相同。

# socket_echo_client_uds.py 
import socket
import sys

# Create a UDS socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

# Connect the socket to the port where the server is listening
server_address = './uds_socket'
print('connecting to {}'.format(server_address))
try:
    sock.connect(server_address)
except socket.error as msg:
    print(msg)
    sys.exit(1)

try:

    # Send data
    message = b'This is the message.  It will be repeated.'
    print('sending {!r}'.format(message))
    sock.sendall(message)

    amount_received = 0
    amount_expected = len(message)

    while amount_received < amount_expected:
        data = sock.recv(16)
        amount_received += len(data)
        print('received {!r}'.format(data))

finally:
    print('closing socket')
    sock.close()
複製程式碼

程式輸出大致相同,並對地址資訊進行適當更新。服務端顯示收到的訊息並將其傳送回客戶端。

$ python3 socket_echo_server_uds.py
starting up on ./uds_socket
waiting for a connection
connection from
received b'This is the mess'
sending data back to the client
received b'age.  It will be'
sending data back to the client
received b' repeated.'
sending data back to the client
received b''
no data from
waiting for a connection
複製程式碼

客戶端一次性傳送訊息,並以遞增方式接收部分訊息。

$ python3 socket_echo_client_uds.py
connecting to ./uds_socket
sending b'This is the message.  It will be repeated.'
received b'This is the mess'
received b'age.  It will be'
received b' repeated.'
closing socket
複製程式碼

許可權

由於 UDS 套接字由檔案系統上的節點表示,因此可以使用標準檔案系統許可權來控制服務端的訪問。

$ ls -l ./uds_socket

srwxr-xr-x  1 dhellmann  dhellmann  0 Aug 21 11:19 uds_socket

$ sudo chown root ./uds_socket

$ ls -l ./uds_socket

srwxr-xr-x  1 root  dhellmann  0 Aug 21 11:19 uds_socket
複製程式碼

以非root使用者執行客戶端會導致錯誤,因為該程式無權開啟套接字。

$ python3 socket_echo_client_uds.py

connecting to ./uds_socket
[Errno 13] Permission denied
複製程式碼

父子程式之間的通訊

為了在 Unix 下進行程式間通訊,通過 socketpair() 函式來設定 UDS 套接字很有用。它建立了一對連線的套接字,當子程式被建立後,在父程式和子程式之間進行通訊。

import socket
import os

parent, child = socket.socketpair()

pid = os.fork()

if pid:
    print('in parent, sending message')
    child.close()
    parent.sendall(b'ping')
    response = parent.recv(1024)
    print('response from child:', response)
    parent.close()

else:
    print('in child, waiting for message')
    parent.close()
    message = child.recv(1024)
    print('message from parent:', message)
    child.sendall(b'pong')
    child.close()
    
# output
# in parent, sending message
# in child, waiting for message
# message from parent: b'ping'
# response from child: b'pong'
複製程式碼

預設情況下,會建立一個 UDS 套接字,但也可以傳遞地址族,套接字型別甚至協議選項來控制套接字的建立方式。

組播

點對點連線可以處理大量通訊需求,但隨著直接連線數量的增加,同時給多個接收者傳遞相同的資訊變得具有挑戰性。分別向每個接收者傳送訊息會消耗額外的處理時間和頻寬,這對於諸如流式視訊或音訊之類的應用來說可能是個問題。使用組播一次向多個端點傳遞訊息可以使效率更高。

組播訊息通過 UDP 傳送,因為 TCP 是一對一的通訊系統。組播地址(稱為 組播組)是為組播流量保留的常規 IPv4 地址範圍(224.0.0.0到230.255.255.255)的子集。這些地址由網路路由器和交換機專門處理,因此傳送到該組的郵件可以通過 Internet 分發給已加入該組的所有收件人。

注意:某些託管交換機和路由器預設禁用組播流量。如果在使用示例程式時遇到問題,請檢查網路硬體設定。

傳送組播訊息

修改上面的客戶端程式使其向組播組傳送訊息,然後報告它收到的所有響應。由於無法知道預期會有多少響應,因此它會使用套接字的超時值來避免在等待答案時無限期地阻塞。

套接字還需要配置訊息的生存時間值(TTL)。TTL 控制接收資料包的網路數量。使用IP_MULTICAST_TTLsetsockopt() 設定 TTL。預設值1表示路由器不會將資料包轉發到當前網段之外。該值最大可達 255,並且應打包為單個位元組。

# socket_multicast_sender.py 
import socket
import struct
import sys

message = b'very important data'
multicast_group = ('224.3.29.71', 10000)

# Create the datagram socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Set a timeout so the socket does not block
# indefinitely when trying to receive data.
sock.settimeout(0.2)

# Set the time-to-live for messages to 1 so they do not
# go past the local network segment.
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)

try:

    # Send data to the multicast group
    print('sending {!r}'.format(message))
    sent = sock.sendto(message, multicast_group)

    # Look for responses from all recipients
    while True:
        print('waiting to receive')
        try:
            data, server = sock.recvfrom(16)
        except socket.timeout:
            print('timed out, no more responses')
            break
        else:
            print('received {!r} from {}'.format(data, server))

finally:
    print('closing socket')
    sock.close()
複製程式碼

發件人的其餘部分看起來像 UDP 客戶端,除了它需要多個響應,因此使用迴圈呼叫 recvfrom() 直到超時。

接收組播訊息

建立組播接收器的第一步是建立 UDP 套接字。建立常規套接字並繫結到埠後,可以使用setsockopt()更改IP_ADD_MEMBERSHIP選項將其新增到組播組。選項值是組播組地址的 8 位元組打包表示,後跟服務端監聽流量的網路介面,由其 IP 地址標識。在這種情況下,接收端使用 INADDR_ANY 所有介面。

# socket_multicast_receiver.py 
import socket
import struct
import sys

multicast_group = '224.3.29.71'
server_address = ('', 10000)

# Create the socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Bind to the server address
sock.bind(server_address)

# Tell the operating system to add the socket to
# the multicast group on all interfaces.
group = socket.inet_aton(multicast_group)
mreq = struct.pack('4sL', group, socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

# Receive/respond loop
while True:
    print('\nwaiting to receive message')
    data, address = sock.recvfrom(1024)

    print('received {} bytes from {}'.format(len(data), address))
    print(data)

    print('sending acknowledgement to', address)
    sock.sendto(b'ack', address)
複製程式碼

接收器的主迴圈就像常規的 UDP 服務端一樣。

示例輸出

此示例顯示在兩個不同主機上執行的組播接收器。A地址192.168.1.13B地址 192.168.1.14

[A]$ python3 socket_multicast_receiver.py

waiting to receive message
received 19 bytes from ('192.168.1.14', 62650)
b'very important data'
sending acknowledgement to ('192.168.1.14', 62650)

waiting to receive message

[B]$ python3 source/socket/socket_multicast_receiver.py

waiting to receive message
received 19 bytes from ('192.168.1.14', 64288)
b'very important data'
sending acknowledgement to ('192.168.1.14', 64288)

waiting to receive message
複製程式碼

發件人正在主機上執行B

[B]$ python3 socket_multicast_sender.py
sending b'very important data'
waiting to receive
received b'ack' from ('192.168.1.14', 10000)
waiting to receive
received b'ack' from ('192.168.1.13', 10000)
waiting to receive
timed out, no more responses
closing socket
複製程式碼

訊息被髮送一次,並且接收到兩個傳出訊息的確認,分別來自主機AB

傳送二進位制資料

套接字傳輸位元組流。這些位元組可以包含編碼為位元組的文字訊息,如前面示例中所示,或者它們也可以是由 struct 打包到緩衝區中的二進位制資料。

此客戶端程式將整數,兩個字元的字串和浮點值,編碼為可傳遞到套接字以進行傳輸的位元組序列。

# socket_binary_client.py 
import binascii
import socket
import struct
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('localhost', 10000)
sock.connect(server_address)

values = (1, b'ab', 2.7)
packer = struct.Struct('I 2s f')
packed_data = packer.pack(*values)

print('values =', values)

try:
    # Send data
    print('sending {!r}'.format(binascii.hexlify(packed_data)))
    sock.sendall(packed_data)
finally:
    print('closing socket')
    sock.close()
複製程式碼

在兩個系統之間傳送多位元組二進位制資料時,重要的是要確保連線的兩端都知道位元組的順序,以及如何將它們解壓回原來的結構。服務端程式使用相同的 Struct說明符來解壓縮接收的位元組,以便按正確的順序還原它們。

# socket_binary_server.py 
import binascii
import socket
import struct
import sys

# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = ('localhost', 10000)
sock.bind(server_address)
sock.listen(1)

unpacker = struct.Struct('I 2s f')

while True:
    print('\nwaiting for a connection')
    connection, client_address = sock.accept()
    try:
        data = connection.recv(unpacker.size)
        print('received {!r}'.format(binascii.hexlify(data)))

        unpacked_data = unpacker.unpack(data)
        print('unpacked:', unpacked_data)

    finally:
        connection.close()
複製程式碼

執行客戶端會產生:

$ python3 source/socket/socket_binary_client.py
values = (1, b'ab', 2.7)
sending b'0100000061620000cdcc2c40'
closing socket
複製程式碼

服務端顯示它收到的值:

$ python3 socket_binary_server.py

waiting for a connection
received b'0100000061620000cdcc2c40'
unpacked: (1, b'ab', 2.700000047683716)

waiting for a connection
複製程式碼

浮點值在打包和解包時會丟失一些精度,否則資料會按預期傳輸。要記住的一件事是,取決於整數的值,將其轉換為文字然後傳輸而不使用 struct 可能更高效。整數1在表示為字串時使用一個位元組,但在打包到結構中時使用四個位元組。

非阻塞通訊和超時

預設情況下,配置 socket 來傳送和接收資料,當套接字準備就緒時會阻塞程式執行。呼叫send()等待緩衝區空間可用於傳出資料,呼叫recv()等待其他程式傳送可讀取的資料。這種形式的 I/O 操作很容易理解,但如果兩個程式最終都在等待另一個傳送或接收資料,則可能導致程式低效,甚至死鎖。

有幾種方法可以解決這種情況。一種是使用單獨的執行緒分別與每個套接字進行通訊。但是,這可能會引入其他複雜的問題,即執行緒之間的通訊。另一個選擇是將套接字更改為不阻塞的,如果尚未準備好處理操作,則立即返回。使用setblocking()方法更改套接字的阻止標誌。預設值為1,表示阻止。0表示不阻塞。如果套接字已關閉阻塞並且尚未準備好,則會引發 socket.error 錯誤。

另一個解決方案是為套接字操作設定超時時間,呼叫 settimeout() 函式,引數是一個浮點值,表示在確定套接字未準備好之前要阻塞的秒數。超時到期時,引發 timeout 異常。

相關文件:

pymotw.com/3/socket/in…

相關文章