@Author : Roger TX (425144880@qq.com)
@Link : https://github.com/paotong999
一、計(jì)算機(jī)網(wǎng)絡(luò)協(xié)議
計(jì)算機(jī)網(wǎng)絡(luò)就是把各個(gè)計(jì)算機(jī)連接到一起,讓網(wǎng)絡(luò)中的計(jì)算機(jī)可以互相通信。網(wǎng)絡(luò)編程就是如何在程序中實(shí)現(xiàn)兩臺(tái)計(jì)算機(jī)的通信。而確切地說,網(wǎng)絡(luò)通信是兩臺(tái)計(jì)算機(jī)上的兩個(gè)進(jìn)程之間的通信。
1、OSI 參考模型和 TCP/IP 分層模型
- OSI 參考模型把計(jì)算機(jī)網(wǎng)絡(luò)分成物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層、會(huì)話層、表示層、應(yīng)用層七層,OSI 模式已成為各種計(jì)算機(jī)網(wǎng)絡(luò)結(jié)構(gòu)的參考標(biāo)準(zhǔn)。
- IP協(xié)議(Internet Protocol)又稱網(wǎng)際協(xié)議,是支持網(wǎng)間互聯(lián)的數(shù)據(jù)報(bào)協(xié)議。IP 協(xié)議提供了網(wǎng)間連接的完善功能,包括 IP 數(shù)據(jù)報(bào)規(guī)定的互聯(lián)網(wǎng)絡(luò)范圍內(nèi)的地址格式。
- TCP協(xié)議(Transmission Control Protocol)又稱傳輸控制協(xié)議,它規(guī)定了一種可靠的數(shù)據(jù)信息傳遞服務(wù)。
雖然 IP 和 TCP 這兩個(gè)協(xié)議的功能不盡相同,也可以分開單獨(dú)使用,但它們是在同一個(gè)時(shí)期作為一個(gè)協(xié)議來設(shè)計(jì)的,并且在功能上是互補(bǔ)的,因此在實(shí)際使用中常常把這兩個(gè)協(xié)議統(tǒng)稱為 TCP/IP 協(xié)議。
OSI 參考模型和 TCP/IP 分層模型的大致對(duì)應(yīng)關(guān)系

2、TCP/IP協(xié)議和端口號(hào)
TCP/IP協(xié)議
- IP地址實(shí)際上是一個(gè)32位整數(shù)(稱為IPv4),以字符串表示的IP地址如192.168.0.1實(shí)際上是把32位整數(shù)按8位分組后的數(shù)字表示,目的是便于閱讀。
- IPv6地址實(shí)際上是一個(gè)128位整數(shù),它是目前使用的IPv4的升級(jí)版,以字符串表示類似于
2001:0db8:85a3:0042:1000:8a2e:0370:7334。 - TCP協(xié)議則是建立在IP協(xié)議之上的。TCP協(xié)議負(fù)責(zé)在兩臺(tái)計(jì)算機(jī)之間建立可靠連接,保證數(shù)據(jù)包按順序到達(dá)。TCP協(xié)議會(huì)通過握手建立連接,然后,對(duì)每個(gè)IP包編號(hào),確保對(duì)方按順序收到,如果包丟掉了,就自動(dòng)重發(fā)。
- 一個(gè)TCP報(bào)文除了包含要傳輸?shù)臄?shù)據(jù)外,還包含源IP地址和目標(biāo)IP地址,源端口和目標(biāo)端口。
端口號(hào)
不同的應(yīng)用程序處理不同端口上的數(shù)據(jù),在同一臺(tái)機(jī)器中不能有兩個(gè)程序使用同一個(gè)端口。
端口號(hào)可以為 0~65535,通常將端口分為如下三類:
- 公認(rèn)端口(Well Known Port):端口號(hào)為 0~1023,它們緊密地綁定(Binding)一些特定的服務(wù)。
- 注冊(cè)端口(Registered Port):端口號(hào)為 1024~49151,它們松散地綁定一些服務(wù)。應(yīng)用程序通常應(yīng)該使用這個(gè)范圍內(nèi)的端口。
- 動(dòng)態(tài)和/或私有端口(Dynamic and/or Private Port):端口號(hào)為 49152~65535,這些端口是應(yīng)用程序使用的動(dòng)態(tài)端口,應(yīng)用程序一般不會(huì)主動(dòng)使用這些端口。
二、Python網(wǎng)絡(luò)編程模塊

