python基礎網路程式設計--轉 python之網路程式設計
本地的程序間通訊(IPC)有很多種方式,但可以總結為下面4類:
- 訊息傳遞(管道、FIFO、訊息佇列)
- 同步(互斥量、條件變數、讀寫鎖、檔案和寫記錄鎖、訊號量)
- 共享記憶體(匿名的和具名的)
- 遠端過程呼叫(Solaris門和Sun RPC)
但這些都不是本文的主題!我們要討論的是網路中程序之間如何通訊?首要解決的問題是如何唯一標識一個程序,否則通訊無從談起!在本地可以通過程序PID來唯一標識一個程序,但是在網路中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網路層的“ip地址
使用TCP/IP協議的應用程式通常採用應用程式設計介面:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網路程序之間的通訊。就目前而言,幾乎所有的應用程式都是採用socket,而現在又是網路時代,網路中程序通訊是無處不在,這就是我為什麼說“一切皆socket”。
網路程式設計對所有開發語言都是一樣的,Python也不例外。用Python進行網路程式設計,就是在Python程式本身這個程序內,連線別的伺服器程序的通訊埠進行通訊。
(1) IP、TCP和UDP
當您編寫socket應用程式的時候,您可以在使用TCP還是使用UDP之間做出選擇。它們都有各自的優點和缺點。
TCP是流協議,而UDP是資料報協議。換句話說,TCP在客戶機和伺服器之間建立持續的開放連線,在該連線的生命期內,位元組可以通過該連線寫出(並且保證順序正確)。然而,通過 TCP 寫出的位元組沒有內建的結構,所以需要高層協議在被傳輸的位元組流內部分隔資料記錄和欄位。
另一方面,UDP不需要在客戶機和伺服器之間建立連線,它只是在地址之間傳輸報文。
對於理解TCP和UDP之間的區別來說,一個有用的類比就是電話呼叫和郵寄信件之間的區別。在呼叫者用鈴聲通知接收者,並且接收者拿起聽筒之前,電話呼叫不是活動的。只要沒有一方結束通話,該電話通道就保持活動,但是在通話期間,他們可以自由地想說多少就說多少。來自任何一方的談話都按臨時的順序發生。另一方面,當你發一封信的時候,郵局在投遞時既不對接收方是否存在作任何保證,也不對信件投遞將花多長時間做出有力保證。接收方可能按與信件的傳送順序不同的順序接收不同的信件,並且傳送方也可能在他們傳送信件是交替地接收郵件。與(理想的)郵政服務不同,無法送達的信件總是被送到死信辦公室處理,而不再返回給傳送。
(2)對等方、埠、名稱和地址
除了TCP和UDP協議以外,通訊一方(客戶機或者伺服器)還需要知道的關於與之通訊的對方機器的兩件事情:IP地址或者埠。IP地址是一個32位的資料值,為了人們好記,一般用圓點分開的4組數字的形式來表示,比如:64.41.64.172。埠是一個16位的資料值,通常被簡單地表示為一個小於65536的數字。大多數情況下,該值介於10到100的範圍內。一個IP地址獲取送到某臺機器的一個數據包,而一個埠讓機器決定將該資料包交給哪個程序/服務(如果有的話)。這種解釋略顯簡單,但基本思路是正確的。
上面的描述幾乎都是正確的,但它也遺漏了一些東西。大多數時候,當人們考慮Internet主機(對等方)時,我們都不會記憶諸如64.41.64.172這樣的數字,而是記憶諸如gnosis.cx這樣的名稱。為了找到與某個特定主機名稱相關聯的IP地址,一般都使用域名伺服器(DNS),但是有時會首先使用本地查詢(經常是通過/etc/hosts的內容)。對於本教程,我們將一般地假設有一個IP地址可用,不過下面討論編寫名稱查詢程式碼。
(3)主機名稱解析
命令列實用程式nslookup可以被用來根據符號名稱查詢主機IP地址。實際上,許多常見的實用程式,比如ping或者網路配置工具,也會順便做同樣的事情。但是以程式設計方式做這樣的事情很簡單。
======================TCP/IP======================
應用層: 它只負責產生相應格式的資料 ssh ftp nfs cifs dns http smtp pop3
-----------------------------------
傳輸層: 定義資料傳輸的兩種模式:
TCP(傳輸控制協議:面向連線,可靠的,效率相對不高)
UDP(使用者資料報協議:非面向連線,不可靠的,但效率高)
-----------------------------------
網路層: 連線不同的網路如乙太網、令牌環網
IP (路由,分片) 、ICMP、 IGMP
ARP ( 地址解析協議,作用是將IP解析成MAC )
-----------------------------------
資料鏈路層: 乙太網傳輸
-----------------------------------
物理層: 主要任務是規定各種傳輸介質和介面與傳輸訊號相關的一些特性
-----------------------------------
TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標準的協議集,它是為廣域網(WANs)設計的。
TCP socket 由於在通向前需要建立連線,所以其模式較 UDP socket 負責些。
UDP(User Data Protocol,使用者資料報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。如圖:
UDP Socket圖:
UDP socket server 端程式碼在進行bind後,無需呼叫listen方法。
TCP/IP協議族包括運輸層、網路層、鏈路層,
而socket所在位置如圖,Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層。
Socket是什麼
socket起源於Unix,而Unix/Linux基本哲學之一就是“一切皆檔案”,都可以用“開啟open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現,socket即是一種特殊的檔案,一些socket函式就是對其進行的操作(讀/寫IO、開啟、關閉).
說白了Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。
注意:其實socket也沒有層的概念,它只是一個facade設計模式的應用,讓程式設計變的更簡單。是一個軟體抽象層。在網路程式設計中,我們大量用的都是通過socket實現的。
Socket是網路程式設計的一個抽象概念。通常我們用一個Socket表示“打開了一個網路連結”,而開啟一個Socket需要知道目標計算機的IP地址和埠號,再指定協議型別即可。
TCP程式設計
Socket是網路程式設計的一個抽象概念。通常我們用一個Socket表示“打開了一個網路連結”,而開啟一個Socket需要知道目標計算機的IP地址和埠號,再指定協議型別即可。 TCP連線簡圖: 三次握手,資料傳輸,四次揮手socket中TCP的三次握手建立連線詳解
我們知道tcp建立連線要進行“三次握手”,即交換三個分組。大致流程如下:
- 客戶端向伺服器傳送一個SYN J
- 伺服器向客戶端響應一個SYN K,並對SYN J進行確認ACK J+1
- 客戶端再想伺服器發一個確認ACK K+1
只有就完了三次握手,但是這個三次握手發生在socket的那幾個函式中呢?請看下圖:
圖1、socket中傳送的TCP三次握手
從圖中可以看出,當客戶端呼叫connect時,觸發了連線請求,向伺服器傳送了SYN J包,這時connect進入阻塞狀態;伺服器監聽到連線請求,即收到SYN J包,呼叫accept函式接收請求向客戶端傳送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到伺服器的SYN K ,ACK J+1之後,這時connect返回,並對SYN K進行確認;伺服器收到ACK K+1時,accept返回,至此三次握手完畢,連線建立。
總結:客戶端的connect在三次握手的第二個次返回,而伺服器端的accept在三次握手的第三次返回。
5、socket中TCP的四次握手釋放連線詳解
上面介紹了socket中TCP的三次握手建立過程,及其涉及的socket函式。現在我們介紹socket中的四次握手釋放連線的過程,請看下圖:
圖2、socket中傳送的TCP四次握手
圖示過程如下:
- 某個應用程序首先呼叫 close主動關閉連線,這時TCP傳送一個FIN M;
- 另一端接收到FIN M之後,執行被動關閉,對這個FIN進行確認。它的接收也作為檔案結束符傳遞給應用程序,因為FIN的接收意味著應用程序在相應的連線上再也接收不到額外資料;
- 一段時間之後,接收到檔案結束符的應用程序呼叫 close關閉它的socket。這導致它的TCP也傳送一個FIN N;
- 接收到這個FIN的源傳送端TCP對它進行確認。
這樣每個方向上都有一個FIN和ACK。
Python3 網路程式設計
Python 提供了兩個級別訪問的網路服務。:
- 低級別的網路服務支援基本的 Socket,它提供了標準的 BSD Sockets API,可以訪問底層作業系統Socket介面的全部方法。
- 高級別的網路服務模組 SocketServer, 它提供了伺服器中心類,可以簡化網路伺服器的開發。
什麼是 Socket?
Socket又稱"套接字",應用程式通常通過"套接字"向網路發出請求或者應答網路請求,使主機間或者一臺計算機上的程序間可以通訊。
socket和file的區別:
- file模組是針對某個指定檔案進行【開啟】【讀寫】【關閉】
- socket模組是針對 伺服器端 和 客戶端Socket 進行【開啟】【讀寫】【關閉】
伺服器端先初始化Socket,然後與埠繫結(bind),對埠進行監聽(listen),呼叫accept阻塞,等待客戶端連線。在這時如果有個客戶端初始化一個Socket,然後連線伺服器(connect),如果連線成功,這時客戶端與伺服器端的連線就建立了。客戶端傳送資料請求,伺服器端接收請求並處理請求,然後把迴應資料傳送給客戶端,客戶端讀取資料,最後關閉連線,一次互動結束。
socket()函式
Python 中,我們用 socket()函式來建立套接字,語法格式如下:
1 |
socket.socket([family[,
type
[, proto]]])
|
引數
- family: 套接字家族可以使AF_UNIX或者AF_INET
- type: 套接字型別可以根據是面向連線的還是非連線分為
SOCK_STREAM
或SOCK_DGRAM
- protocol: 一般不填預設為0.
簡單例項
服務端
我們使用 socket 模組的 socket 函式來建立一個 socket 物件。socket 物件可以通過呼叫其他函式來設定一個 socket 服務。
現在我們可以通過呼叫 bind(hostname, port) 函式來指定服務的 port(埠)。
接著,我們呼叫 socket 物件的 accept 方法。該方法等待客戶端的連線,並返回 connection 物件,表示已連線到客戶端。
完整程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#!/usr/bin/python3
# 檔名:server.py
# 匯入 socket、sys 模組
import
socket
import
sys
# 建立 socket 物件
serversocket
=
socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
# 獲取本地主機名
host
=
socket.gethostname()
port
=
9999
# 繫結埠
serversocket.bind((host, port))
# 設定最大連線數,超過後排隊
serversocket.listen(
5
)
while
True
:
# 建立客戶端連線
clientsocket,addr
=
serversocket.accept()
print
(
"連線地址: %s"
%
str
(addr))
msg
=
'歡迎訪問python教程!'
+
"\r\n"
clientsocket.send(msg.encode(
'utf-8'
))
clientsocket.close()
|
客戶端
接下來我們寫一個簡單的客戶端例項連線到以上建立的服務。埠號為 12345。
socket.connect(hosname, port ) 方法開啟一個 TCP 連線到主機為 hostname 埠為 port 的服務商。連線後我們就可以從服務端後期資料,記住,操作完成後需要關閉連線。
完整程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#!/usr/bin/python3
# 檔名:client.py
# 匯入 socket、sys 模組
import
socket
import
sys
# 建立 socket 物件
s
=
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 獲取本地主機名
host
=
socket.gethostname()
# 設定埠好
port
=
9999
# 連線服務,指定主機和埠
s.connect((host, port))
# 接收小於 1024 位元組的資料
msg
=
s.recv(
1024
)
s.close()
print
(msg.decode(
'utf-8'
))
|
先執行server端,然後開啟client端就能看到結果
客戶端
大多數連線都是可靠的TCP連線。建立TCP連線時,主動發起連線的叫客戶端,被動響應連線的叫伺服器。
舉個例子,當我們在瀏覽器中訪問新浪時,我們自己的計算機就是客戶端,瀏覽器會主動向新浪的伺服器發起連線。如果一切順利,新浪的伺服器接受了我們的連線,一個TCP連線就建立起來的,後面的通訊就是傳送網頁內容了。
所以,我們要建立一個基於TCP連線的Socket,可以這樣做:
1 2 3 4 5 6 7 |
# 匯入socket庫:
import
socket
# 建立一個socket:
s
=
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連線:
s.connect((
'www.sina.com.cn'
,
80
))
|
建立Socket
時,AF_INET
指定使用IPv4協議,如果要用更先進的IPv6,就指定為AF_INET6
。SOCK_STREAM
指定使用面向流的TCP協議,這樣,一個Socket
物件就建立成功,但是還沒有建立連線。
客戶端要主動發起TCP連線,必須知道伺服器的IP地址和埠號。新浪網站的IP地址可以用域名www.sina.com.cn
自動轉換到IP地址,但是怎麼知道新浪伺服器的埠號呢?
答案是作為伺服器,提供什麼樣的服務,埠號就必須固定下來。由於我們想要訪問網頁,因此新浪提供網頁服務的伺服器必須把埠號固定在80
埠,因為80
埠是Web服務的標準埠。其他服務都有對應的標準埠號,例如SMTP服務是25
埠,FTP服務是21
埠,等等。埠號小於1024的是Internet標準服務的埠,埠號大於1024的,可以任意使用。
因此,我們連線新浪伺服器的程式碼如下:
1 |
s.connect((
'www.sina.com.cn'
,
80
))
|
注意引數是一個tuple
,包含地址和埠號。
建立TCP連線後,我們就可以向新浪伺服器傳送請求,要求返回首頁的內容:
1 2 |
# 傳送資料:
s.send(b
'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n'
)
|
TCP連線建立的是雙向通道,雙方都可以同時給對方發資料。但是誰先發誰後發,怎麼協調,要根據具體的協議來決定。例如,HTTP協議規定客戶端必須先發請求給伺服器,伺服器收到後才發資料給客戶端。
傳送的文字格式必須符合HTTP標準,如果格式沒問題,接下來就可以接收新浪伺服器返回的資料了:
接收資料時,呼叫recv(max)
方法,一次最多接收指定的位元組數,因此,在一個while迴圈中反覆接收,直到recv()
返回空資料,表示接收完畢,退出迴圈。
當我們接收完資料後,呼叫close()
方法關閉Socket,這樣,一次完整的網路通訊就結束了:
1 2 |
# 關閉連線:
s.close()
|
接收到的資料包括HTTP頭和網頁本身,我們只需要把HTTP頭和網頁分離一下,把HTTP頭打印出來,網頁內容儲存到檔案:
1 2 3 4 5 |
header, html
=
data.split(b
'\r\n\r\n'
,
1
)
print
(header.decode(
'utf-8'
))
# 把接收的資料寫入檔案:
with
open
(
'sina.html'
,
'wb'
) as f:
f.write(html)
|
現在,只需要在瀏覽器中開啟這個sina.html
檔案,就可以看到新浪的首頁了。
伺服器
和客戶端程式設計相比,伺服器程式設計就要複雜一些。
伺服器程序首先要繫結一個埠並監聽來自其他客戶端的連線。如果某個客戶端連線過來了,伺服器就與該客戶端建立Socket連線,隨後的通訊就靠這個Socket連線了。
所以,伺服器會開啟固定埠(比如80)監聽,每來一個客戶端連線,就建立該Socket連線。由於伺服器會有大量來自客戶端的連線,所以,伺服器要能夠區分一個Socket連線是和哪個客戶端繫結的。一個Socket依賴4項:伺服器地址、伺服器埠、客戶端地址、客戶端埠來唯一確定一個Socket。
但是伺服器還需要同時響應多個客戶端的請求,所以,每個連線都需要一個新的程序或者新的執行緒來處理,否則,伺服器一次就只能服務一個客戶端了。
我們來編寫一個簡單的伺服器程式,它接收客戶端連線,把客戶端發過來的字串加上Hello
再發回去。
首先,建立一個基於IPv4和TCP協議的Socket:
1 |
s
=
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
然後,我們要繫結監聽的地址和埠。伺服器可能有多塊網絡卡,可以繫結到某一塊網絡卡的IP地址上,也可以用0.0.0.0
繫結到所有的網路地址,還可以用127.0.0.1
繫結到本機地址。127.0.0.1
是一個特殊的IP地址,表示本機地址,如果繫結到這個地址,客戶端必須同時在本機執行才能連線,也就是說,外部的計算機無法連線進來。
埠號需要預先指定。因為我們寫的這個服務不是標準服務,所以用9999
這個埠號。請注意,小於1024
的埠號必須要有管理員許可權才能繫結:
1 2 |
# 監聽埠:
s.bind((
'127.0.0.1'
,
9999
))
|
緊接著,呼叫listen()
方法開始監聽埠,傳入的引數指定等待連線的最大數量:
1 2 |
s.listen(
5
)
print
(
'Waiting for connection...'
)
|
接下來,伺服器程式通過一個永久迴圈來接受來自客戶端的連線,accept()
會等待並返回一個客戶端的連線:
1 2 3 4 5 6 |
while
True
:
# 接受一個新連線:
sock, addr
=
s.accept()
# 建立新執行緒來處理TCP連線:
t
=
threading.Thread(target
=
tcplink, args
=
(sock, addr))
t.start()
|
每個連線都必須建立新執行緒(或程序)來處理,否則,單執行緒在處理連線的過程中,無法接受其他客戶端的連線:
1 2 3 4 5 6 7 8 9 10 11 |
def
tcplink(sock, addr):
print
(
'Accept new connection from %s:%s...'
%
addr)
sock.send(b
'Welcome!'
)
while
True
:
data
=
sock.recv(
1024
)
time.sleep(
1
)
if
not
data
or
data.decode(
'utf-8'
)
=
=
'exit'
:
break
sock.send((
'Hello, %s!'
%
data.decode(
'utf-8'
)).encode(
'utf-8'
))
sock.close()
print
(
'Connection from %s:%s closed.'
%
addr)
|
連線建立後,伺服器首先發一條歡迎訊息,然後等待客戶端資料,並加上Hello
再發送給客戶端。如果客戶端傳送了exit
字串,就直接關閉連線。
要測試這個伺服器程式,我們還需要編寫一個客戶端程式:
1 2 3 4 5 6 7 8 9 10 11 |
s
=
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連線:
s.connect((
'127.0.0.1'
,
9999
))
# 接收歡迎訊息:
print
(s.recv(
1024
).decode(
'utf-8'
))
for
data
in
[b
'Michael'
, b
'Tracy'
, b
'Sarah'
]:
# 傳送資料:
s.send(data)
print
(s.recv(
1024
).decode(
'utf-8'
))
s.send(b
'exit'
)
s.close()
|
我們需要開啟兩個命令列視窗,一個執行伺服器程式,另一個執行客戶端程式,就可以看到效果了:
UDP程式設計
TCP是建立可靠連線,並且通訊雙方都可以以流的形式傳送資料。相對TCP,UDP則是面向無連線的協議。
使用UDP協議時,不需要建立連線,只需要知道對方的IP地址和埠