python之網路編程 本地的進程間通信(IPC)有很多種方式,但可以總結為下麵4類: 消息傳遞(管道、FIFO、消息隊列) 同步(互斥量、條件變數、讀寫鎖、文件和寫記錄鎖、信號量) 共用記憶體(匿名的和具名的) 遠程過程調用(Solaris門和Sun RPC) 但這些都不是本文的主題!我們要討論的是 ...
python之網路編程
本地的進程間通信(IPC)有很多種方式,但可以總結為下麵4類:
- 消息傳遞(管道、FIFO、消息隊列)
- 同步(互斥量、條件變數、讀寫鎖、文件和寫記錄鎖、信號量)
- 共用記憶體(匿名的和具名的)
- 遠程過程調用(Solaris門和Sun RPC)
但這些都不是本文的主題!我們要討論的是網路中進程之間如何通信?首要解決的問題是如何唯一標識一個進程,否則通信無從談起!在本地可以通過進程PID來唯一標識一個進程,但是在網路中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網路層的“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不需要在客戶機和伺服器之間建立連接,它只是在地址之間傳輸報文。UDP的一個很好特性在於它的包是自分隔的(self-delimiting),也就是一個數據報都準確地指出它的開始和結束位置。然而,UDP的一個可能的缺點在於,它不保證包將會按順序到達,甚至根本就不保證。當然,建立在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地址和埠號,就可以直接發數據包。但是,能不能到達就不知道了。
雖然用UDP傳輸數據不可靠,但它的優點是和TCP比,速度快,對於不要求可靠到達的數據,就可以使用UDP協議。
我們來看看如何通過UDP協議傳輸數據。和TCP類似,使用UDP的通信雙方也分為客戶端和伺服器。伺服器首先需要綁定埠:
1 2 3 |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 綁定埠:
s.bind(( '127.0.0.1' , 9999 ))
|
創建Socket時,SOCK_DGRAM
指定了這個Socket的類型是UDP。綁定埠和TCP一樣,但是不需要調用listen()
方法,而是直接接收來自任何客戶端的數據:
1 2 3 4 5 6 |
print 'Bind UDP on 9999...'
while True :
# 接收數據:
data, addr = s.recvfrom( 1024 )
print 'Received from %s:%s.' % addr
s.sendto( 'Hello, %s!' % data, addr)
|
recvfrom()
方法返回數據和客戶端的地址與埠,這樣,伺服器收到數據後,直接調用sendto()
就可以把數據用UDP發給客戶端。
註意這裡省掉了多線程,因為這個例子很簡單。
客戶端使用UDP時,首先仍然創建基於UDP的Socket,然後,不需要調用connect()
,直接通過sendto()
給伺服器發數據:
1 2 3 4 5 6 7 |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [ 'Michael' , 'Tracy' , 'Sarah' ]:
# 發送數據:
s.sendto(data, ( '127.0.0.1' , 9999 ))
# 接收數據:
print s.recv( 1024 )
s.close()
|
從伺服器接收數據仍然調用recv()
方法。
小結
UDP的使用與TCP類似,但是不需要建立連接。此外,伺服器綁定UDP埠和TCP埠互不衝突,也就是說,UDP的9999埠與TCP的9999埠可以各自綁定。
Python 提供了兩個級別訪問的網路服務。:
- 低級別的網路服務支持基本的 Socket,它提供了標準的 BSD Sockets API,可以訪問底層操作系統Socket介面的全部方法。
- 高級別的網路服務模塊 SocketServer, 它提供了伺服器中心類,可以簡化網路伺服器的開發。