- 網(wǎng)絡(luò)接口層和網(wǎng)絡(luò)層都是協(xié)議底層,通常來說,很少會(huì)直接基于底層進(jìn)行應(yīng)用程序編程。
- 傳輸層協(xié)議主要是 TCP 和 UDP,Python 提供了 socket 等模塊針對(duì)傳輸層協(xié)議進(jìn)行編程。
- 應(yīng)用層協(xié)議更豐富,F(xiàn)TP、HTTP、TELNET 等協(xié)議都屬于應(yīng)用層協(xié)議,Python 同樣為基于應(yīng)用層協(xié)議的編程提供了豐富的支持。
Python 標(biāo)準(zhǔn)庫中的網(wǎng)絡(luò)相關(guān)模塊
| 模塊 | 描述 |
|---|---|
| socke | 基于傳輸層 TCP、UDP 協(xié)議進(jìn)行網(wǎng)絡(luò)編程的模塊 |
| asyncore | socket 模塊的異步版,支持基于傳輸層協(xié)議的異步通信 |
| asynchat | asyncore 的增強(qiáng)版 |
| cg | 基本的 CGI(Common Gateway Interface,早期開發(fā)動(dòng)態(tài)網(wǎng)站的技術(shù))支持 |
| E-mail 和 MLME 消息處理模塊 | |
| ftplib | 支持 FTP 協(xié)議的客戶端模塊 |
| httplib、http.client | 支持 HTTP 協(xié)議以及 HTTP 客戶揣的模塊 |
| imaplib | 支持 IMAP4 協(xié)議的客戶端模塊 |
| mailbox | 操作不同格式郵箱的模塊 |
| mailcap | 支持 Mailcap 文件處理的模塊 |
| nntplib | 支持 NTTP 協(xié)議的客戶端模塊 |
| smtplib | 支持 SMTP 協(xié)議(發(fā)送郵件)的客戶端模塊 |
| poplib | 支持 POP3 協(xié)議的客戶端模塊 |
| telnetlib | 支持TELNET 協(xié)議的客戶端模塊 |
| urllib及其子模塊 | 支持URL 處理的模塊 |
| xmlrpc、xmlrpc.server、xmlrpc.client | 支持XML-RPC協(xié)議的服務(wù)器端和客戶端模塊 |
三、Python socket建立TCP連接
程序在使用 socket 之前,必須先創(chuàng)建 socket 對(duì)象,可通過該類的如下構(gòu)造器來創(chuàng)建 socket 實(shí)例:
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
- family 參數(shù)用于指定網(wǎng)絡(luò)類型。該參數(shù)支持 socket.AF_UNIX(UNIX 網(wǎng)絡(luò))、socket.AF_INET(基于 IPv4 協(xié)議的網(wǎng)絡(luò))和 socket.AF_INET6(基于 IPv6 協(xié)議的網(wǎng)絡(luò))這三個(gè)常量。
- type 參數(shù)用于指定網(wǎng)絡(luò) Sock 類型。該參數(shù)可支持 SOCK_STREAM(默認(rèn)值,創(chuàng)建基于 TCP 協(xié)議的 socket)、SOCK_DGRAM(創(chuàng)建基于 UDP 協(xié)議的 socket)和 SOCK_RAW(創(chuàng)建原始 socket)。
- proto 參數(shù)用于指定協(xié)議號(hào),如果沒有特殊要求,該參數(shù)默認(rèn)為 0 ,并可以忽略。
作為服務(wù)器端使用的 socket 必須被綁定到指定 IP 地址和端口,并在該 IP 地址和端口進(jìn)行監(jiān)聽,接收來自客戶端的連接。 TCP 通信的服務(wù)器端編程的基本步驟:
- 服務(wù)器端先創(chuàng)建一個(gè) socket 對(duì)象(
socket.socket())。 - 服務(wù)器端 socket 將自己綁定到指定 IP 地址和端口(
socket.bind(address,host))。 - 服務(wù)器端 socket 調(diào)用 listen() 方法監(jiān)聽網(wǎng)絡(luò)。
- 程序采用循環(huán)不斷調(diào)用 socket 的 accept() 方法接收來自客戶端的連接。
- 發(fā)送數(shù)據(jù):使用 send() 方法。注意,sendto() 方法用于 UDP 協(xié)議的通信。
# 導(dǎo)入 socket 模塊
import socket
# 創(chuàng)建socket對(duì)象
s = socket.socket()
# 將socket綁定到本機(jī)IP和端口
s.bind(('192.168.1.88', 30000))
# 服務(wù)端開始監(jiān)聽來自客戶端的連接
s.listen()
while True:
# 每當(dāng)接收到客戶端socket的請(qǐng)求時(shí),該方法返回對(duì)應(yīng)的socket和遠(yuǎn)程地址
c, addr = s.accept()
print(c)
print('連接地址:', addr)
c.send('您好,您收到了服務(wù)器的新年祝福!'.encode('utf-8'))
# 關(guān)閉連接
c.close()
作為客戶端先創(chuàng)建一個(gè) socket 對(duì)象,然后調(diào)用 socket 的 connect() 方法建立與服務(wù)器端的連接,這樣就可以建立一個(gè)基于 TCP 協(xié)議的網(wǎng)絡(luò)連接。TCP 通信的客戶端編程的基本步驟大致歸納如下:
- 客戶端先創(chuàng)建一個(gè) socket 對(duì)象(
socket.socket())。 - 客戶端 socket 調(diào)用 connect() 方法連接遠(yuǎn)程服務(wù)器。
- 接收數(shù)據(jù):使用 recv() 方法。
# 導(dǎo)入socket模塊
import socket
# 創(chuàng)建socket對(duì)象
s = socket.socket()
# 連接遠(yuǎn)程主機(jī)
s.connect(('192.168.1.88', 30000)) # ①
print('--%s--' % s.recv(1024).decode('utf-8'))
s.close()
四、Python socket建立UDP連接
UDP協(xié)議,全稱 User Datagram Protocol,中文名稱為用戶數(shù)據(jù)報(bào)協(xié)議,主要用來支持那些需要在計(jì)算機(jī)之間傳輸數(shù)據(jù)的網(wǎng)絡(luò)連接。
UDP 是一種面向非連接的協(xié)議,面向非連接指的是在正式通信前不必與對(duì)方先建立連接,不管對(duì)方狀態(tài)就直接發(fā)送數(shù)據(jù)。至于對(duì)方是否可以接收到這些數(shù)據(jù),UDP 協(xié)議無法控制,所以說 UDP 是一種不可靠的協(xié)議。
UDP 協(xié)議和 TCP 協(xié)議簡(jiǎn)單對(duì)比如下:
- TCP 協(xié)議:可靠,傳輸大小無限制,但是需要連接建立時(shí)間,差錯(cuò)控制開銷大。
- UDP 協(xié)議:不可靠,差錯(cuò)控制開銷較小,傳輸大小限制在 64 KB以下,不需要建立連接。
- UDP 在通信實(shí)例的兩端各建立一個(gè) socket,但它們只是發(fā)送、接收數(shù)據(jù)的對(duì)象,沒有客戶端和服務(wù)器端的概念
socket建立UDP連接
程序在創(chuàng)建 socket 時(shí),可通過 type 參數(shù)指定該 socket 的類型,如果將該參數(shù)指定為 SOCK_DGRAM,則意味著創(chuàng)建基于 UDP 協(xié)議的 socket。程序可以通過如下兩個(gè)方法來發(fā)送和接收數(shù)據(jù):
- socket.sendto(bytes, address):將 bytes 數(shù)據(jù)發(fā)送到 address 地址。
- socket.recvfrom(bufsize[, flags]):接收數(shù)據(jù)。該方法可以同時(shí)返回 socket 中的數(shù)據(jù)和數(shù)據(jù)來源地址。
使用 UDP 協(xié)議的 socket 在發(fā)送數(shù)據(jù)時(shí)必須使用 sendto() 方法,這是因?yàn)槌绦虮仨氈付òl(fā)送數(shù)據(jù)的目標(biāo)地址
使用 UDP 協(xié)議的 socket 在接收數(shù)據(jù)時(shí),既可使用普通的 recv() 方法,也可使用 recvfrom() 方法。如果程序需要得到數(shù)據(jù)報(bào)的來源,則應(yīng)該使用 recvfrom() 方法
作為服務(wù)器端使用的 socket
import socket
PORT = 30000;
# 定義每個(gè)數(shù)據(jù)報(bào)的大小最大為4KB
DATA_LEN = 4096;
# 定義一個(gè)字符串?dāng)?shù)組,服務(wù)器端發(fā)送該數(shù)組的元素
books = ("瘋狂Python講義",
"瘋狂Kotlin講義",
"瘋狂Android講義",
"瘋狂Swift講義")
# 通過type屬性指定創(chuàng)建基于UDP協(xié)議的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 將該socket綁定到本機(jī)的指定IP和端口
s.bind(('192.168.199.1', PORT))
# 采用循環(huán)接收數(shù)據(jù)
for i in range(1000):
# 讀取s中的數(shù)據(jù)的數(shù)據(jù)的發(fā)送地址
data, addr = s.recvfrom(DATA_LEN)
# 將接收到的內(nèi)容轉(zhuǎn)換成字符串后輸出
print(data.decode('utf-8'),addr)
# 從字符串?dāng)?shù)組中取出一個(gè)元素作為發(fā)送數(shù)據(jù)
send_data = books[i % 4].encode('utf-8')
# 將數(shù)據(jù)報(bào)發(fā)送給addr地址
s.sendto(send_data, addr)
s.close()
作為客戶端使用的 socket
import socket
PORT = 30000;
# 定義每個(gè)數(shù)據(jù)報(bào)的大小最大為4KB
DATA_LEN = 4096;
DEST_IP = "192.168.199.1";
# 通過type屬性指定創(chuàng)建基于UDP協(xié)議的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 不斷地讀取鍵盤輸入
while True:
line = input('')
if line is None or line == 'exit':
break
data = line.encode('utf-8')
# 發(fā)送數(shù)據(jù)報(bào)
s.sendto(data, (DEST_IP, PORT))
# 讀取socket中的數(shù)據(jù)
data = s.recv(DATA_LEN)
print(data.decode('utf-8'))
s.close()
五、Python smtplib模塊:發(fā)送郵件
使用 Python 的 smtplib 模塊來發(fā)送郵件非常簡(jiǎn)單,只需要按照如下 3 步來發(fā)送郵件即可:
- 連接 SMTP 服務(wù)器,并使用用戶名、密碼登錄服務(wù)器。
- 創(chuàng)建 EmailMessage 對(duì)象,該對(duì)象代表郵件本身。
- 調(diào)用代表與 SMTP 服務(wù)器連接的對(duì)象的 sendmail() 方法發(fā)送郵件。
- 要為郵件設(shè)置主題、發(fā)件人名字和收件人名字,需要設(shè)置 EmailMessage 對(duì)象的相應(yīng)屬性。
- 要將郵件改為 HTML ,需要調(diào)用 EmailMessage的set_content() 方法的第二個(gè)參數(shù),設(shè)置為 html。
import smtplib
from email.message import EmailMessage
smtp_server = 'smtp.qq.com' # 定義SMTP服務(wù)器地址:
from_addr = '425144880@qq.com' # 定義發(fā)件人地址
password = '123456' # 定義登錄郵箱的密碼
to_addr = 'wozhufuni999@163.com' # 定義收件人地址
# 創(chuàng)建SMTP連接
#conn = smtplib.SMTP(smtp_server, 25)
conn = smtplib.SMTP_SSL(smtp_server,465)
conn.set_debuglevel(1) # smtplib 調(diào)試模式
conn.login(from_addr, password)
first_id = '0000001'
# 創(chuàng)建郵件對(duì)象
msg = EmailMessage()
# 設(shè)置郵件內(nèi)容
msg.set_content('<h2>郵件內(nèi)容</h2>'
'<p>您好,這是一封來自Python的郵件<p>'
'<img src="cid:' + first_id +'"><p>'
'來自<a html', 'utf-8')
msg['subject'] = '一封HTML郵件'
msg['from'] = '田翔<%s>' % from_addr
msg['to'] = '新用戶 <%s>' % to_addr
with open('E:/logo.jpg', 'rb') as f:
# 添加附件
msg.add_attachment(f.read(), maintype='image',
subtype='jpeg', filename='test.png', cid=first_id)
# 發(fā)送郵件
conn.sendmail(from_addr, [to_addr], msg.as_string())
# 退出連接
conn.quit()
為了給郵件添加附件,只需調(diào)用 EmailMessage的add_attachment() 方法即可。該方法支持很多參數(shù),最常見的參數(shù)如下:
maintype:指定附件的主類型。比如指定 image 代表附件是圖片。
subtype:指定附件的子類型。比如指定為 png,代表附件是 PNG 圖片。一般來說,子類型受主類型的限制。
filename:指定附件的文件名。
cid=img:指定附件的資源 ID,郵件正文可通過資源 ID 來引用該資源。
六、Python poplib模塊:收取郵件
使用 poplib 收取郵件可分為兩步:
- 使用 poplib.POP3 或 poplib.POP3_SSL 按 POP3 協(xié)議從服務(wù)器端下載郵件。
- 使用 email.parser.Parser 或 email.parser.BytesParser 解析郵件內(nèi)容,得到 EmailMessage 對(duì)象,從 EmailMessage 對(duì)象中讀取郵件內(nèi)容。
import poplib, os.path, mimetypes
from email.parser import BytesParser, Parser
from email.policy import default
# 輸入郵件地址, 口令和POP3服務(wù)器地址:
email = 'tx@henghaodata.com'
password = '123456'
pop3_server = 'pop-ent.21cn.com'
# 連接到POP 3服務(wù)器:
#conn = poplib.POP3(pop3_server, 110)
conn = poplib.POP3_SSL(pop3_server, 995)
# 可以打開或關(guān)閉調(diào)試信息:
#conn.set_debuglevel(1)
# 可選:打印POP 3服務(wù)器的歡迎文字:
print(conn.getwelcome().decode('utf-8'))
# 輸入用戶名、密碼信息
# 相當(dāng)于發(fā)送POP 3的user命令
conn.user(email)
# 相當(dāng)于發(fā)送POP 3的pass命令
conn.pass_(password)
# 獲取郵件統(tǒng)計(jì)信息,相當(dāng)于發(fā)送POP 3的stat命令
message_num, total_size = conn.stat()
print('郵件數(shù): %s. 總大小: %s' % (message_num, total_size))
# 獲取服務(wù)器上的郵件列表,相當(dāng)于發(fā)送POP 3的list命令
# resp保存服務(wù)器的響應(yīng)碼
# mails列表保存每封郵件的編號(hào)、大小
resp, mails, octets = conn.list()
print(resp, mails)
# 獲取指定郵件的內(nèi)容(此處傳入總長(zhǎng)度,也就是獲取最后一封郵件)
# 相當(dāng)于發(fā)送POP 3的retr命令
# resp保存服務(wù)器的響應(yīng)碼
# data保存該郵件的內(nèi)容
resp, data, octets = conn.retr(len(mails))
# 將data的所有數(shù)據(jù)(原本是一個(gè)字節(jié)列表)拼接在一起
msg_data = b'\r\n'.join(data)
# 將字符串內(nèi)容解析成郵件,此處一定要指定policy=default
msg = BytesParser(policy=default).parsebytes(msg_data) #①
print(type(msg))
print('發(fā)件人:' + msg['from'])
print('收件人:' + msg['to'])
print('主題:' + msg['subject'])
print('第一個(gè)收件人名字:' + msg['to'].addresses[0].username)
print('第一個(gè)發(fā)件人名字:' + msg['from'].addresses[0].username)
for part in msg.walk():
counter = 1
# 如果maintype是multipart,說明是容器(用于包含正文、附件等)
if part.get_content_maintype() == 'multipart' :
continue
# 如果maintype是multipart,說明是郵件正文部分
elif part.get_content_maintype() == 'text':
print(part.get_content())
# 處理附件
else :
# 獲取附件的文件名
filename = part.get_filename()
# 如果沒有文件名,程序要負(fù)責(zé)為附件生成文件名
if not filename:
# 根據(jù)附件的contnet_type來推測(cè)它的后綴名
ext = mimetypes.guess_extension(part.get_content_type())
# 如果推測(cè)不出后綴名
if not ext:
# 使用.bin作為后綴名
ext = '.bin'
# 程序?yàn)楦郊砩晌募? filename = 'part-%03d%s' % (counter, ext)
counter += 1
# 將附件寫入的本地文件
with open(os.path.join('.', filename), 'wb') as fp:
fp.write(part.get_payload(decode=True))
# 退出服務(wù)器,相當(dāng)于發(fā)送POP 3的quit命令
conn.quit()
上面程序通過 poplib 模塊使用 POP3 命令從服務(wù)器端下載郵件,其實(shí)就是依次發(fā)送 user、pass、stat、list、retr 命令的過程。當(dāng) retr 命令執(zhí)行完成后,將得到最后一封郵件的數(shù)據(jù) data,該 data 是一個(gè) list 列表,因此程序需要先將這些數(shù)據(jù)拼接成一個(gè)整體,然后將郵件數(shù)據(jù)恢復(fù)成 EmailMessage 對(duì)象。這里有一點(diǎn)需要指出,程序在創(chuàng)建 BytesParser(解析字節(jié)串格式的郵件數(shù)據(jù))或 Parser(解析字符串格式的郵件數(shù)據(jù))時(shí),必須指定 policy=default
如果程序要讀取 EmailMessage 的各部分,則需要調(diào)用該對(duì)象的 walk() 方法,該方法返回一個(gè)可迭代對(duì)象,程序使用 for 循環(huán)遍歷 walk() 方法的返回值,對(duì)郵件內(nèi)容進(jìn)行逐項(xiàng)處理:
- 如果郵件某項(xiàng)的 maintype 是 'multipart',則說明這一項(xiàng)是容器,用于包含郵件內(nèi)容、附件等其他項(xiàng)。
- 如果郵件某項(xiàng)的 maintype 是 'text',則說明這一項(xiàng)的內(nèi)容是文本,通常就是郵件正文或文本附件。對(duì)于這種文本內(nèi)容,程序直接將其輸出到控制臺(tái)中。
- 如果郵件某項(xiàng)的 maintype 是其他,則說明這一項(xiàng)的內(nèi)容是附件,程序?qū)⒏郊?nèi)容保存在本地文件中。
運(yùn)行上面程序,可以看到程序收取了指定郵件的最后一封郵件,并將郵件內(nèi)容輸出到控制臺(tái)中,將郵件附件保存在本地文件中。