【Python 100天重新手到大師】網絡編程入門

網絡編程入門

計算機網絡基礎

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

計算機網絡發展史

  1. 1960s - 美國國防部ARPANET項目問世,奠基了分組交換網絡的基礎。html

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

  3. 1990s - 英國人蒂姆·伯納斯-李發明了圖形化的瀏覽器,瀏覽器的簡單易用性使得計算機網絡迅速被普及。web

    在沒有瀏覽器的年代,上網是這樣的。數據庫

    有了瀏覽器之後,上網是這樣的。編程

TCP/IP模型

實現網絡通訊的基礎是網絡通訊協議,這些協議一般是由互聯網工程任務組 (IETF)制定的。所謂「協議」就是通訊計算機雙方必須共同聽從的一組約定,例如怎樣創建鏈接、怎樣互相識別等,網絡協議的三要素是:語法、語義和時序。構成咱們今天使用的Internet的基礎的是TCP/IP協議族,所謂協議族就是一系列的協議及其構成的通訊模型,咱們一般也把這套東西稱爲TCP/IP模型。與國際標準化組織發佈的OSI/RM這個七層模型不一樣,TCP/IP是一個四層模型,也就是說,該模型將咱們使用的網絡從邏輯上分解爲四個層次,自底向上依次是:網絡接口層、網絡層、傳輸層和應用層,以下圖所示。json

IP一般被翻譯爲網際協議,它服務於網絡層,主要實現了尋址和路由的功能。接入網絡的每一臺主機都須要有本身的IP地址,IP地址就是主機在計算機網絡上的身份標識。固然因爲IPv4地址的匱乏,咱們日常在家裏、辦公室以及其餘能夠接入網絡的公共區域上網時得到的IP地址並非全球惟一的IP地址,而是一個局域網(LAN)中的內部IP地址,經過網絡地址轉換(NAT)服務咱們也能夠實現對網絡的訪問。計算機網絡上有大量的被咱們稱爲「路由器」的網絡中繼設備,它們會存儲轉發咱們發送到網絡上的數據分組,讓從源頭髮出的數據最終可以找到傳送到目的地通路,這項功能就是所謂的路由。api

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
複製代碼

固然咱們也能夠經過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 = 'abcdefg@126.com'
    receivers = ['uvwxyz@qq.com', 'uvwxyz@126.com']
    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 = 'abcdefg@126.com'
    receivers = ['uvwxyz@qq.com']
    # 登陸到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()
複製代碼
相關文章
相關標籤/搜索