跳到主要内容

1. 网络编程

网络通信就是两个进程之间的通信;

TCP/IP

  • IP 地址 计算机的网络接口,通常是网卡,可有多个,是 32 位整数(IPv4),IPv6 是 128 位整数;

  • IP 协议 负责把数据从一台计算机通过网络发送到另一台计算机,数据被分割成小块,IP 包特点是速度快,途径多个路由,不保证到达,也不保证顺序;

  • TCP 协议 在 IP 协议基础上,负责在两台计算机上建立起可靠连接,保证数据包顺序到达。对每个 IP 包编号,顺序发收,失败的自动重发;

  • TCP 报文 传输的数据,源 IP、目标 IP、源端口号、目标端口号;

HTTP 协议、SMTP 协议都建立在 TCP 协议基础上;

一个进程可能与多个计算机建立连接,因此可能申请很多个端口;

TCP 编程

传输控制协议;

  • Socket,通常是表示打开一个网络链接,需要知道目标计算机的 IP 地址、端口号,还有指定协议类型;

服务端

一个服务端 Socket 依赖 4 项确定唯一:服务器地址,服务器端口,客户端地址,客户端端口;

服务端接收的每个连接需要一个新进程/线程来处理,否则服务器一次只能服务一个客户端;

import socket, threading, time
# 创建一个 Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址和端口
# 0.0.0.0 是广播地址,集所有网络地址
# 127.0.0.1 表示本机地址
s.bind(('localhost', 6666))
# 开始监听,5 是最大连接数
s.listen(5)
print('Waiting for connection...')

def tcplink(sock, addr):
print('accept new connection from %s:%s...' % addr)
sock.send(b'Welcome.')

while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
print(f"receive, {data.decode('utf-8')}")
sock.send(('hello, %s.' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('connection from %s:%s closed.' % addr)

# 通过永久循环接收客户端连接
while True:
# 接收并返回一个客户端连接
sock, addr = s.accept()
# 构造一个线程处理这个连接
t = threading.Thread(target=tcplink, args=(sock, addr))
# 启动线程
t.start()

客户端

import socket
# AF_INET 表示 IPv4
# SOCK_STREAM 表示 TCP 协议
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# HTTP 协议规定客户端必须先发起请求,由服务端接收后再发送数据给客户端
# 端口 1024 以内为标准端口,如 SMTP 25,FTP 21
s.connect(('localhost', 6666))
# 接收指定长度的字节数据
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

TCP 协议进行 Socket 编程,客户端需要主动连接服务器 IP 和端口,服务端需要先监听指定端口,通常服务器程序会无限运行下去,同一个端口 Socket 绑定后,就不能被另一个 Socket 绑定(同协议类型);

UDP 编程

用户数据报协议;

相对 TCP 的可靠连接,UDP 是面向无连接的协议,UDP 协议知道对方 IP 和端口就能发送数据包,但不能保证送达,速度快;

服务端

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# UDP 不需要listen(),直接接收
s.bind(('localhost', 6666))

print('bind udp on 6666...')

while True:
# 返回数据和客户端IP、端口
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
# 向客户端回发
s.sendto(b'Hello, %s.' % data, addr)

客户端

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

for data in [b'Michael', b'Tracy', b'Sarah']:
# 不需要 connect()
s.sendto(data, ('localhost', 6666))
print(s.recv(1024).decode('utf-8'))
s.close()

服务器绑定相同的 UDP 端口和 TCP 端口不冲突

2. 电子邮件

发送邮件过程如下:

发件人 -> MUA -> MTA -> 若干 MTA -> MDA <- MUA <- 收件人
  • MUA Mail User Agent,邮件用户代理

  • MTA Mail Transfer Agent,邮件传输代理

  • MDA Mail Delivery Agent,邮件投递代理

  • 发送 编写邮件用 MUA 发到 MTA

  • 收件 编写 MUA 从 MDA 收取邮件

  • SMTP Simple Mail Transfer Protocol,负责 MUA -> MTA, MTA -> MTA

  • POP3 Post Office Protocol v3,负责 MUA -> MDA

  • IMAP Internet Message Access Protocol,负责 MUA -> MDA,可收取邮件和操作 MDA 上的邮件

使用邮件客户端需:

  • 发送,配置 SMTP 服务器
  • 收件,配置 POP3 或 IMAP 服务器

SMTP 发送邮件

email 模块用于构造邮件,smtplib 用于发送邮件;

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
from email.header import Header
from email.utils import parseaddr, formataddr
import smtplib


def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))


