網絡編程
網絡編程對所有開發語言都是一樣的,Python也不例外。用Python進行網絡編程,就是在Python程序本身這個進程內,連接別的服務器進程的通信端口進行通信。
1. TCP/IP簡介
為了把全世界的所有不同類型的計算機都連接起來,就必須規定一套全球通用的協議,為了實現互聯網這個目標,互聯網協議簇(Internet Protocol Suite)就是通用協議標準。Internet是由inter和net兩個單詞組合起來的,原意就是連接“網絡”的網絡,有了Internet,任何私有網絡,只要支持這個協議,就可以聯入互聯網。
因為互聯網協議包含了上百種協議標準,但是最重要的兩個協議是TCP和IP協議,所以,大家把互聯網的協議簡稱TCP/IP協議。
通信的時候,雙方必須知道對方的標識,好比發郵件必須知道對方的郵件地址。互聯網上每個計算機的唯一標識就是IP地址,類123.123.123.123
。如果一臺計算機同時接入到兩個或更多的網絡,比如路由器,它就會有兩個或多個IP地址,所以,IP地址對應的實際上是計算機的網絡接口,通常是網卡。
P協議負責把數據從一臺計算機通過網絡發送到另一臺計算機。數據被分割成一小塊一小塊,然后通過IP包發送出去。由于互聯網鏈路復雜,兩臺計算機之間經常有多條線路,因此,路由器就負責決定如何把一個IP包轉發出去。IP包的特點是按塊發送,途徑多個路由,但不保證能到達,也不保證順序到達。
IP地址實際上是一個32位整數(稱為IPv4),以字符串表示的IP地址如192.168.0.1實際上是把32位整數按8位分組后的數字表示,目的是便于閱讀。
IPv6地址實際上是一個128位整數,它是目前使用的IPv4的升級版,以字符串表示類似于2001:0db8:85a3:0042:1000:8a2e:0370:7334
。
TCP協議則是建立在IP協議之上的。TCP協議負責在兩臺計算機之間建立可靠連接,保證數據包按順序到達。TCP協議會通過握手建立連接,然后,對每個IP包編號,確保對方按順序收到,如果包丟掉了,就自動重發。
許多常用的更高級的協議都是建立在TCP協議基礎上的,比如用于瀏覽器的HTTP協議、發送郵件的SMTP協議等。
一個IP包除了包含要傳輸的數據外,還包含源IP地址和目標IP地址,源端口和目標端口。
端口有什么作用?在兩臺計算機通信時,只發IP地址是不夠的,因為同一臺計算機上跑著多個網絡程序。一個IP包來了之后,到底是交給瀏覽器還是QQ,就需要端口號來區分。每個網絡程序都向操作系統申請唯一的端口號,這樣,兩個進程在兩臺計算機之間建立網絡連接就需要各自的IP地址和各自的端口號。
一個進程也可能同時與多個計算機建立鏈接,因此它會申請很多端口。
2. TCP編程
Socket是網絡編程的一個抽象概念。通常我們用一個Socket表示“打開了一個網絡鏈接”,而打開一個Socket需要知道目標計算機的IP地址和端口號,再指定協議類型即可。
客戶端
大多數連接都是可靠的TCP連接。創建TCP連接時,主動發起連接的叫客戶端,被動響應連接的叫服務器。
舉個例子,當我們在瀏覽器中訪問新浪時,我們自己的計算機就是客戶端,瀏覽器會主動向新浪的服務器發起連接。如果一切順利,新浪的服務器接受了我們的連接,一個TCP連接就建立起來的,后面的通信就是發送網頁內容了。
所以,我們要創建一個基于TCP連接的Socket,可以這樣做:
# 導入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的,可以任意使用。
服務器
和客戶端編程相比,服務器編程就要復雜一些。
服務器進程首先要綁定一個端口并監聽來自其他客戶端的連接。如果某個客戶端連接過來了,服務器就與該客戶端建立Socket連接,隨后的通信就靠這個Socket連接了。
所以,服務器會打開固定端口(比如80)監聽,每來一個客戶端連接,就創建該Socket連接。由于服務器會有大量來自客戶端的連接,所以,服務器要能夠區分一個Socket連接是和哪個客戶端綁定的。一個Socket依賴4項:服務器地址、服務器端口、客戶端地址、客戶端端口來唯一確定一個Socket。
但是服務器還需要同時響應多個客戶端的請求,所以,每個連接都需要一個新的進程或者新的線程來處理,否則,服務器一次就只能服務一個客戶端了。詳情參考網址
3. UDP編程
TCP是建立可靠連接,并且通信雙方都可以以流的形式發送數據。相對TCP,UDP則是面向無連接的協議。
使用UDP協議時,不需要建立連接,只需要知道對方的IP地址和端口號,就可以直接發數據包。但是,能不能到達就不知道了。
雖然用UDP傳輸數據不可靠,但它的優點是和TCP比,速度快,對于不要求可靠到達的數據,就可以使用UDP協議。
我們來看看如何通過UDP協議傳輸數據。和TCP類似,使用UDP的通信雙方也分為客戶端和服務器。服務器首先需要綁定端口:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 綁定端口:
s.bind(('127.0.0.1', 9999))
創建Socket時,SOCK_DGRAM
指定了這個Socket的類型是UDP。綁定端口和TCP一樣,但是不需要調用listen()
方法,而是直接接收來自任何客戶端的數據:
print('Bind UDP on 9999...')
while True:
# 接收數據:
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)
recvfrom()
方法返回數據和客戶端的地址與端口,這樣,服務器收到數據后,直接調用sendto()就可以把數據用UDP發給客戶端。
注意這里省掉了多線程,因為這個例子很簡單。
客戶端使用UDP時,首先仍然創建基于UDP的Socket,然后,不需要調用connect(),直接通過sendto()給服務器發數據:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
# 發送數據:
s.sendto(data, ('127.0.0.1', 9999))
# 接收數據:
print(s.recv(1024).decode('utf-8'))
s.close()
從服務器接收數據仍然調用recv()
方法。
小結
UDP的使用與TCP類似,但是不需要建立連接。此外,服務器綁定UDP端口和TCP端口互不沖突,也就是說,UDP的9999端口與TCP的9999端口可以各自綁定。