專欄地址:每週一個 Python 模塊html
Socket
提供了標準的 BSD Socket API,以便使用 BSD 套接字接口經過網絡進行通訊。它包括用於處理實際數據通道的類,還包括與網絡相關的功能,例如將服務器的名稱轉換爲地址以及格式化要經過網絡發送的數據。node
socket 是由程序用來傳遞數據或經過互聯網通訊的信道的一個端點。套接字有兩個主要屬性來控制它們發送數據的方式: 地址族控制所使用的 OSI 網絡層協議, 以及套接字類型控制傳輸層協議。python
Python 支持三種地址族。最多見的是 AF_INET
,用於 IPv4 網絡尋址。IPv4 地址長度爲四個字節,一般表示爲四個數字的序列,每八字節一個,用點分隔(例如,10.1.1.5
和127.0.0.1
)。這些值一般被稱爲 IP 地址。git
AF_INET6
用於IPv6 網絡尋址。IPv6 是網絡協議的下一代版本,支持 IPv4 下不可用的 128 位地址,流量整型和路由功能。IPv6 的採用率持續增加,特別是隨着雲計算的發展,以及因爲物聯網項目而添加到網絡中的額外設備的激增。程序員
AF_UNIX
是 Unix 域套接字(UDS)的地址族,它是 POSIX 兼容系統上可用的進程間通訊協議。UDS 的實現容許操做系統將數據直接從一個進程傳遞到另外一個進程,而無需經過網絡堆棧。這比使用 AF_INET
更高效,但因爲文件系統用做尋址的命名空間,所以 UDS 僅限於同一系統上的進程。使用 UDS 而不是其餘 IPC 機制(如命名管道或共享內存)的吸引力在於編程接口與 IP 網絡相同,所以應用程序能夠在單個主機上運行時高效通訊。github
注意:AF_UNIX
常量僅定義在支持 UDS 的系統上。算法
套接字類型一般用 SOCK_DGRAM
處理面向消息的數據報傳輸,用 SOCK_STREAM
處理面向字節流的傳輸。數據報套接字一般與 UDP(用戶數據報協議)相關聯 ,它們提供不可靠的單個消息傳遞。面向流的套接字與 TCP(傳輸控制協議)相關聯 。它們在客戶端和服務器之間提供字節流,經過超時管理,重傳和其餘功能確保消息傳遞或故障通知。數據庫
大多數提供大量數據的應用程序協議(如 HTTP)都是基於 TCP 構建的,由於它能夠在自動處理消息排序和傳遞時更輕鬆地建立複雜的應用程序。UDP 一般用於消息不過重要的協議(例如經過 DNS 查找名稱),或者用於多播(將相同數據發送到多個主機)。UDP 和 TCP 均可以與 IPv4 或 IPv6 尋址一塊兒使用。apache
注意: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()
採用幾個參數來過濾結果列表。host
和port
是必傳參數。可選的參數是family
, socktype
,proto
,和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
,服務器的規範名稱(可能與主機具備別名時用於查找的值不一樣)包含在結果中。若是沒有該標誌,則規範名稱將保留爲空。
用 C 編寫的網絡程序使用數據類型將 IP 地址表示爲二進制值(而不是一般在 Python 程序中的字符串地址)。要在 Python 表示和 C 表示之間轉換 IPv4 地址,請使用structsockaddr
、inet_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 地址已是十六進制值,所以將打包版本轉換爲一系列十六進制數字會生成相似於原始值的字符串。
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
使用建立成功鏈接的第一個配置返回已打開的鏈接參數。family
,type
和proto
屬性能夠用來檢查返回的類型是 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
同時監聽全部地址。儘管 socket
爲 INADDR_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 域套接字和 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_TTL
和 setsockopt()
設置 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.13
和B
地址 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
複製代碼
消息被髮送一次,而且接收到兩個傳出消息的確認,分別來自主機A
和B
。
套接字傳輸字節流。這些字節能夠包含編碼爲字節的文本消息,如前面示例中所示,或者它們也能夠是由 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
異常。
相關文檔: