0.前言 代碼目錄: https://github.com/brandon rhodes/fopnp/tree/m/py3 0.1.網路實驗環境:理解客戶端與伺服器是如何通過網路進行通信的 每台機器通過一個Docker容器實現 0.1.1.數據機A和B下麵的客戶機(h1~h4)表示典型客戶端場景 ...
0.前言
代碼目錄: https://github.com/brandon-rhodes/fopnp/tree/m/py3
0.1.網路實驗環境:理解客戶端與伺服器是如何通過網路進行通信的
每台機器通過一個Docker容器實現
0.1.1.數據機A和B下麵的客戶機(h1~h4)表示典型客戶端場景,家庭或咖啡店(內部網路,不能訪問互聯網,如果要連互聯網,都通過數據機IP進行連接)
0.1.2.數據機通過ISP網關連接廣域網(主幹路由器,負責將數據包發送至與之相連的網路)
0.1.3.example.com及相連機器表示機房配置。沒有網路地址轉換或偽裝,互聯網上的各個客戶端可隨意訪問example.com後的三個伺服器提供的服務埠
0.1.4.ftp、mail、www伺服器運行正確配置的守護進程,Python腳本可以運行在其他機器,併成功連接到上述服務
0.1.5.所有伺服器成功安裝TLS證書,所有客戶機有example.com的簽名及安裝受信證書,及要求TLS認證的Python腳本可以成功獲取認證
0.1.6.可以在網路環境的任意一臺機器上連接並運行命令,可對網路中的任意一個點進行數據包追蹤,查看客戶端和服務端之間的網路數據傳輸情況
1.客戶端、伺服器網路編程
1.1.協議棧與庫
1.1.1.協議棧:複雜的網路服務建立在簡單網路服務的基礎上
1.1.2.使用Python庫(內置標準庫+三方庫),解析要使用的網路通信協議
網路編程就是選擇並使用一個已經支持所需網路操作的庫的過程。通過瞭解底層網路服務知識,除了可以理解網路庫的運行原理,還能夠在底層部分出現錯誤時,知道具體發生了什麼。
標準庫:http://docs.python.org/3/library/
三方庫:https://pypi.python.org/
谷歌地理編碼服務pygeocoder包
郵箱地址:207 N. Defiance St Archbold, OH
獲取該物理地址的緯度和經度:
安裝virtualenv來避免,在幾個月的開發後,Python安裝環境包含無用包及新安裝包與已安裝包不相容的問題。
win中,虛擬環境中包含Python二進位文件的目錄為Scripts,不是bin
virtualenv -p python3 geo_env
cd geo_env
ls
O]bin/ include/ lib/
.bin/activate
python -c 'import pygeocoder'
O]error
pygcocoder包未安裝,在虛擬環境中使用pip安裝pygeocoder包
pip install pygeocoder
python -c 'import pygeocoder'
#獲取經度與緯度 search1.py
#!/usr/bin/env python3
from pygeocoder import Geocoder
if __name__ == "__main__":
address = '207 N. Definace St, Archbold, OH'
print(Geocoder.geocode(address)[o].coordinates)
執行python3 search1.py
O](41.521954, -84.306691)
pygeocoder介面背後的原理是怎樣的?
後面將詳細學習如何在一個包含至少6層的網路協議棧的頂層構建這個複雜的服務
1.2.應用層:
如果需要自己為谷歌地圖API編寫客戶端:沒有使用直接提供地理編功能的三方庫,而是使用更底層的Requests庫。
#!/usr/bin/env python3
#/chapter01/search2.py
import requests
def geocode(address):
parameters = {'address': address, 'sensor': 'false'}
base = 'http://maps.googleapis.com/maps/api/geocode/json'
response = requests.get(base, params=parameters)
answer = response.json()
print(answer['results'][0]['geometry']['location']
if __name__ == '__main__':
gecode('207 N. Definace St, Archbold, OH')
執行python3 search2.py
O]{'lat': 41.521954, 'lng': -84.306691}
結果並不完全相同,JSON數據將結果封裝為對象,Requests庫以Python字典形式提供該對象
谷歌文檔:http://code.google.com/apis/maps/documentation/geocodeing/
search2.py沒有通過地址和緯度直接解決問題,而是通過構造URL,獲取查詢響應,然後將結果轉化為JSON,一步步解決問題
高層的代碼描述了查詢的意義,而底層的代碼展示了查詢的構造細節
1.3.協議的使用:
search2.py腳本構建了一個URL,並獲取了該URL查詢的響應文檔,為了使URL查詢看起來像一個基礎操作,Web瀏覽器做了很多事情。
URL可以獲取某個文檔的原因:描述了網路上該特定文檔的位置及獲取方法。提供了更底層協議查詢該文檔所需的指令,search2.py就能夠解析URL並獲取響應文檔了。
URL包含了協議的名稱,後面跟著保存文檔的主機名,最後是該主機上特定文檔的路徑。
Requests庫從谷歌獲取結果的具體原理,其實就是由HTTP提供的,如果不想使用Requests庫提供的功能,而是想直接使用HTTP來獲取結果,使用/chapter01/search3.py
#!/usr/bin/env python3
import http.client
import json
from urllib.parse import quote_plus
base = '/maps/api/geocode/json'
def geocode(address):
path = '{}?address={}&sensor=false'.format(base, quote_polus(address))
connection = http.client.HTTPConnection('maps.google.com')
connection.request('GET', path)
rawreply = connection.getresponse().read()
reply = json.loads(rawreply.decode('utf-8'))
print(reply['results'][o]['geometry']['location'])
if __name__ == '__main__':
geocode('207 N. Definace St, Archbold, OH')
該程式直接使用HTTP協議:請求連接特定主機->手動構造帶path參數的GET查詢->直接從HTTP連接獲取響應結果。
此方法沒有使用字典將查詢參數方便的表示為獨立的鍵值對,而是手動嵌入到查詢地址中。
要通過該方法完成查詢,需要在?後跟上&分隔的參數,這些參數通過name=value的形式表示
1.4.原始的網路會話
HTTP協議利用,現代操作系統提供的使用TCP協議在IP網路的不同程式間進行純文本網路會話的功能,在兩台機器間傳輸數據。
HTTP協議精確描述了兩台主機間通過TCP傳輸的信息格式,並以此提供HTTP的各項功能。
通過Python可以方便操作的網路協議棧的最底層:/chapter01/search4.py,像網路發送了一個原始文本信息作為請求,並收到了很多原始文本作為響應。
#!/usr/bin/env python3
import socket
from urllib.parse import quote_plus
request_text = """\
GET /maps/api/geocode/json?address={}&sensor=false HTTP/.1\r/n/
Host: maps.google.com:80\r\n\
User-Agent:search4.py (Foundations of Python Network Programming)\r\n\
Connection: close\r\n\
\r\n\
"""
def geocode(address):
sock = socket.socket()
sock.connect(('maps.google.com', 80))
request = request_text.format(quote_plus(address))
sock.sendall(request.encode('ascii'))
raw_reply = b''
while True:
more = sock.recv(4096)
if not more:
break
raw_reply += more
print(raw_reply.decode('utf-8'))
if __name__ == '__main__':
geocode('207 N. Defiance St, Archbold, OH')
search4.py本質的不同:深入到最底層:使用主機操作系統提供的原始socket()函數來支持IP網路上的網路通信。相當於C寫底層系統一樣
原始網路通信的過程就是發送與接收字元串的過程。發送的查詢是一個字元串,接收到的響應同樣也是一個字元串。
通過sendall()函數傳入的參數瞭解到該HTTP查詢的具體內容。查詢中包含了關鍵字GET,GET後跟著待獲取文檔的路徑以及支持的HTTP版本。
GET /maps/api/geocode/json?address=207+N.+Defiance+St%2C+Archbold%2C+OH&sensor=false HTTP/1.1
GET信息後跟著一些請求頭,每個請求頭包含了名稱、冒號、值。最後是請求結束的回車符和換行符
python search4.py
O]
HTTP/1.1 200 OK
Content-Type:
Date: Sat, 23 Nov 2013 18:34:30 GMT
Expires: Sun, 24 Nov 2013 18:34:30 GMT
Cache-Control:public, max-age=86500
Vary: Accept-Language
Access-Control_Allow-Origin: *
Server: mafe
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Alternate-Protocol: 80:quic
Connection: close
{
"results": [
{
...
"formatted_address": "207 North Definace Street, Archbold, OH 43502, USA",
"geometry": {
"location" : {
"lat" : 41.521954,
"lng" : -84.306691
},
...
},
"types" : [ "street_address" ]
}
],
"status": "OK"
}
HTTP響應的結構和HTTP請求類似。首先是狀態行,跟著一些響應頭,響應頭後有一個空行,接著就是響應體(JSON格式~JavaScripts數據結構),此JSON就是之前查詢的響應結果,描述了查詢谷歌地理編碼API返回的地理位置。
這裡所有狀態和響應頭都是使用httplib庫處理的底層細節。如果沒有沒有分層,網路通信的具體細節即為此。
1.5.層層深入
1.5.1.協議棧:先構建利用網路硬體在兩台PC間傳送文本字元串的原始會話功能,然後在此基礎上創建更複雜、更高層語義更豐富的對話(郵寄地址對應的地理位置)。
例子中分析的協議棧包含4層
谷歌地理編碼API,封裝了1)如何用URL表示地理信息查詢;2)如何獲取包含坐標信息的JSON數據
URL,標識了科通過HTTP獲取的文檔
HTTP層,支持面向文檔的命令(GET),操作使用了原始TCP/IP套接字
TCP/IP套接字,只處理位元組串的發送和接收
1.5.1.1.協議棧的每一層都使用了其底層協議提供的功能,並同時向上層協議提供服務。
1.5.1.2.Python對涉及的各網路層都提供了非常全面的支持。除非使用應用提供商定製的協議並定製請求的格式(pygeocoder連接谷歌服務),否則無需使用三方庫
1.5.1.3.使用的通信協議越底層,程式質量也明顯隨之下降。故應該儘可能使用標準庫或三方庫。
1.5.1.4.高層的網路協議(如解析街道地址的谷歌地理編碼API)通常會將底層網路細節隱藏,如果值使用過pygeocoder庫,可能永遠不會知道URL和HTTP是pygeocoder用來解決問題的底層機制。
1.5.1.5.socket()介面其實並不是查詢谷歌涉及的最底層的協議,套接字這一抽象其實也基於更底層的協議,只不過這些協議由操作系統管理,非Python
socket()API層以下的幾層:
1.5.2.1.傳輸控制協議(TCP),通過發送、接收以及重排數據包(packet)的小型網路星系,支持由位元組流組成的雙向網路會話。
1.5.2.2.網際協議(IP),該層處理不同電腦間數據包的發送
1.5.2.3.最底層的"鏈路層",負責在直接相連的電腦之間發送物理星系,由網路硬體設備組成,如乙太網埠和無線網卡
1.6.編碼與解碼
1.6.1.python3對字元串和底層位元組序列做了區分。
1.6.1.1.位元組(byte)是網路通信中實際傳輸的二進位數。每個位元組8位二進位,範圍從00000000到11111111,轉為十進位就是0到255.
1.6.1.2.字元(character)串包含了Unicode字元,如a、{,∅(空集)。每個Unicode字元均有一個編碼點(code point)的數字標識符與之對應。除非主動請求Python對字元和外部可見的實際位元組進行相互轉化,否則對使用者可見的只有字元。
1.6.2.位元組和字元串兩者間的相互操作為解碼(decoding)和編碼(encoding)
1.6.2.1.解碼:應用程式使用位元組時發生的。當應用程式從文件或網路接收到位元組時,程式就像一個一流間諜一樣,對通信通道間傳輸的原始位元組進行解密。
1.6.2.2.編碼:程式將字元串對外輸出時所實施的過程。此時應用程式使用一種編碼方法將字元串轉化為位元組。當電腦需要傳輸或存儲符號時,位元組才是真正使用的格式。
使用python3操作這兩個過程相當簡單。
1.6.3.1.使用decode()方法將讀入的位元組串轉化為字元串,
1.6.3.2.使用encode()方法對要輸出的字元串進行編碼。
if __name__ == '__main__':
# Translating from the outside world of bytes to Unicode characters.
input_bytes = b'\xff\xfe4\x001\x003\x00 \x00i\x00s\x00 \x00i\x00.\x00'
input_characters = input_bytes.decode('utf-16')
print(repr(input_characters))
# Translating chsracters back into bytes before sending them.
output_characters = 'We copy you down, Eagle.\n'
output_bytes = output_characters.encode('utf-8')
with open ('eagle.txt', 'wb') as f:
f.write(output_bytes)
註意在調用兩者的repr()方法時的區別:
1.6.4.1.位元組串由字母b開始,如b'Hello';
1.6.4.2.字元串則沒有起始字母,如'world'。
為了消除位元組串與字元串帶來的混淆,Py3只對字元串類型提供了大量的字元串方法。
1.7.網際協議(IP):為全世界通過互聯網連接的電腦賦予統一地址系統的一種機制,使得數據包能夠從互聯網的一段發送至另一段。理想情況下。網路瀏覽器無需瞭解具體使用哪種網路設備來傳輸數據包,就能夠連接上任意一臺主機。
1.7.1.網路互聯是通過物理鏈路將多台電腦連接,使之可以互相通信。
1.7.2.網際互聯是將相鄰的物理網路相連,使之形成更大的網路系統,如互聯網。
但兩者本質都是允許資源共用的精心設計的機制。
1.7.3.電腦中的各種各樣的資源都需要被共用,網路設備間進行共用的基本單元是數據包(packet),只要有需要,就可以交換。一個數據表是一串長度在幾位元組到幾千位元組間的位元組串,是網路設備間進行數據傳輸的基本單元。
1.7.4.數據包在物理層通常只有兩個屬性:包含的位元組串數據+目標傳輸地址。
1.7.5.物理數據包的地址一般是一個唯一的標識符,表示了再傳輸數據包的過程中,插入同一乙太網段的其他網卡或無線通道。
1.7.6.網卡負責發送並接收這樣的數據包,使得電腦操作系統不用關心網路是如何處理網線、電壓即信號這些細節的。
Python程式很少直接操作IP這麼底層的協議。
1.8.IP地址:最初版本為連接到萬維網的每台電腦分配了一個4位元組的地址,通常寫為由句點分隔的十進位數。每個十進位數表示地址的1位元組,因此,每個數的範圍是0到255。
由於純數字表示的地址不便記憶,人們使用主機名(hostname)來代替IP地址,只要鍵入google.com就可訪問谷歌,其實,它是將主機名解析到了類似74.125.67.103的地址,實際上通過互聯網將數據包傳輸到了該地址。
# 1-7 chapter01/getname.py 主機名轉換為IP地址
import socket
if __name__ == '__main__':
hostname = 'www.python.org'
addr = socket.gethostbyname(hostname)
print('The IP address of {} is {}'.format(hostname, addr))
O]The IP address of www.python.org is 151.101.40.223
1.8.1.無論一個互聯網應用程式看起來多麼新奇,實際上IP協議總是使用數字表示的IP地址來作為數據包傳輸的目標地址。
1.8.2.將主機名解析為IP地址這一複雜的細節是由操作系統來處理的,操作系統傾向於自己處理IP的多數操作細節,對於用戶及Python代碼不可見。
4位元組IP已經不夠用,又部署了IPv6的拓展地址機制,允許使用16位元組的地址:fe80::fcfd:4aff:fecf:ea4e,只要代碼從用戶處接收IP地址或主機名,將它們傳遞給網路庫來處理,那麼就永遠不需要考慮IPv4和v6的區別,運行代碼的操作系統會知道使用的IP版本,並作出相應的解析。
1.8.2.IP地址從左往右,前兩個直接表示某個機構,第3個位元組表示目標機器所在的特定子網,最後一個位元組將地址細化至該特定的機器或服務。
1.8.3.特殊IP地址段:
1.8.3.1.127...*:由機器上運行的本地應用程式使用。當程式連接到這一地址段中的地址時,其實是在與同一機器上的一些其他服務或程式交互。大多數只使用127.0.0.1,表示該程式的機器本身,通常可以通過主機名localhost來訪問
1.8.3.2.10...、172.16-31..、192.168..*:為私有子網(private subnet)預留的。
運營互聯網的機構保證,絕不會把這三個地址段中的任何地址分發給運行伺服器或服務的實體公司。故連接互聯網時,這些地址是沒有意義的,並不對應可連接的任一主機。
構建組織內部網路,可以隨意使用這些地址來自由分配內部的IP地址,不需讓外網訪問這些主機。
1.9.路由:根據目的IP地址,選擇將IP數據包發往何處
一旦程式請求操作系統向某一特定IP地址發送數據,操作系統就需要決定如何使用該機器連接的某一物理網路來傳輸數據。這一決定就叫路由(routing)
1.9.1. 如果IP地址形如127...*,那麼操作系統會知道數據包的目的地址,是本機運行的另一個程式,該數據包不會傳送給物理網路設備,直接通過操作系統內部數據複製轉交給另一應用程式。
1.9.2. 如果目的IP地址與本機處於同一子網,可以通過簡單檢查本地乙太網段、無線通道,或是其他任何網路信息來找到目標主機,將數據包發送給本地連接的機器。
1.9.3. 否則將數據包轉發給一臺網關機器(gateway machine),該網關將本地子網連接至互聯網,再決定將該數據包發往何處。
路由只是在網路邊緣時才這麼容易,Py應用程式很少運行在互聯網骨幹路由器上,所以實際情況幾乎全是簡單路由情形。
1.9.4. 同一子網中所有主機有著相同的IP地址首碼,信號表示地址可變部分,但ASCII信號並沒有插入到路由表中,而是通過結合IP地址和掩碼來表示子網。
1.9.5. 掩碼指出了某主機屬於某子網所需的高位比特匹配數。
1.9.5.1. 127.0.0.0/8:該模式指出地址的前8位(1位元組必須與127匹配,餘下的24位(3位元組)則可以是任意值。
1.9.5.2. 192.168.0.0/16:該模式匹配了屬於192.168私有地址段的任何IP地址,指出前16位必須完全匹配,後16位可以是任意值
1.9.5.3. 192.168.5.0/24:明確指定一個特定的獨立子網,最常見的子網掩碼。屬於該子網的機器只有最後1位元組不同,允許有256個不同的地址。通常, .0地址用來表示子網名, .255地址用作'廣播數據包'的目標地址,會被髮送到子網內的所有主機。這樣,就有254個地址隨機分配給電腦。 .1地址通常用於連接外網的網關,但有些公司/學校也會選擇其他地址。
py代碼直接使用主機操作系統體統的功能,去正確選擇數據包路由,和之前依靠操作系統來將主機名解析至IP地址是一樣的。
1.10.數據包分組
IP支持的數據包極大,最大可至64KB,但是構建於IP網路之上的實際網路設備,通常並不支持這麼大的數據包,所以分組是必要的。乙太網只支持1500B的數據包。
網路數據包中包含一個表示"不分組"(DF,Don't Fragment)的標記,在源電腦與目的電腦之間的某條物理網路無法容納太大的數據包時,發送者可以通過這個標記選擇是否進行分組。
1.10.1.如果沒有設置DF標記,允許分組。當數據包大小超過網路能夠容納的上限時,網關能夠將其分為多個小數據包,併進行標記,表示接受方在接受之後需要將這些小數據包重組為原始大數據包。-->將大的拆分為小的,接收後再拼接成大的。
1.10.2.如果設置了DF標記,不允許分組。如果網路無法容納數據包,將會丟棄該數據包,併發回一條錯誤信息。錯誤信息由特殊信號數據包表示,叫做Internet控制報文協議(ICMP)數據包。發送方在收到錯誤後,會嘗試將信息分割為較小的數據包重發
DF標記無法由Py程式控制,由操作系統來設置。-->一開始就一直發小的,接收小的
系統通常使用的邏輯:
1.10.3.如果正在進行一個,由網路鍵傳輸的獨立數據報組成的UDP會話,那麼操作系統不會設置DF標記,故無論需要傳輸多少數據,所有數據包都能到達接收方。
1.10.4.如果是TCP會話,TCP可能是由多達上千個數據包組成的長數據流,那麼系統將設置DF標記,選擇正確的數據包大小,使得TCP會話順暢進行,如果不這樣做,數據包會在途中不斷分組,從而使得會話較為低效。
一個互聯網子網能夠接收的最大數據包叫做最大傳輸單元(MTU),90年代,互聯網服務商(DSL鏈路電話公司)使用PPPoE,PPPoE對IP數據包進行封裝,封裝後大小隻有1492B,不是乙太網允許的1500B,使得很多預設1500B的網站措手不及,還使用錯誤的安全措施,阻塞了所有的ICMP數據包,收不到錯誤信息,就不知道1500B的數據包到達客戶的DSL鏈路時無法相容,導致小文件和網頁的訪問沒有問題,Telnet和SSH等互動式協議也都正常,這兩種操作發送的數據包都小於1492B。一旦用戶嘗試下載一個大文件,或者Telnet/SSH一次性大量輸出好幾個屏幕的信息,那麼連接就會被凍結並無法響應。
1.11.進一步學習IP
餘下的章節,學習IP層之上的協議,描述IP的官方資源是IETF發佈的RFC文檔,可以瞭解到網際協議工作的每個細節,網址:http://tools.ietf.org/html/rfc791
或《TCP/IP詳解:協議》