# 输入Email地址和口令:
from_addr = 'aurelius-shu@outlook.com'
# QQ 邮箱密码需换成短信授权码
password = input('password: ')
# 输入收件人地址:
to_addr = 'aurelius-shu@qq.com'
# 输入SMTP服务器地址:
smtp_server = 'smtp.office365.com'

# 邮件对象:
msg = MIMEMultipart('alternative')
msg['From'] = _format_addr('发件人 <%s>' % from_addr)
# 多个时以逗号分隔
msg['To'] = _format_addr('收件人 <%s>' % to_addr)
msg['Subject'] = Header('标题', 'utf-8').encode()

# msg = MIMEText(
# '<html><body><h1>Hello</h1>' +
# '<p>send by <a href="http://www.python.org">Python</a>...</p>' +
# '</body></html>', 'html', 'utf-8')
msg.attach(MIMEText('hello, send by Python...', 'plain', 'utf-8'))
# 邮件正文是MIMEText:
# msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
msg.attach(
MIMEText(
'<html><body><h1>Hello</h1><p><img src="cid:0"></p></body></html>',
'html', 'utf-8'))

# 添加附件就是加上一个MIMEBase,从本地读取一个图片:
with open(r'D:\Users\Aurelius\Pictures\threefish.jpg', 'rb') as f:
# 设置附件的MIME和文件名,这里是png类型:
mime = MIMEBase('image', 'png', filename='threefish.jpg')
# 加上必要的头信息:
mime.add_header('Content-Disposition',
'attachment',
filename='threefish.jpg')
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)

# SMTP协议默认端口是 25
# SMTP 安全连接端口 587
server = smtplib.SMTP(smtp_server, 587)
# set_debuglevel(1) 可以打印 SMTP 服务器交互信息
server.set_debuglevel(1)
# 表示自己需要身份验证
server.ehlo()
# 创建 ssl 安全连接,SMTP encryption method STARTTLS
server.starttls()
server.login(from_addr, password)
# [to_addr] 可以指定发送多人
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()

发送 HTML 邮件

内容正文换成HTML文本,plain换成html

发送附件

可以看作包含若干部分的邮件,文本和若干附件用 MIMEMultipart 对象代表邮件本身,附加邮件正文(MIMEText)和附件(MIMEBase);

图片嵌入

先把图片当作附件添加,再在 HTML 通过应用 src='cid:x' 把附件图片嵌入,避免直接在 HTML 邮件链接图片地址,外部链接会被大部分邮件服务商屏蔽;

同时支持 HTML 与 Plain

指定 MIMEMultipartsubtypealternative,然后附加 HTMLPlain

加密 SMTP

先创建ssl安全连接,再使用SMTP协议发送邮件;

email.mine

构建一个邮件对象就是构建一个Message,文本对象是MIMEText对象,MIMEImage对象是图片附件,MIMEMultipart对象是组合对象,MIMEBase是任何对象;

Message
<- MIMEBase
<- MIMEMultipart
<- MIMENonMultipart
<- MIMEMessage
<- MIMEText
<- MIMEImage

POP3 收取邮件

poplib模块实现了POP协议,email模块用来解析原始邮件对象;

from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import poplib

# 输入邮件地址, 口令和POP3服务器地址:
email = 'aurelius-shu@outlook.com'
password = input('Password: ')
pop3_server = 'outlook.office365.com'

# 连接到POP3服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))

# 身份认证:
server.user(email)
server.pass_(password)

# stat()返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list()返回所有邮件的编号:
resp, mails, octets = server.list()
# 可以查看返回的列表类似[b'1 82923', b'2 2184', ...]
print(mails)

# 获取最新一封邮件, 注意索引号从1开始:
index = len(mails)
resp, lines, octets = server.retr(index)

# lines存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)

# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()


# indent用于缩进显示:
def print_info(msg, indent=0):
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header == 'Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type == 'text/plain' or content_type == 'text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))


def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value


def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset

POP3 收取的 Message 对象可能是一个嵌套对象,所以解析要递归的进行;


PS:欢迎各路道友阅读评论,感谢道友点赞关注收藏