目录
一、客户端/服务器架构
二、osi五层|七层
三、socket是什么?
四、套接字
1、套接字工作流程
2、基于tcp协议的套接字编程
① 实现一次通信
② 实现通信循环
③ 实现服务端永久通信
④ tcp协议的三次握手和四次挥手
3、基于udp协议的套接字编程
4、udp模拟ntp服务
5、基于tcp实现远程命令调用
6、基于UDP实现远程命令调用
五、粘包
1、发送端粘包
2、接收端粘包
3、解决粘包
六、socketserver实现并发
1、socketserver_tcp的实现
2、socketserver_udp的实现
七、SMTP邮件编程
1、纯文本邮件
2、HTML邮件
3、带附件的邮件
- C/S=Client/Server
- B/S=Brower/Server
- 一个基于客户端/服务器,一个基于浏览器/服务器
- 互联网中处处是C/S架构,如电影网站是服务端你的浏览器是客户端(B/S架构也是 C/S架构的一种)
C/S架构与socket的关系:我们学习socket就是为了完成C/S架构的开发
一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷哈的)。
如果你要跟别人一起玩,那你就需要上网了,什么是互联网?
互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都采用了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。
人们按照分工不同把互联网协议从逻辑上划分了层级:
每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件,用户感知到的只是最上面一层应用层,自上而下每层都依赖于下一层。
1、物理层由来:上面提到,孤立的计算机之间要想一起玩,就必须接入 internet,言外之意就是计算机之间必须完成组网
物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字02、数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组每组什么意思
数据链路层的功能:定义了电信号的分组方式以太网协议:早期的时候各个公司都有自己的分组方式,后来形成了统一的标准即以太网协议ethernet
mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址长度为48位2进制通常由12位16进制数表示(前六位是厂商编号后六位是流水线号)广播:有了mac地址同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址)ethernet 采用最原始的方式,广播的方式进行通信即计算机通信基本靠吼
3、网络层由来:有了ethernet、mac地址、广播的发送方式世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难。
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址。
4、传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,等多个应用程序那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序与网卡关联的编号。
传输层功能:建立端口到端口的通信。
5、应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可必须规定好数据的组织形式以开发自己的应用程序,数据多种多样。
应用层功能:规定应用程序的数据格式
我们经常把socket 翻译为套接字,Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket 接口后面,对用户来说,一组简单的接口就是全部让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解 tcp/udp 协议,socket 已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这合机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序。而程序的pid是同一台机器上不同进程或者线程的标识。
套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的Unix。一开始套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或IPC。套接字有两种(或者称为有两个种族,分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族套接宇家族的名字: AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字: AF INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中AF_INET是使用最广泛的一个,python 支持很多种地址家族但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
工作流程:
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用 accept()阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端客户端读取数据,最后关闭连接,一次交互结束。
tcp的整个流程类似打电话的一个过程:
服务端:
a. 买于机tcp_server=socket.socket(socket.AF_INET,socket.SOCKSTREAM)b. 绑定电话卡tcp_server.bind(("ip”,端口))
c. 待机tcp_server.listen(最大链接数)
d. 接听电话conn,addr=tcp_serveraccept()得到链接和对方地址e. 接收消息,听话data=connrecv(接收字节数)
g. 挂电话conn.close()
h. 手机关机tcp_server.close()
客户端:
a. 买手机tcp_client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
b. 拨号tcp_client.connect(("ip",服务端端口))
c. 发消息,说话tcp_client.send(发送的是字节数据需要编码)
d. 接收消息,听话data=tcpclient.recv(1024)
e. 挂电话tcpclient.close()
- tcp 是基于连接的,必须先启动服务端,然后再启动客户端去连接服务端
- tcp 的 recv()和 send()没有对应关系,都是从各自的缓冲区进行操作
tcp_server.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 声明服务端的ip和端口
tcp_server.bind(('127.0.0.1', 8000)) # 绑定电话卡
# 声明最大连接数
tcp_server.listen(5) # 待机
# 得到一个持续连接和客户端地址
conn, addr = tcp_server.accept() # 接听电话
# 接收客户端的消息
data = conn.recv(1024) # 听消息,听话
print('客户端说:', data.decode('utf-8'))
# 发送信息给客户端
conn.send('hello我是服务端...'.encode('utf-8')) # 发消息,说话
# 断开与客户端的连接
conn.close() # 挂电话
# 关闭服务器
tcp_server.close() # 关机
tcp_client.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 与服务端建立连接
tcp_client.connect(('127.0.0.1', 8000)) # 拨号
# 发送消息给服务器
tcp_client.send('hello我是客户端'.encode('utf-8')) # 说话
# 接收服务端的信息
data = tcp_client.recv(1024) # 听话
print("服务端说:", data.decode('utf-8'))
# 断开与服务器的连接
tcp_client.close() # 挂电话
tcp_server_forever.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 声明服务端的ip和端口
tcp_server.bind(('127.0.0.1', 8000)) # 绑定电话卡
# 声明最大连接数
tcp_server.listen(5) # 待机
# 得到一个持续连接和客户端地址
conn, addr = tcp_server.accept() # 接听电话
while True:
# 接收客户端的消息
data = conn.recv(1024) # 听消息,听话
if not data.strip():
break
# 如果客户端发送exit,中断连接
if 'exit' == data.decode('utf-8'):
conn.send(data)
break
print('客户端说:', data.decode('utf-8'))
# 发送信息给客户端
msg = input(">>:")
conn.send(msg.encode('utf-8')) # 发消息,说话
# 断开与客户端的连接
conn.close() # 挂电话
# 关闭服务器
tcp_server.close() # 关机
tcp_client_forever.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 与服务端建立连接
tcp_client.connect(('127.0.0.1', 8000)) # 拨号
while True:
msg = input(">>:")
if not msg.strip():
continue
# 发送消息给服务器
tcp_client.send(msg.encode('utf-8')) # 说话
# 接收服务端的信息
data = tcp_client.recv(1024) # 听话
if 'exit' == data.decode('utf-8'):
print('与服务器断开连接,如需发送请重连')
break
print("服务端说:", data.decode('utf-8'))
# 断开与服务器的连接
tcp_client.close() # 挂电话
客户端不存在什么永久,断开就是断开了
tcp_server_while.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 声明服务端的ip和端口
tcp_server.bind(('127.0.0.1', 8000)) # 绑定电话卡
# 声明最大连接数
tcp_server.listen(5) # 待机
# 外层循环,保证服务端永久执行
while True:
# 得到一个持续连接和客户端地址
conn, addr = tcp_server.accept() # 接听电话
# 内层循环,实现与客户端循环交互
while True:
# 客户端强制终端,linux下会不断发送空给服务器
# 客户端强制终端,Windows下会抛出异常
try:
# 接收客户端的消息
data = conn.recv(1024) # 听消息,听话
if not data.strip():
break
# 如果客户端发送exit,中断连接
if 'exit' == data.decode('utf-8'):
conn.send(data)
break
print('客户端说:', data.decode('utf-8'))
# 发送信息给客户端
msg = input(">>:")
conn.send(msg.encode('utf-8')) # 发消息,说话
except Exception as e:
print('程序出现异常,异常信息:', e)
break
# 断开与客户端的连接
conn.close() # 挂电话
# 关闭服务器
tcp_server.close() # 关机
三次握于过程说明:
1、由客户端发送建立TCP连接的请求报文,其中报文中包含seq序列号,是由发送端随机生成的并且将报文中的SYN字段置为1表示需要建立TCP连接。(SYN=1seq=xx为随机生成数值)
2、由服务端回复客户端发送的TCP连接请求报文,其中包含seq 序列号,是由回复端随机生成的,并且将SYN置为1,而且会产生ACK字段,ACK字段数值是在客户端发送过来的序列号seq的基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP建立请求已得到验证。(SYN=1,ACK=x+1,seq=y,y为随机生成数值) 这里的ack加1可以理解为是确认和谁建立连接。
3、客户端收到服务端发送的TCP建立验证请求后,会使自己的序列号加1表示,并且再次回复ACK验证请求,在服务端发过来的seq上加1进行回复(SYN=1ACK=y+1seq=x+1)。四次挥于过程说明:
1、客户端发送断开TCP连接请求的报文,其中报文中包含seg 序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1seg=xX由客户端随机生成)
2、服务端会回复客户端发送的TCP 断开请求报文,其包含seq 序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq 序列号基础上加1进行回复,以便客户端收到信息时知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成)
3、服务端在回复完客户端的TCP 断开请求后不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1ACK=x+1seq=zz由服务端
随机生成)4、客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq字段和ACK字段ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)至此TCP断开的4次挥于过程完毕。
- udp是无连接的,先启动那一端都不会报错(前提是在客户端连接服务端语句执行前,服务端已启动)
- QQ就是udp协议为主开发的
- udp一次recvfrom对应一个 sendto,缓冲区大小不一致会丢失数据或报错
udp_server.py:
from socket import *
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
# 创建socket连接 SOCK_DGRAM->udp协议
udp_server = socket(AF_INET, SOCK_DGRAM)
# 绑定ip和端口
udp_server.bind(ip_port)
while True:
try:
# 收客户端消息
data, addr = udp_server.recvfrom(buffer_size)
if not data.strip():
break
# 如果客户端发送exit,中断连接
if 'exit' == data.decode('utf-8'):
udp_server.sendto(data, addr)
break
print(data.decode('utf-8'), addr)
# 发送消息给客户端
msg = input('>>:') # 阻塞
udp_server.sendto(msg.encode('utf-8'), addr)
except Exception as e:
print('程序出现异常,异常是:', e)
break
# 关闭连接
udp_server.close()
udp_client.py:
from socket import *
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
# 创建socket连接 SOCK_DGRAM->udp协议
udp_client = socket(AF_INET, SOCK_DGRAM)
while True:
# 发送消息给服务端
msg = input('>>:') # 阻塞
if not msg:
continue
udp_client.sendto(msg.encode('utf-8'), ip_port)
# 接收服务端消息
data, addr = udp_client.recvfrom(buffer_size)
if 'exit' == data.decode('utf-8'):
udp_client.sendto(data, addr)
break
print(data.decode('utf-8'), addr)
# 关闭连接
udp_client.close()
ntp_server.py:
from socket import *
import time
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
# 创建socket连接 SOCK_DGRAM->udp协议
udp_server = socket(AF_INET, SOCK_DGRAM)
# 绑定ip和端口
udp_server.bind(ip_port)
while True:
# 收客户端消息
data, addr = udp_server.recvfrom(buffer_size) # 返回的是元组
if not data.strip():
# 用户输入空,返回默认时间格式
fmt = '%Y-%m-%d %X'
else:
fmt = data.decode('utf-8')
time.strftime(fmt)
msg = 'ntp服务的标准时间是:' + time.strftime(fmt)
udp_server.sendto(msg.encode('utf-8'), addr)
# 关闭连接
udp_server.close()
ntp_client.py:
from socket import *
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
# 创建socket连接 SOCK_DGRAM->udp协议
udp_client = socket(AF_INET, SOCK_DGRAM)
while True:
# 发送消息给服务端
msg = input('>>:') # 阻塞
udp_client.sendto(msg.encode('utf-8'), ip_port)
# 接收服务端消息
data, addr = udp_client.recvfrom(buffer_size)
print(data.decode('utf-8'))
# 关闭连接
udp_client.close()
cmd_server.py:
from socket import *
import subprocess
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024
# 创建socket连接,tcp协议
cmd_server = socket(AF_INET, SOCK_STREAM)
# 绑定端口
cmd_server.bind(ip_port)
# 声明最大连接数
cmd_server.listen(back_log)
# 外层循环,保证服务端永久执行
while True:
conn, addr = cmd_server.accept()
# 保持和客户端的持续连接
while True:
# 防止客户端异常中断, Windows下
try:
cmd = conn.recv(buffer_size)
# 防止客户端异常中断, Linux下
if not cmd.strip():
break
print('客户端指令:', cmd.decode('utf-8'))
# 执行指定
"""
Popen函数的参数:
args:指令序列或者字符串指令
shell:True表示指令为字符串,False表示指令为序列
stderr:返回错误通道信息,subprocess.PIPE返回一个fail,可以直接给客户端展示
stdin:标准输入通道
stdout:返回标准通道信息
"""
cmd_result = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err_msg = cmd_result.stderr.read() # 从错误通道获取信息
"""
cmd发送命令后,可能返回两种结果:
1、错误指令:返回错误通道信息
2、正确指令:返回正确通道信息
a. 指令执行成功,返回空,比如:cd . cd ..
b. 指令执行成功,返回信息
"""
# 如果错误通道有消息,返回客户端提示
if err_msg.strip():
msg = err_msg
else:
# 如果错误通道没有消息,获取标准通道信息
msg = cmd_result.stdout.read() # 从标准通道获取信息
# 返回内容为空(执行cd .,cd ..)
if not msg:
msg = '执行成功'.encode('gbk') # 返回一个友好提示
# continue # 为空后无需将空返回
# 返回标准通道信息
conn.send(msg) # msg已经是gbk
except Exception as e:
print('程序出现异常,异常信息:', e)
break
# 与客户端中断连接
conn.close()
# 关闭socket连接
cmd_server.close()
cmd_client.py:
from socket import *
import subprocess
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024
# 创建socket连接,tcp协议
cmd_client = socket(AF_INET, SOCK_STREAM)
# 建立连接
cmd_client.connect(ip_port)
# 实现通信循环
while True:
cmd_msg = input(">>:").strip()
if not cmd_msg:
continue
# 如果发送exit,中断与服务端的连接
if cmd_msg == 'exit':
break
# 发消息给服务端
cmd_client.send(cmd_msg.encode('utf-8'))
data = cmd_client.recv(500)
print(data.decode('gbk'))
cmd_client.close()
cmd_server.py:
from socket import *
import subprocess
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
# 创建socket连接,udp协议
cmd_server = socket(AF_INET, SOCK_DGRAM)
# 绑定端口
cmd_server.bind(ip_port)
# 保持和客户端的持续连接
while True:
# 防止客户端异常中断, Windows下
try:
cmd, addr = cmd_server.recvfrom(buffer_size)
# 防止客户端异常中断, Linux下
if not cmd.strip():
break
print('客户端指令:', cmd.decode('utf-8'))
# -----------------------与系统交互代码块--------start--------------------
# 执行指定
"""
Popen函数的参数:
args:指令序列或者字符串指令
shell:True表示指令为字符串,False表示指令为序列
stderr:返回错误通道信息,subprocess.PIPE返回一个fail,可以直接给客户端展示
stdin:标准输入通道
stdout:返回标准通道信息
"""
cmd_result = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err_msg = cmd_result.stderr.read() # 从错误通道获取信息
"""
cmd发送命令后,可能返回两种结果:
1、错误指令:返回错误通道信息
2、正确指令:返回正确通道信息
a. 指令执行成功,返回空,比如:cd . cd ..
b. 指令执行成功,返回信息
"""
# 如果错误通道有消息,返回客户端提示
if err_msg.strip():
msg = err_msg
else:
# 如果错误通道没有消息,获取标准通道信息
msg = cmd_result.stdout.read() # 从标准通道获取信息
# 返回内容为空(执行cd .,cd ..)
if not msg:
msg = '执行成功'.encode('gbk') # 返回一个友好提示
# -----------------------与系统交互代码块-----------end-----------------
# 返回标准通道信息
cmd_server.sendto(msg, addr) # msg已经是gbk
except Exception as e:
print('程序出现异常,异常信息:', e)
break
# 关闭socket连接
cmd_server.close()
cmd_client.py:
from socket import *
import subprocess
ip_port = ('127.0.0.1', 8000)
buffer_size = 102400
# 创建socket连接,udp协议
cmd_client = socket(AF_INET, SOCK_DGRAM)
# 实现通信循环
while True:
cmd_msg = input(">>:").strip()
if not cmd_msg:
continue
# 如果发送exit,中断与服务端的连接
if cmd_msg == 'exit':
break
# 发消息给服务端
cmd_client.sendto(cmd_msg.encode('utf-8'), ip_port)
data, addr = cmd_client.recvfrom(buffer_size)
print(data.decode('gbk'))
cmd_client.close()
只有 TCP 有粘包现象,UDP 永远不会粘包
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。发送方引起的粘包是由 TCP 协议本身造成的,TCP 为提高传输效率,发送方往往要收集到足够多的数据后才发送一个 TCP 段。若连续几次需要 send 的数据都很少通常 TCP 会根据优化算法把这些数据合成一个 TCP 段后一次发送出去,这样接收方就收到了粘包数据。
两种情况下会发生粘包:
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合到一起,产生粘包)
tcp_server.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 声明服务端的ip和端口
tcp_server.bind(('127.0.0.1', 8000)) # 绑定电话卡
# 声明最大连接数
tcp_server.listen(5) # 待机
# 得到一个持续连接和客户端地址
conn, addr = tcp_server.accept() # 接听电话
# 接收客户端的消息
data1 = conn.recv(1024) # 听消息,听话
print('第一次数据:', data1.decode('utf-8'))
# 接收客户端的消息
data2 = conn.recv(1024) # 听消息,听话
print('第二次数据:', data2.decode('utf-8'))
# 接收客户端的消息
data3 = conn.recv(1024) # 听消息,听话
print('第三次数据:', data3.decode('utf-8'))
# 发送信息给客户端
conn.send('hello我是服务端...'.encode('utf-8')) # 发消息,说话
# 断开与客户端的连接
conn.close() # 挂电话
# 关闭服务器
tcp_server.close() # 关机
tcp_client.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 与服务端建立连接
tcp_client.connect(('127.0.0.1', 8000)) # 拨号
# 发送消息给服务器
tcp_client.send('hello'.encode('utf-8')) # 说话
# 发送消息给服务器
tcp_client.send('world'.encode('utf-8')) # 说话
# 发送消息给服务器
tcp_client.send('Python'.encode('utf-8')) # 说话
# 接收服务端的信息
data = tcp_client.recv(1024) # 听话
print("服务端说:", data.decode('utf-8'))
# 断开与服务器的连接
tcp_client.close() # 挂电话
发送的三次内容,一次接收:
接收方不知道消息之间的界限,不知道一次性提取多少字节的数据,造成多个包接收(客户端发送了一段数据、服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
tcp_server.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 声明服务端的ip和端口
tcp_server.bind(('127.0.0.1', 8000)) # 绑定电话卡
# 声明最大连接数
tcp_server.listen(5) # 待机
# 得到一个持续连接和客户端地址
conn, addr = tcp_server.accept() # 接听电话
# 接收客户端的消息
data1 = conn.recv(1) # 听消息,听话
print('第一次数据:', data1.decode('utf-8'))
# 接收客户端的消息
data2 = conn.recv(5) # 听消息,听话
print('第二次数据:', data2.decode('utf-8'))
# 接收客户端的消息
data3 = conn.recv(1024) # 听消息,听话
print('第三次数据:', data3.decode('utf-8'))
# 发送信息给客户端
conn.send('hello我是服务端...'.encode('utf-8')) # 发消息,说话
# 断开与客户端的连接
conn.close() # 挂电话
# 关闭服务器
tcp_server.close() # 关机
tcp_client.py:
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 与服务端建立连接
tcp_client.connect(('127.0.0.1', 8000)) # 拨号
# 发送消息给服务器
tcp_client.send('hello world python!'.encode('utf-8')) # 说话
# 接收服务端的信息
data = tcp_client.recv(1024) # 听话
print("服务端说:", data.decode('utf-8'))
# 断开与服务器的连接
tcp_client.close() # 挂电话
cmd_server_solve.py:
from socket import *
import subprocess # 与系统交互模块
import struct # 封包 拆包
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024
# 创建socket连接,tcp协议
cmd_server = socket(AF_INET, SOCK_STREAM)
# 绑定端口
cmd_server.bind(ip_port)
# 声明最大连接数
cmd_server.listen(back_log)
# 外层循环,保证服务端永久执行
while True:
conn, addr = cmd_server.accept()
# 保持和客户端的持续连接
while True:
# 防止客户端异常中断, Windows下
try:
cmd = conn.recv(buffer_size)
# 防止客户端异常中断, Linux下
if not cmd.strip():
break
print('客户端指令:', cmd.decode('utf-8'))
# -----------------------------与系统交互代码块-------start---------------------------
# 执行指定
"""
Popen函数的参数:
args:指令序列或者字符串指令
shell:True表示指令为字符串,False表示指令为序列
stderr:返回错误通道信息,subprocess.PIPE返回一个fail,可以直接给客户端展示
stdin:标准输入通道
stdout:返回标准通道信息
"""
cmd_result = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err_msg = cmd_result.stderr.read() # 从错误通道获取信息
"""
cmd发送命令后,可能返回两种结果:
1、错误指令:返回错误通道信息
2、正确指令:返回正确通道信息
a. 指令执行成功,返回空,比如:cd . cd ..
b. 指令执行成功,返回信息
"""
# 如果错误通道有消息,返回客户端提示
if err_msg.strip():
msg = err_msg
else:
# 如果错误通道没有消息,获取标准通道信息
msg = cmd_result.stdout.read() # 从标准通道获取信息
# 返回内容为空(执行cd .,cd ..)
if not msg:
msg = '执行成功'.encode('gbk') # 返回一个友好提示
# -----------------------------与系统交互代码块--------end-------------------
# -----------------------------解决粘包--------start--------------------------
"""
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
"""
# 根据格式化字符串fmt(i integer 4字节)封装,返回一个值的字节对象
data_length = struct.pack('i', len(msg)) # 封包,以4字节封包
conn.send(data_length)
# -----------------------------解决粘包---------end-------------------------
# 返回标准通道信息
conn.send(msg) # msg已经是gbk
except Exception as e:
print('程序出现异常,异常信息:', e)
break
# 与客户端中断连接
conn.close()
# 关闭socket连接
cmd_server.close()
cmd_client_solve.py:
from socket import *
import struct
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024
# 创建socket连接,tcp协议
cmd_client = socket(AF_INET, SOCK_STREAM)
# 建立连接
cmd_client.connect(ip_port)
# 实现通信循环
while True:
cmd_msg = input(">>:").strip()
if not cmd_msg:
continue
# 如果发送exit,中断与服务端的连接
if cmd_msg == 'exit':
break
# 发消息给服务端
cmd_client.send(cmd_msg.encode('utf-8'))
# -----------------------------解决粘包----------start------------------------
data_length = struct.unpack('i', cmd_client.recv(4))[0]
# print(data_length) # 1052
recv_size = 0
recv_msg = b''
while recv_size < data_length:
recv_msg += cmd_client.recv(300)
recv_size = len(recv_msg)
# -----------------------------解决粘包------------end----------------------
print(recv_msg.decode('gbk'))
cmd_client.close()
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环。
socketserver 模块中分两大类:
server类(解决连接问题)
request 类 (解决通信问题)
tcp_server.py:
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
print('conn:', self.request)
print('addr:', self.client_address)
# 内层循环,实现与客户端循环交互
while True:
# 客户端强制终端,linux下会不断发送空给服务器
# 客户端强制终端,Windows下会抛出异常
try:
# 接收客户端的消息
data = self.request.recv(1024) # 听消息,听话
if not data.strip():
break
# 如果客户端发送exit,中断连接
if b'exit' == data:
self.request.sendall(data)
self.request.close() # 断开通信循环
break
print('客户端说:', data.decode('utf-8'))
# 发送信息给客户端
msg = input(">>:")
self.request.sendall(msg.encode('utf-8')) # 发消息,说话
except Exception as e:
print('程序出现异常,异常信息:', e)
break
# 断开连接循环
self.server.shutdown()
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8000), MyHandler)
server.serve_forever() # 实现连接循环
tcp_client_1.py、tcp_client_1.py:多个客户端可同时与服务端通信
import socket
# 建立socket连接(AF_INET:基于网络套接字家族,SOCK_STREAM:tcp协议)
tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
# 与服务端建立连接
tcp_client.connect(('127.0.0.1', 8000)) # 拨号
while True:
msg = input(">>:")
if not msg.strip():
continue
# 发送消息给服务器
tcp_client.send(msg.encode('utf-8')) # 说话
# 接收服务端的信息
data = tcp_client.recv(1024) # 听话
if 'exit' == data.decode('utf-8'):
print('与服务器断开连接,如需发送请重连')
break
print("服务端说:", data.decode('utf-8'))
# 断开与服务器的连接
tcp_client.close() # 挂电话
udp_server.py:
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
# print(self.request) # (b'hh', )
data = self.request[0] # 客户端发来的消息
udp_server = self.request[1] # 客户端发来的消息
# 如果信息为空,中断连接
if not data:
return
# 如果消息为指定字符,中断连接
if b'exit' == data:
udp_server.sendto(data, self.client_address)
self.server.shutdown()
return
print(self.client_address) # ('127.0.0.1', 62275)
udp_server.sendto(data.upper(), self.client_address)
if __name__ == '__main__':
server = socketserver.ThreadingUDPServer(('127.0.0.1', 8000), MyHandler)
server.serve_forever() # 实现通信循环
udp_client.py:
from socket import *
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
# 创建socket连接 SOCK_DGRAM->udp协议
udp_client = socket(AF_INET, SOCK_DGRAM)
while True:
# 发送消息给服务端
msg = input('>>:') # 阻塞
if not msg:
continue
udp_client.sendto(msg.encode('utf-8'), ip_port)
# 接收服务端消息
data, addr = udp_client.recvfrom(buffer_size)
if b'exit' == data:
print('与服务端断开连接,如需操作请重连')
break
print(data.decode('utf-8'), addr)
# 关闭连接
udp_client.close()
SMTP是发送邮件的协议,Python内置对SMTP的支持,可以发送纯文本邮件、HTML邮件以及带附件的邮件。
Python对SMTP支持有smtplib和email两个模块,email负责构造邮件,smtplib负责发送邮件。
发送流程
1. 导email模块(构建邮件发送实体对象)和smtplb模块(负责发送邮件)2. 构建邮件内容对象标准邮件需要三个头部信息: From To和Subject构建邮件内容对象之前先准备好所需数据
a. 格式化邮箱(不格式化会被当做垃圾邮件去发送或者发送失败)b. 构建一个发送内容对象
msg= MIMEText("邮件内容", "plain", "utf-8")
c. 设置发送人信息格式msg[From]
d. 设置接收人信息格式msg[To]
e. 设置发送主题(标题) msg['Subject]3. 发送
a. 创建发送邮件服务器的对象
server=smtplib.SMTP(smtpserver,25) 端口默认25b. 设置发送的日志级别
server.set_debuglevel(1)
c. 登录发送邮箱
server.login(from_addr,password)d. 调用发送方法
server.sendmail(from_addr,[to_addr],msg.as_string())f. 关闭发送server.quit()
text_email.py:
from email.header import Header # 对中文进行编码
from email.mime.text import MIMEText # 邮件对象
from email.utils import parseaddr, formataddr # 格式化邮箱
import smtplib # 发送邮件
# 格式化邮箱(不格式化会被当做垃圾邮件去发送或者发送失败)
def format_addr(s):
name, addr = parseaddr(s) # 比如:东皇太一 ,
return formataddr((Header(name, 'utf-8').encode('utf-8'), addr))
# 准备数据
from_addr = '[email protected]' # 发件人
password = 'QXKWQKVZXWJDNWEP' # 授权码
smtp_server = 'smtp.163.com' # 服务器地址
to_addr = input('收件人邮箱:') # 收件人
# ---------------构建邮件内容对象------start---------------
# 构建一个发送内容对象
msg = MIMEText("邮件内容", "plain", "utf-8")
# 标准邮件需要三个头部信息 From To和Subject
msg['From'] = format_addr(u'东皇太一<%s>' % from_addr) # 发件人
to_name = input('收件人名称:')
msg['To'] = format_addr(u'{}<%s>'.format(to_name) % to_addr) # 发件人
msg['Subject'] = Header(u'Python学习资料!', 'utf-8').encode('utf-8') # 标题
# ---------------构建邮件内容对象------end-----------------
# ---------------发送邮件------start---------------
server = smtplib.SMTP(smtp_server, 25) # 163smtp服务器默认端口:25, smtp_server: 域名,等价于ip
server.set_debuglevel(1) # 是否显示发送日志,1显示,0不显示
server.login(from_addr, password)
server.sendmail(from_addr, to_addr, msg.as_string()) #发送邮件
server.quit() # 关闭发送
# ---------------发送邮件------end-----------------
发送方:
接收方:
和发送文本不一样的地方就是构建内容对象时,传入的内容时带有标签的,使用的媒体类型是HTML
from email.header import Header # 对中文进行编码
from email.mime.text import MIMEText # 邮件对象
from email.utils import parseaddr, formataddr # 格式化邮箱
import smtplib # 发送邮件
# 格式化邮箱(不格式化会被当做垃圾邮件去发送或者发送失败)
def format_addr(s):
name, addr = parseaddr(s) # 比如:东皇太一 ,
return formataddr((Header(name, 'utf-8').encode('utf-8'), addr))
# 准备数据
from_addr = '[email protected]' # 发件人
password = 'QXKWQKVZXWJDNWEP' # 授权码
smtp_server = 'smtp.163.com' # 服务器地址
to_addr = input('收件人邮箱:') # 收件人
# ---------------构建邮件内容对象------start---------------
html = """
菜鸟教程(runoob.com)
我的第一个标题
我的第一个段落。
"""
# 构建一个发送内容对象
msg = MIMEText(html, "html", "utf-8")
# 标准邮件需要三个头部信息 From To和Subject
msg['From'] = format_addr(u'东皇太一<%s>' % from_addr) # 发件人
to_name = input('收件人名称:')
msg['To'] = format_addr(u'{}<%s>'.format(to_name) % to_addr) # 发件人
msg['Subject'] = Header(u'Python学习资料!', 'utf-8').encode('utf-8') # 标题
# ---------------构建邮件内容对象------end-----------------
# ---------------发送邮件------start---------------
server = smtplib.SMTP(smtp_server, 25) # 163smtp服务器默认端口:25, smtp_server: 域名,等价于ip
server.set_debuglevel(1) # 是否显示发送日志,1显示,0不显示
server.login(from_addr, password)
server.sendmail(from_addr, to_addr, msg.as_string()) #发送邮件
server.quit() # 关闭发送
# ---------------发送邮件------end-----------------
发件方:
收件方:
带附件的邮件可以看做包含若干部分的邮件:文本和各个附件本身,所以,可以构造一个MIMEMultipart对象代表邮件本身,然后往里面加上一个MIMEText作为邮件正文再
继续往里面加上表示附件的MIMEBase对象即可。
from email.header import Header # 对中文进行编码
from email.mime.text import MIMEText # 邮件对象
from email.utils import parseaddr, formataddr # 格式化邮箱
import smtplib # 发送邮件
# 附件对象使用的模块
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
# 格式化邮箱(不格式化会被当做垃圾邮件去发送或者发送失败)
def format_addr(s):
name, addr = parseaddr(s) # 比如:东皇太一 ,
return formataddr((Header(name, 'utf-8').encode('utf-8'), addr))
# 准备数据
from_addr = '[email protected]' # 发件人
password = 'QXKWQKVZXWJDNWEP' # 授权码
smtp_server = 'smtp.163.com' # 服务器地址
to_addr = input('收件人邮箱:') # 收件人
# ---------------构建邮件内容对象------start---------------
# 创建附件对象
msg = MIMEMultipart()
# add MIMEText:
msg.attach(MIMEText('这是一封带有附件的Python发送邮件', 'plain', 'utf-8'))
# add file:
with open('tang.jpg', 'rb') as f:
# 设置附件的MIME和文件名以及类型
mine = MIMEBase('image', 'jpg', filename='tang.jpg')
# 加上必要的请求头信息
mine.add_header('Content-Disposition', 'attachment', filename='tang.jpg')
mine.add_header('Content-ID','<0>')
mine.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来
mine.set_payload(f.read())
# 用Base64编码
encoders.encode_base64(mine)
# 添加到MIMEMulpart
msg.attach(mine)
# 标准邮件需要三个头部信息 From To和Subject
msg['From'] = format_addr(u'东皇太一<%s>' % from_addr) # 发件人
to_name = input('收件人名称:')
msg['To'] = format_addr(u'{}<%s>'.format(to_name) % to_addr) # 发件人
msg['Subject'] = Header(u'Python学习资料!', 'utf-8').encode('utf-8') # 标题
# ---------------构建邮件内容对象------end-----------------
# ---------------发送邮件------start---------------
server = smtplib.SMTP(smtp_server, 25) # 163smtp服务器默认端口:25, smtp_server: 域名,等价于ip
server.set_debuglevel(1) # 是否显示发送日志,1显示,0不显示
server.login(from_addr, password)
server.sendmail(from_addr, to_addr, msg.as_string()) #发送邮件
server.quit() # 关闭发送
# ---------------发送邮件------end-----------------
发送方:
收件方: