@Author : Roger TX (425144880@qq.com)
@Link : https://github.com/paotong999
一、计算机网络协议
计算机网络就是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。而确切地说,网络通信是两台计算机上的两个进程之间的通信。
1、OSI 参考模型和 TCP/IP 分层模型
- OSI 参考模型把计算机网络分成物理层、数据链路层、网络层、传输层、会话层、表示层、应用层七层,OSI 模式已成为各种计算机网络结构的参考标准。
- IP协议(Internet Protocol)又称网际协议,是支持网间互联的数据报协议。IP 协议提供了网间连接的完善功能,包括 IP 数据报规定的互联网络范围内的地址格式。
- TCP协议(Transmission Control Protocol)又称传输控制协议,它规定了一种可靠的数据信息传递服务。
虽然 IP 和 TCP 这两个协议的功能不尽相同,也可以分开单独使用,但它们是在同一个时期作为一个协议来设计的,并且在功能上是互补的,因此在实际使用中常常把这两个协议统称为 TCP/IP 协议。
OSI 参考模型和 TCP/IP 分层模型的大致对应关系
2、TCP/IP协议和端口号
TCP/IP协议
- IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。
- IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于
2001:0db8:85a3:0042:1000:8a2e:0370:7334
。 - TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
- 一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
端口号
不同的应用程序处理不同端口上的数据,在同一台机器中不能有两个程序使用同一个端口。
端口号可以为 0~65535,通常将端口分为如下三类:
- 公认端口(Well Known Port):端口号为 0~1023,它们紧密地绑定(Binding)一些特定的服务。
- 注册端口(Registered Port):端口号为 1024~49151,它们松散地绑定一些服务。应用程序通常应该使用这个范围内的端口。
- 动态和/或私有端口(Dynamic and/or Private Port):端口号为 49152~65535,这些端口是应用程序使用的动态端口,应用程序一般不会主动使用这些端口。
二、Python网络编程模块
四层网络模型及对应的协议- 网络接口层和网络层都是协议底层,通常来说,很少会直接基于底层进行应用程序编程。
- 传输层协议主要是 TCP 和 UDP,Python 提供了 socket 等模块针对传输层协议进行编程。
- 应用层协议更丰富,FTP、HTTP、TELNET 等协议都属于应用层协议,Python 同样为基于应用层协议的编程提供了丰富的支持。
Python 标准库中的网络相关模块
模块 | 描述 |
---|---|
socke | 基于传输层 TCP、UDP 协议进行网络编程的模块 |
asyncore | socket 模块的异步版,支持基于传输层协议的异步通信 |
asynchat | asyncore 的增强版 |
cg | 基本的 CGI(Common Gateway Interface,早期开发动态网站的技术)支持 |
E-mail 和 MLME 消息处理模块 | |
ftplib | 支持 FTP 协议的客户端模块 |
httplib、http.client | 支持 HTTP 协议以及 HTTP 客户揣的模块 |
imaplib | 支持 IMAP4 协议的客户端模块 |
mailbox | 操作不同格式邮箱的模块 |
mailcap | 支持 Mailcap 文件处理的模块 |
nntplib | 支持 NTTP 协议的客户端模块 |
smtplib | 支持 SMTP 协议(发送邮件)的客户端模块 |
poplib | 支持 POP3 协议的客户端模块 |
telnetlib | 支持TELNET 协议的客户端模块 |
urllib及其子模块 | 支持URL 处理的模块 |
xmlrpc、xmlrpc.server、xmlrpc.client | 支持XML-RPC协议的服务器端和客户端模块 |
三、Python socket建立TCP连接
程序在使用 socket 之前,必须先创建 socket 对象,可通过该类的如下构造器来创建 socket 实例:
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
- family 参数用于指定网络类型。该参数支持 socket.AF_UNIX(UNIX 网络)、socket.AF_INET(基于 IPv4 协议的网络)和 socket.AF_INET6(基于 IPv6 协议的网络)这三个常量。
- type 参数用于指定网络 Sock 类型。该参数可支持 SOCK_STREAM(默认值,创建基于 TCP 协议的 socket)、SOCK_DGRAM(创建基于 UDP 协议的 socket)和 SOCK_RAW(创建原始 socket)。
- proto 参数用于指定协议号,如果没有特殊要求,该参数默认为 0 ,并可以忽略。
作为服务器端使用的 socket 必须被绑定到指定 IP 地址和端口,并在该 IP 地址和端口进行监听,接收来自客户端的连接。 TCP 通信的服务器端编程的基本步骤:
- 服务器端先创建一个 socket 对象(
socket.socket()
)。 - 服务器端 socket 将自己绑定到指定 IP 地址和端口(
socket.bind(address,host)
)。 - 服务器端 socket 调用 listen() 方法监听网络。
- 程序采用循环不断调用 socket 的 accept() 方法接收来自客户端的连接。
- 发送数据:使用 send() 方法。注意,sendto() 方法用于 UDP 协议的通信。
# 导入 socket 模块
import socket
# 创建socket对象
s = socket.socket()
# 将socket绑定到本机IP和端口
s.bind(('192.168.1.88', 30000))
# 服务端开始监听来自客户端的连接
s.listen()
while True:
# 每当接收到客户端socket的请求时,该方法返回对应的socket和远程地址
c, addr = s.accept()
print(c)
print('连接地址:', addr)
c.send('您好,您收到了服务器的新年祝福!'.encode('utf-8'))
# 关闭连接
c.close()
作为客户端先创建一个 socket 对象,然后调用 socket 的 connect() 方法建立与服务器端的连接,这样就可以建立一个基于 TCP 协议的网络连接。TCP 通信的客户端编程的基本步骤大致归纳如下:
- 客户端先创建一个 socket 对象(
socket.socket()
)。 - 客户端 socket 调用 connect() 方法连接远程服务器。
- 接收数据:使用 recv() 方法。
# 导入socket模块
import socket
# 创建socket对象
s = socket.socket()
# 连接远程主机
s.connect(('192.168.1.88', 30000)) # ①
print('--%s--' % s.recv(1024).decode('utf-8'))
s.close()
四、Python socket建立UDP连接
UDP协议,全称 User Datagram Protocol,中文名称为用户数据报协议,主要用来支持那些需要在计算机之间传输数据的网络连接。
UDP 是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送数据。至于对方是否可以接收到这些数据,UDP 协议无法控制,所以说 UDP 是一种不可靠的协议。
UDP 协议和 TCP 协议简单对比如下:
- TCP 协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
- UDP 协议:不可靠,差错控制开销较小,传输大小限制在 64 KB以下,不需要建立连接。
- UDP 在通信实例的两端各建立一个 socket,但它们只是发送、接收数据的对象,没有客户端和服务器端的概念
socket建立UDP连接
程序在创建 socket 时,可通过 type 参数指定该 socket 的类型,如果将该参数指定为 SOCK_DGRAM,则意味着创建基于 UDP 协议的 socket。程序可以通过如下两个方法来发送和接收数据:
- socket.sendto(bytes, address):将 bytes 数据发送到 address 地址。
- socket.recvfrom(bufsize[, flags]):接收数据。该方法可以同时返回 socket 中的数据和数据来源地址。
使用 UDP 协议的 socket 在发送数据时必须使用 sendto() 方法,这是因为程序必须指定发送数据的目标地址
使用 UDP 协议的 socket 在接收数据时,既可使用普通的 recv() 方法,也可使用 recvfrom() 方法。如果程序需要得到数据报的来源,则应该使用 recvfrom() 方法
作为服务器端使用的 socket
import socket
PORT = 30000;
# 定义每个数据报的大小最大为4KB
DATA_LEN = 4096;
# 定义一个字符串数组,服务器端发送该数组的元素
books = ("疯狂Python讲义",
"疯狂Kotlin讲义",
"疯狂Android讲义",
"疯狂Swift讲义")
# 通过type属性指定创建基于UDP协议的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 将该socket绑定到本机的指定IP和端口
s.bind(('192.168.199.1', PORT))
# 采用循环接收数据
for i in range(1000):
# 读取s中的数据的数据的发送地址
data, addr = s.recvfrom(DATA_LEN)
# 将接收到的内容转换成字符串后输出
print(data.decode('utf-8'),addr)
# 从字符串数组中取出一个元素作为发送数据
send_data = books[i % 4].encode('utf-8')
# 将数据报发送给addr地址
s.sendto(send_data, addr)
s.close()
作为客户端使用的 socket
import socket
PORT = 30000;
# 定义每个数据报的大小最大为4KB
DATA_LEN = 4096;
DEST_IP = "192.168.199.1";
# 通过type属性指定创建基于UDP协议的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')
# 发送数据报
s.sendto(data, (DEST_IP, PORT))
# 读取socket中的数据
data = s.recv(DATA_LEN)
print(data.decode('utf-8'))
s.close()
五、Python smtplib模块:发送邮件
使用 Python 的 smtplib 模块来发送邮件非常简单,只需要按照如下 3 步来发送邮件即可:
- 连接 SMTP 服务器,并使用用户名、密码登录服务器。
- 创建 EmailMessage 对象,该对象代表邮件本身。
- 调用代表与 SMTP 服务器连接的对象的 sendmail() 方法发送邮件。
- 要为邮件设置主题、发件人名字和收件人名字,需要设置 EmailMessage 对象的相应属性。
- 要将邮件改为 HTML ,需要调用 EmailMessage的set_content() 方法的第二个参数,设置为 html。
import smtplib
from email.message import EmailMessage
smtp_server = 'smtp.qq.com' # 定义SMTP服务器地址:
from_addr = '425144880@qq.com' # 定义发件人地址
password = '123456' # 定义登录邮箱的密码
to_addr = 'wozhufuni999@163.com' # 定义收件人地址
# 创建SMTP连接
#conn = smtplib.SMTP(smtp_server, 25)
conn = smtplib.SMTP_SSL(smtp_server,465)
conn.set_debuglevel(1) # smtplib 调试模式
conn.login(from_addr, password)
first_id = '0000001'
# 创建邮件对象
msg = EmailMessage()
# 设置邮件内容
msg.set_content('<h2>邮件内容</h2>'
'<p>您好,这是一封来自Python的邮件<p>'
'<img src="cid:' + first_id +'"><p>'
'来自<a href="https://github.com/paotong999">Github</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)
# 发送邮件
conn.sendmail(from_addr, [to_addr], msg.as_string())
# 退出连接
conn.quit()
为了给邮件添加附件,只需调用 EmailMessage的add_attachment() 方法即可。该方法支持很多参数,最常见的参数如下:
maintype:指定附件的主类型。比如指定 image 代表附件是图片。
subtype:指定附件的子类型。比如指定为 png,代表附件是 PNG 图片。一般来说,子类型受主类型的限制。
filename:指定附件的文件名。
cid=img:指定附件的资源 ID,邮件正文可通过资源 ID 来引用该资源。
六、Python poplib模块:收取邮件
使用 poplib 收取邮件可分为两步:
- 使用 poplib.POP3 或 poplib.POP3_SSL 按 POP3 协议从服务器端下载邮件。
- 使用 email.parser.Parser 或 email.parser.BytesParser 解析邮件内容,得到 EmailMessage 对象,从 EmailMessage 对象中读取邮件内容。
import poplib, os.path, mimetypes
from email.parser import BytesParser, Parser
from email.policy import default
# 输入邮件地址, 口令和POP3服务器地址:
email = 'tx@henghaodata.com'
password = '123456'
pop3_server = 'pop-ent.21cn.com'
# 连接到POP 3服务器:
#conn = poplib.POP3(pop3_server, 110)
conn = poplib.POP3_SSL(pop3_server, 995)
# 可以打开或关闭调试信息:
#conn.set_debuglevel(1)
# 可选:打印POP 3服务器的欢迎文字:
print(conn.getwelcome().decode('utf-8'))
# 输入用户名、密码信息
# 相当于发送POP 3的user命令
conn.user(email)
# 相当于发送POP 3的pass命令
conn.pass_(password)
# 获取邮件统计信息,相当于发送POP 3的stat命令
message_num, total_size = conn.stat()
print('邮件数: %s. 总大小: %s' % (message_num, total_size))
# 获取服务器上的邮件列表,相当于发送POP 3的list命令
# resp保存服务器的响应码
# mails列表保存每封邮件的编号、大小
resp, mails, octets = conn.list()
print(resp, mails)
# 获取指定邮件的内容(此处传入总长度,也就是获取最后一封邮件)
# 相当于发送POP 3的retr命令
# resp保存服务器的响应码
# data保存该邮件的内容
resp, data, octets = conn.retr(len(mails))
# 将data的所有数据(原本是一个字节列表)拼接在一起
msg_data = b'\r\n'.join(data)
# 将字符串内容解析成邮件,此处一定要指定policy=default
msg = BytesParser(policy=default).parsebytes(msg_data) #①
print(type(msg))
print('发件人:' + msg['from'])
print('收件人:' + msg['to'])
print('主题:' + msg['subject'])
print('第一个收件人名字:' + msg['to'].addresses[0].username)
print('第一个发件人名字:' + 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()
# 如果没有文件名,程序要负责为附件生成文件名
if not filename:
# 根据附件的contnet_type来推测它的后缀名
ext = mimetypes.guess_extension(part.get_content_type())
# 如果推测不出后缀名
if not ext:
# 使用.bin作为后缀名
ext = '.bin'
# 程序为附件来生成文件名
filename = 'part-%03d%s' % (counter, ext)
counter += 1
# 将附件写入的本地文件
with open(os.path.join('.', filename), 'wb') as fp:
fp.write(part.get_payload(decode=True))
# 退出服务器,相当于发送POP 3的quit命令
conn.quit()
上面程序通过 poplib 模块使用 POP3 命令从服务器端下载邮件,其实就是依次发送 user、pass、stat、list、retr 命令的过程。当 retr 命令执行完成后,将得到最后一封邮件的数据 data,该 data 是一个 list 列表,因此程序需要先将这些数据拼接成一个整体,然后将邮件数据恢复成 EmailMessage 对象。这里有一点需要指出,程序在创建 BytesParser(解析字节串格式的邮件数据)或 Parser(解析字符串格式的邮件数据)时,必须指定 policy=default
如果程序要读取 EmailMessage 的各部分,则需要调用该对象的 walk() 方法,该方法返回一个可迭代对象,程序使用 for 循环遍历 walk() 方法的返回值,对邮件内容进行逐项处理:
- 如果邮件某项的 maintype 是 'multipart',则说明这一项是容器,用于包含邮件内容、附件等其他项。
- 如果邮件某项的 maintype 是 'text',则说明这一项的内容是文本,通常就是邮件正文或文本附件。对于这种文本内容,程序直接将其输出到控制台中。
- 如果邮件某项的 maintype 是其他,则说明这一项的内容是附件,程序将附件内容保存在本地文件中。
运行上面程序,可以看到程序收取了指定邮件的最后一封邮件,并将邮件内容输出到控制台中,将邮件附件保存在本地文件中。
网友评论