進軍Python全棧開發--13.網路程式設計入門和網路應用開發

2020-08-13 21:06:29

網路程式設計入門

計算機網路基礎

計算機網路是獨立自主的計算機互聯而成的系統的總稱,組建計算機網路最主要的目的是實現多臺計算機之間的通訊和資源共用。今天計算機網路中的裝置和計算機網路的使用者已經多得不可計數,而計算機網路也可以稱得上是一個「複雜巨系統」,對於這樣的系統,我們不可能用一兩篇文章把它講清楚,有興趣的讀者可以自行閱讀Andrew S.Tanenbaum老師的經典之作《計算機網路》或Kurose和Ross老師合著的《計算機網路:自頂向下方法》來了解計算機網路的相關知識。

計算機網路發展史

  1. 1960s - 美國國防部ARPANET專案問世,奠定了分組交換網路的基礎。

    [外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-hTB6Gfgt-1597321380975)(./res/arpanet.png)]

  2. 1980s - 國際標準化組織(ISO)發佈OSI/RM,奠定了網絡技術標準化的基礎。

    [外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-zZSzyKbo-1597321380977)(./res/osimodel.png)]

  3. 1990s - 英國人TCP/IP模型

    實現網路通訊的基礎是網路通訊協定,這些協定通常是由區域網(LAN)中的內部IP地址,通過網路地址轉換(NAT)服務我們也可以實現對網路的存取。計算機網路上有大量的被我們稱爲「路由器」的網路中繼裝置,它們會儲存轉發我們發送到網路上的數據分組,讓從源頭髮出的數據最終能夠找到傳送到目的地通路,這項功能就是所謂的路由。

    TCP全稱傳輸控制協定,它是基於IP提供的定址和路由服務而建立起來的負責實現端到端可靠傳輸的協定,之所以將TCP稱爲可靠的傳輸協定是因爲TCP向呼叫者承諾了三件事情:

    1. 數據不傳丟不傳錯(利用握手、校驗和重傳機制 機製可以實現)。
    2. 流量控制(通過滑動視窗匹配數據發送者和接收者之間的傳輸速度)。
    3. 擁塞控制(通過RTT時間以及對滑動視窗的控制緩解網路擁堵)。

    網路應用模式

    1. C/S模式和B/S模式。這裏的C指的是Client(用戶端),通常是一個需要安裝到某個宿主操作系統上的應用程式;而B指的是Browser(瀏覽器),它幾乎是所有圖形化操作系統都預設安裝了的一個應用軟體;通過C或B都可以實現對S(伺服器)的存取。關於二者的比較和討論在網路上有一大堆的文章,在此我們就不再浪費筆墨了。
    2. 去中心化的網路應用模式。不管是B/S還是C/S都需要伺服器的存在,伺服器就是整個應用模式的中心,而去中心化的網路應用通常沒有固定的伺服器或者固定的用戶端,所有應用的使用者既可以作爲資源的提供者也可以作爲資源的存取者。

    基於HTTP協定的網路資源存取

    HTTP(超文字傳輸協定)

    HTTP是超文字傳輸協定(Hyper-Text Transfer Proctol)的簡稱,維基百科上對HTTP的解釋是:超文字傳輸協定是一種用於分佈式、共同作業式和超媒體資訊系統的應用層協定,它是萬維網數據通訊的基礎,設計HTTP最初的目的是爲了提供一種發佈和接收HTML頁面的方法,通過HTTP或者HTTPS(超文字傳輸安全協定)請求的資源由URI(統一資源識別符號)來標識。關於HTTP的更多內容,我們推薦閱讀阮一峯老師的《HTTP 協定入門》,簡單的說,通過HTTP我們可以獲取網路上的(基於字元的)資源,開發中經常會用到的網路API(有的地方也稱之爲網路數據介面)就是基於HTTP來實現數據傳輸的。

    JSON格式

    JSONJavaScript 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 Thread
    
    import requests
    
    
    # 繼承Thread類建立自定義的執行緒類
    class DownloadHanlder(Thread):
    
        def __init__(self, url):
            super().__init__()
            self.url = url
    
        def 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 datetime
    
    
    def 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
    

    [外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-BClMPfY4-1597321380986)(./res/telnet.png)]

    當然我們也可以通過Python的程式來實現TCP用戶端的功能,相較於實現伺服器程式,實現用戶端程式就簡單多了,程式碼如下所示。

    from socket import socket
    
    
    def 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 Thread
    
    
    def main():
        
        # 自定義執行緒類
        class FileTransferHandler(Thread):
    
            def __init__(self, cclient):
                super().__init__()
                self.cclient = cclient
    
            def 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 b64decode
    
    
    def main():
        client = socket()
        client.connect(('192.168.1.2', 5566))
        # 定義一個儲存二進制數據的物件
        in_data = bytes()
        # 由於不知道伺服器發送的數據有多大每次接收1024位元組
        data = client.recv(1024)
        while data:
            # 將收到的數據拼接起來
            in_data += data
            data = 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個字元表示從000000111111的64種狀態。維基百科上有關於Base64編碼的詳細講解,不熟悉Base64的讀者可以自行閱讀。

    說明: 上面的程式碼主要爲了講解網路程式設計的相關內容因此並沒有對異常狀況進行處理,請讀者自行新增例外處理程式碼來增強程式的健壯性。

    UDP通訊端

    傳輸層除了有可靠的傳輸協定TCP之外,還有一種非常輕便的傳輸協定叫做用戶數據報協定,簡稱UDP。TCP和UDP都是提供端到端傳輸服務的協定,二者的差別就如同打電話和發簡訊的區別,後者不對傳輸的可靠性和可達性做出任何承諾從而避免了TCP中握手和重傳的開銷,所以在強調效能和而不是數據完整性的場景中(例如傳輸網路音視訊數據),UDP可能是更好的選擇。可能大家會注意到一個現象,就是在觀看網路視訊時,有時會出現卡頓,有時會出現花屏,這無非就是部分數據傳丟或傳錯造成的。在Python中也可以使用UDP通訊端來建立網路應用,對此我們不進行贅述,有興趣的讀者可以自行研究。

    網路應用開發

    發送電子郵件

    在即時通訊軟體如此發達的今天,電子郵件仍然是網際網路上使用最爲廣泛的應用之一,公司嚮應聘者發出錄用通知、網站向使用者發送一個啓用賬號的鏈接、銀行向客戶推廣它們的理財產品等幾乎都是通過電子郵件來完成的,而這些任務應該都是由程式自動完成的。

    就像我們可以用HTTP(超文字傳輸協定)來存取一個網站一樣,發送郵件要使用SMTP(簡單郵件傳輸協定),SMTP也是一個建立在TCP(傳輸控制協定)提供的可靠數據傳輸服務的基礎上的應用級協定,它規定了郵件的發送者如何跟發送郵件的伺服器進行通訊的細節,而Python中的smtplib模組將這些操作簡化成了幾個簡單的函數。

    下面 下麪的程式碼演示瞭如何在Python發送郵件。

    from smtplib import SMTP
    from email.header import Header
    from email.mime.text import MIMEText
    
    
    def main():
        # 請自行修改下面 下麪的郵件發送者和接收者
        sender = '[email protected]'
        receivers = ['[email protected]', '[email protected]']
        message = MIMEText('用Python發送郵件的範例程式碼.', 'plain', 'utf-8')
        message['From'] = Header('王大錘', 'utf-8')
        message['To'] = Header('駱昊', 'utf-8')
        message['Subject'] = Header('範例程式碼實驗郵件', 'utf-8')
        smtper = SMTP('smtp.126.com')
        # 請自行修改下面 下麪的登錄口令
        smtper.login(sender, 'secretpass')
        smtper.sendmail(sender, receivers, message.as_string())
        print('郵件發送完成!')
    
    
    if __name__ == '__main__':
        main()
    

    如果要發送帶有附件的郵件,那麼可以按照下面 下麪的方式進行操作。

    from smtplib import SMTP
    from email.header import Header
    from email.mime.text import MIMEText
    from email.mime.image import MIMEImage
    from email.mime.multipart import MIMEMultipart
    
    import urllib
    
    
    def main():
        # 建立一個帶附件的郵件訊息物件
        message = MIMEMultipart()
        
        # 建立文字內容
        text_content = MIMEText('附件中有本月數據請查收', 'plain', 'utf-8')
        message['Subject'] = Header('本月數據', 'utf-8')
        # 將文字內容新增到郵件訊息物件中
        message.attach(text_content)
    
        # 讀取檔案並將檔案作爲附件新增到郵件訊息物件中
        with open('/Users/Hao/Desktop/hello.txt', 'rb') as f:
            txt = MIMEText(f.read(), 'base64', 'utf-8')
            txt['Content-Type'] = 'text/plain'
            txt['Content-Disposition'] = 'attachment; filename=hello.txt'
            message.attach(txt)
        # 讀取檔案並將檔案作爲附件新增到郵件訊息物件中
        with open('/Users/Hao/Desktop/彙總數據.xlsx', 'rb') as f:
            xls = MIMEText(f.read(), 'base64', 'utf-8')
            xls['Content-Type'] = 'application/vnd.ms-excel'
            xls['Content-Disposition'] = 'attachment; filename=month-data.xlsx'
            message.attach(xls)
        
        # 建立SMTP物件
        smtper = SMTP('smtp.126.com')
        # 開啓安全連線
        # smtper.starttls()
        sender = '[email protected]'
        receivers = ['[email protected]']
        # 登錄到SMTP伺服器
        # 請注意此處不是使用密碼而是郵件用戶端授權碼進行登錄
        # 對此有疑問的讀者可以聯繫自己使用的郵件伺服器客服
        smtper.login(sender, 'secretpass')
        # 發送郵件
        smtper.sendmail(sender, receivers, message.as_string())
        # 與郵件伺服器斷開連線
        smtper.quit()
        print('發送完成!')
    
    
    if __name__ == '__main__':
        main()
    

    發送簡訊

    發送簡訊也是專案中常見的功能,網站的註冊碼、驗證碼、行銷資訊基本上都是通過簡訊來發送給使用者的。在下面 下麪的程式碼中我們使用了互億無線簡訊平臺(該平臺爲註冊使用者提供了50條免費簡訊以及常用開發語言發送簡訊的demo,可以登錄該網站並在使用者自服務頁面中對簡訊進行設定)提供的API介面實現了發送簡訊的服務,當然國內的簡訊平臺很多,讀者可以根據自己的需要進行選擇(通常會考慮費用預算、簡訊達到率、使用的難易程度等指標),如果需要在商業專案中使用簡訊服務建議購買簡訊平臺提供的套餐服務。

    import urllib.parse
    import http.client
    import json
    
    
    def main():
        host  = "106.ihuyi.com"
        sms_send_uri = "/webservice/sms.php?method=Submit"
        # 下面 下麪的參數需要填入自己註冊的賬號和對應的密碼
        params = urllib.parse.urlencode({'account': '你自己的賬號', 'password' : '你自己的密碼', 'content': '您的驗證碼是:147258。請不要把驗證碼泄露給其他人。', 'mobile': '接收者的手機號', 'format':'json' })
        print(params)
        headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
        conn = http.client.HTTPConnection(host, port=80, timeout=30)
        conn.request('POST', sms_send_uri, params, headers)
        response = conn.getresponse()
        response_str = response.read()
        jsonstr = response_str.decode('utf-8')
        print(json.loads(jsonstr))
        conn.close()
    
    
    if __name__ == '__main__':
        main()