網絡編程入門
計算機網絡基礎
計算機網絡是獨立自主的計算機互聯而成的系統的總稱,組建計算機網絡最主要的目的是實現多臺計算機之間的通信和資源共享。今天計算機網絡中的設備和計算機網絡的用戶已經多得不可計數,而計算機網絡也可以稱得上是一個“復雜巨系統”,對于這樣的系統,我們不可能用一兩篇文章把它講清楚,有興趣的讀者可以自行閱讀Andrew S.Tanenbaum老師的經典之作《計算機網絡》或Kurose和Ross老師合著的《計算機網絡:自頂向下方法》來了解計算機網絡的相關知識。
計算機網絡發展史
-
1960s - 美國國防部ARPANET項目問世,奠定了分組交換網絡的基礎。
-
1980s - 國際標準化組織(ISO)發布OSI/RM,奠定了網絡技術標準化的基礎。
-
1990s - 英國人蒂姆·伯納斯-李發明了圖形化的瀏覽器,瀏覽器的簡單易用性使得計算機網絡迅速被普及。
在沒有瀏覽器的年代,上網是這樣的。
有了瀏覽器以后,上網是這樣的。
TCP/IP模型
實現網絡通信的基礎是網絡通信協議,這些協議通常是由互聯網工程任務組 (IETF)制定的。所謂“協議”就是通信計算機雙方必須共同遵從的一組約定,例如怎樣建立連接、怎樣互相識別等,網絡協議的三要素是:語法、語義和時序。構成我們今天使用的Internet的基礎的是TCP/IP協議族,所謂協議族就是一系列的協議及其構成的通信模型,我們通常也把這套東西稱為TCP/IP模型。與國際標準化組織發布的OSI/RM這個七層模型不同,TCP/IP是一個四層模型,也就是說,該模型將我們使用的網絡從邏輯上分解為四個層次,自底向上依次是:網絡接口層、網絡層、傳輸層和應用層,如下圖所示。
IP通常被翻譯為網際協議,它服務于網絡層,主要實現了尋址和路由的功能。接入網絡的每一臺主機都需要有自己的IP地址,IP地址就是主機在計算機網絡上的身份標識。當然由于IPv4地址的匱乏,我們平常在家里、辦公室以及其他可以接入網絡的公共區域上網時獲得的IP地址并不是全球唯一的IP地址,而是一個局域網(LAN)中的內部IP地址,通過網絡地址轉換(NAT)服務我們也可以實現對網絡的訪問。計算機網絡上有大量的被我們稱為“路由器”的網絡中繼設備,它們會存儲轉發我們發送到網絡上的數據分組,讓從源頭發出的數據最終能夠找到傳送到目的地通路,這項功能就是所謂的路由。
TCP全稱傳輸控制協議,它是基于IP提供的尋址和路由服務而建立起來的負責實現端到端可靠傳輸的協議,之所以將TCP稱為可靠的傳輸協議是因為TCP向調用者承諾了三件事情:
- 數據不傳丟不傳錯(利用握手、校驗和重傳機制可以實現)。
- 流量控制(通過滑動窗口匹配數據發送者和接收者之間的傳輸速度)。
- 擁塞控制(通過RTT時間以及對滑動窗口的控制緩解網絡擁堵)。
網絡應用模式
- C/S模式和B/S模式。這里的C指的是Client(客戶端),通常是一個需要安裝到某個宿主操作系統上的應用程序;而B指的是Browser(瀏覽器),它幾乎是所有圖形化操作系統都默認安裝了的一個應用軟件;通過C或B都可以實現對S(服務器)的訪問。關于二者的比較和討論在網絡上有一大堆的文章,在此我們就不再浪費筆墨了。
- 去中心化的網絡應用模式。不管是B/S還是C/S都需要服務器的存在,服務器就是整個應用模式的中心,而去中心化的網絡應用通常沒有固定的服務器或者固定的客戶端,所有應用的使用者既可以作為資源的提供者也可以作為資源的訪問者。
基于HTTP協議的網絡資源訪問
HTTP(超文本傳輸協議)
HTTP是超文本傳輸協議(Hyper-Text Transfer Proctol)的簡稱,維基百科上對HTTP的解釋是:超文本傳輸協議是一種用于分布式、協作式和超媒體信息系統的應用層協議,它是萬維網數據通信的基礎,設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法,通過HTTP或者HTTPS(超文本傳輸安全協議)請求的資源由URI(統一資源標識符)來標識。關于HTTP的更多內容,我們推薦閱讀阮一峰老師的《HTTP 協議入門》,簡單的說,通過HTTP我們可以獲取網絡上的(基于字符的)資源,開發中經常會用到的網絡API(有的地方也稱之為網絡數據接口)就是基于HTTP來實現數據傳輸的。
JSON格式
JSON(JavaScript Object Notation)是一種輕量級的數據交換語言,該語言以易于讓人閱讀的文字(純文本)為基礎,用來傳輸由屬性值或者序列性的值組成的數據對象。盡管JSON是最初只是Javascript中一種創建對象的字面量語法,但它在當下更是一種獨立于語言的數據格式,很多編程語言都支持JSON格式數據的生成和解析,Python內置的json模塊也提供了這方面的功能。由于JSON是純文本,它和XML一樣都適用于異構系統之間的數據交換,而相較于XML,JSON顯得更加的輕便和優雅。下面是表達同樣信息的XML和JSON,而JSON的優勢是相當直觀的。
XML的例子:
<?xml version="1.0" encoding="UTF-8"?>
<message><from>Alice</from><to>Bob</to><content>Will you marry me?</content>
</message>
JSON的例子:
{'from': 'Alice','to': 'Bob','content': 'Will you marry me?'
}
requests庫
requests是一個基于HTTP協議來使用網絡的第三庫,其官方網站有這樣的一句介紹它的話:“Requests是唯一的一個非轉基因的Python HTTP庫,人類可以安全享用。”簡單的說,使用requests庫可以非常方便的使用HTTP,避免安全缺陷、冗余代碼以及“重復發明輪子”(行業黑話,通常用在軟件工程領域表示重新創造一個已有的或是早已被優化過的基本方法)。前面的文章中我們已經使用過這個庫,下面我們還是通過requests來實現一個訪問網絡數據接口并從中獲取美女圖片下載鏈接然后下載美女圖片到本地的例子程序,程序中使用了天行數據提供的網絡API。
我們可以先通過pip安裝requests及其依賴庫。
pip install requests
如果使用PyCharm作為開發工具,可以直接在代碼中書寫import requests
,然后通過代碼修復功能來自動下載安裝requests。
from time import time
from threading import Threadimport requests# 繼承Thread類創建自定義的線程類
class DownloadHanlder(Thread):def __init__(self, url):super().__init__()self.url = urldef run(self):filename = self.url[self.url.rfind('/') + 1:]resp = requests.get(self.url)with open('/Users/Hao/' + filename, 'wb') as f:f.write(resp.content)def main():# 通過requests模塊的get函數獲取網絡資源# 下面的代碼中使用了天行數據接口提供的網絡API# 要使用該數據接口需要在天行數據的網站上注冊# 然后用自己的Key替換掉下面代碼的中APIKey即可resp = requests.get('http://api.tianapi.com/meinv/?key=APIKey&num=10')# 將服務器返回的JSON格式的數據解析為字典data_model = resp.json()for mm_dict in data_model['newslist']:url = mm_dict['picUrl']# 通過多線程的方式實現圖片下載DownloadHanlder(url).start()if __name__ == '__main__':main()
基于傳輸層協議的套接字編程
套接字這個詞對很多不了解網絡編程的人來說顯得非常晦澀和陌生,其實說得通俗點,套接字就是一套用C語言寫成的應用程序開發庫,主要用于實現進程間通信和網絡編程,在網絡應用開發中被廣泛使用。在Python中也可以基于套接字來使用傳輸層提供的傳輸服務,并基于此開發自己的網絡應用。實際開發中使用的套接字可以分為三類:流套接字(TCP套接字)、數據報套接字和原始套接字。
TCP套接字
所謂TCP套接字就是使用TCP協議提供的傳輸服務來實現網絡通信的編程接口。在Python中可以通過創建socket對象并指定type屬性為SOCK_STREAM來使用TCP套接字。由于一臺主機可能擁有多個IP地址,而且很有可能會配置多個不同的服務,所以作為服務器端的程序,需要在創建套接字對象后將其綁定到指定的IP地址和端口上。這里的端口并不是物理設備而是對IP地址的擴展,用于區分不同的服務,例如我們通常將HTTP服務跟80端口綁定,而MySQL數據庫服務默認綁定在3306端口,這樣當服務器收到用戶請求時就可以根據端口號來確定到底用戶請求的是HTTP服務器還是數據庫服務器提供的服務。端口的取值范圍是0~65535,而1024以下的端口我們通常稱之為“著名端口”(留給像FTP、HTTP、SMTP等“著名服務”使用的端口,有的地方也稱之為“周知端口”),自定義的服務通常不使用這些端口,除非自定義的是HTTP或FTP這樣的著名服務。
下面的代碼實現了一個提供時間日期的服務器。
from socket import socket, SOCK_STREAM, AF_INET
from datetime import datetimedef main():# 1.創建套接字對象并指定使用哪種傳輸服務# family=AF_INET - IPv4地址# family=AF_INET6 - IPv6地址# type=SOCK_STREAM - TCP套接字# type=SOCK_DGRAM - UDP套接字# type=SOCK_RAW - 原始套接字server = socket(family=AF_INET, type=SOCK_STREAM)# 2.綁定IP地址和端口(端口用于區分不同的服務)# 同一時間在同一個端口上只能綁定一個服務否則報錯server.bind(('192.168.1.2', 6789))# 3.開啟監聽 - 監聽客戶端連接到服務器# 參數512可以理解為連接隊列的大小server.listen(512)print('服務器啟動開始監聽...')while True:# 4.通過循環接收客戶端的連接并作出相應的處理(提供服務)# accept方法是一個阻塞方法如果沒有客戶端連接到服務器代碼不會向下執行# accept方法返回一個元組其中的第一個元素是客戶端對象# 第二個元素是連接到服務器的客戶端的地址(由IP和端口兩部分構成)client, addr = server.accept()print(str(addr) + '連接到了服務器.')# 5.發送數據client.send(str(datetime.now()).encode('utf-8'))# 6.斷開連接client.close()if __name__ == '__main__':main()
運行服務器程序后我們可以通過Windows系統的telnet來訪問該服務器,結果如下圖所示。
telnet 192.168.1.2 6789
當然我們也可以通過Python的程序來實現TCP客戶端的功能,相較于實現服務器程序,實現客戶端程序就簡單多了,代碼如下所示。
from socket import socketdef main():# 1.創建套接字對象默認使用IPv4和TCP協議client = socket()# 2.連接到服務器(需要指定IP地址和端口)client.connect(('192.168.1.2', 6789))# 3.從服務器接收數據print(client.recv(1024).decode('utf-8'))client.close()if __name__ == '__main__':main()
需要注意的是,上面的服務器并沒有使用多線程或者異步I/O的處理方式,這也就意味著當服務器與一個客戶端處于通信狀態時,其他的客戶端只能排隊等待。很顯然,這樣的服務器并不能滿足我們的需求,我們需要的服務器是能夠同時接納和處理多個用戶請求的。下面我們來設計一個使用多線程技術處理多個用戶請求的服務器,該服務器會向連接到服務器的客戶端發送一張圖片。
服務器端代碼:
from socket import socket, SOCK_STREAM, AF_INET
from base64 import b64encode
from json import dumps
from threading import Threaddef main():# 自定義線程類class FileTransferHandler(Thread):def __init__(self, cclient):super().__init__()self.cclient = cclientdef run(self):my_dict = {}my_dict['filename'] = 'guido.jpg'# JSON是純文本不能攜帶二進制數據# 所以圖片的二進制數據要處理成base64編碼my_dict['filedata'] = data# 通過dumps函數將字典處理成JSON字符串json_str = dumps(my_dict)# 發送JSON字符串self.cclient.send(json_str.encode('utf-8'))self.cclient.close()# 1.創建套接字對象并指定使用哪種傳輸服務server = socket()# 2.綁定IP地址和端口(區分不同的服務)server.bind(('192.168.1.2', 5566))# 3.開啟監聽 - 監聽客戶端連接到服務器server.listen(512)print('服務器啟動開始監聽...')with open('guido.jpg', 'rb') as f:# 將二進制數據處理成base64再解碼成字符串data = b64encode(f.read()).decode('utf-8')while True:client, addr = server.accept()# 啟動一個線程來處理客戶端的請求FileTransferHandler(client).start()if __name__ == '__main__':main()
客戶端代碼:
from socket import socket
from json import loads
from base64 import b64decodedef main():client = socket()client.connect(('192.168.1.2', 5566))# 定義一個保存二進制數據的對象in_data = bytes()# 由于不知道服務器發送的數據有多大每次接收1024字節data = client.recv(1024)while data:# 將收到的數據拼接起來in_data += datadata = client.recv(1024)# 將收到的二進制數據解碼成JSON字符串并轉換成字典# loads函數的作用就是將JSON字符串轉成字典對象my_dict = loads(in_data.decode('utf-8'))filename = my_dict['filename']filedata = my_dict['filedata'].encode('utf-8')with open('/Users/Hao/' + filename, 'wb') as f:# 將base64格式的數據解碼成二進制數據并寫入文件f.write(b64decode(filedata))print('圖片已保存.')if __name__ == '__main__':main()
在這個案例中,我們使用了JSON作為數據傳輸的格式(通過JSON格式對傳輸的數據進行了序列化和反序列化的操作),但是JSON并不能攜帶二進制數據,因此對圖片的二進制數據進行了Base64編碼的處理。Base64是一種用64個字符表示所有二進制數據的編碼方式,通過將二進制數據每6位一組的方式重新組織,剛好可以使用0~9的數字、大小寫字母以及“+”和“/”總共64個字符表示從000000
到111111
的64種狀態。維基百科上有關于Base64編碼的詳細講解,不熟悉Base64的讀者可以自行閱讀。
說明:上面的代碼主要為了講解網絡編程的相關內容因此并沒有對異常狀況進行處理,請讀者自行添加異常處理代碼來增強程序的健壯性。
UDP套接字
傳輸層除了有可靠的傳輸協議TCP之外,還有一種非常輕便的傳輸協議叫做用戶數據報協議,簡稱UDP。TCP和UDP都是提供端到端傳輸服務的協議,二者的差別就如同打電話和發短信的區別,后者不對傳輸的可靠性和可達性做出任何承諾從而避免了TCP中握手和重傳的開銷,所以在強調性能和而不是數據完整性的場景中(例如傳輸網絡音視頻數據),UDP可能是更好的選擇。可能大家會注意到一個現象,就是在觀看網絡視頻時,有時會出現卡頓,有時會出現花屏,這無非就是部分數據傳丟或傳錯造成的。在Python中也可以使用UDP套接字來創建網絡應用,對此我們不進行贅述,有興趣的讀者可以自行研究。