文章目录
1. 网络编程
1.1 网络基础知识
1.1.1 网络通信标准
- OSI 7层模型
-
好处
-
建立了统一的通信标准
-
降低开发难度,每层功能明确,各司其职
-
七层模型实际规定了每一层的任务,该完成什么事情
-
-
TCP/IP模型
- 七层模型过于理想,结构细节太复杂
- 在工程中应用实践难度大
- 实际工作中以TCP/IP模型为工作标准流程
-
网络协议
-
什么是网络协议:在网络数据传输中,都遵循的执行规则。
-
网络协议实际上规定了每一层在完成自己的任务时应该遵循什么规范。
-
-
需要应用工程师做的工作 : 编写应用工功能,明确对方地址,选择传输服务。
1.1.2 通信地址
-
IP地址
-
IP地址 : 即在网络中标识一台计算机的地址编号。
-
IP地址分类
- IPv4 : 192.168.1.5
- IPv6 :fe80::80a:76cf:ab11:2d73
-
IPv4 特点
- 分为4个部分,每部分是一个整数,取值分为0-255
-
IPv6 特点(了解)
- 分为8个部分,每部分4个16进制数,如果出现连续的数字 0 则可以用 ::省略中间的0
-
IP地址相关命令
-
ifconfig : 查看Linux系统下计算机的IP地址
-
ping [ip]:查看计算机的连通性
-
-
公网IP和内网IP
- 公网IP指的是连接到互联网上的公共IP地址,大家都可以访问。(将来进公司,公司会申请公网IP作为网络项目的被访问地址)
- 内网IP指的是一个局域网络范围内由网络设备分配的IP地址。
-
-
端口号
-
端口:网络地址的一部分,在一台计算机上,每个网络程序对应一个端口。
-
端口号特点
- 取值范围: 0 —— 65535 的整数
- 一台计算机上的网络应用所使用的端口不会重复
- 通常 0——1023 的端口会被一些有名的程序或者系统服务占用,个人一般使用 > 1024的端口
-
1.2 UDP 传输方法
1.2.1 套接字简介
-
套接字(Socket) : 实现网络编程进行数据传输的一种技术手段,网络上各种各样的网络服务大多都是基于 Socket 来完成通信的。
-
Python套接字编程模块:import socket
1.2.3 UDP套接字编程
- 创建套接字
sockfd=socket.socket(family,type)
功能:创建套接字
参数:family 网络地址类型 AF_INET表示ipv4
type 套接字类型 SOCK_DGRAM 表示udp套接字 (也叫数据报套接字)
返回值: 套接字对象
下面是实际用法:用法1
import socket
sockfd=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
实例用法:用法2
from socket import *
sockfd=socket(AF_INET,SOCK_DGRAM)
- 绑定地址(3种方式绑定)
- 本地地址 :‘127.0.0.1’
- 网络地址 : ‘172.40.91.185’ (通过ifconfig查看)
- 自动获取地址: ‘0.0.0.0’
sockfd.bind(addr)
功能: 绑定本机网络地址
参数:是个元组(ip,port) #写法('0.0.0.0',8888),参数是个元组,要把元组传进入
# 3种用法
from socket import *
udp_socket=socket(AF_INET,SOCK_DGRAM)
# 1.网络地址 别人可以通过该ip地址与我通信
udp_socket.bind(("172.40.91.124",8888))#参数是个元组,元组里写ip和端口号
# 2.测试地址 别人可以通过127.0.0.1地址与我
# 通信,但是对方程序需要和我在同一计算机上
udp_socket.bind(("127.0.0.1",8888))
# 3.万能地址 别人可以通过以上两种情形访问
udp_socket.bind(("0.0.0.0",8888))
- 消息接收
data,addr = sockfd.recvfrom(buffersize)
功能: 接收UDP消息。
参数: 每次最多接收多少字节。比如写1024 #1024个字节
返回值:data 接收的内容,类型是字节码,想看的话需要.decode()解码
addr 消息发送方的ip地址
下面是实例:
from socket import *
# 创建udp套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)
# 绑定地址
udp_socket.bind(("0.0.0.0",8888)) #ip地址,端口号。#服务端的ip可以无脑写"0.0.0.0"
# 接收消息 data-->bytes,接收的内容类型是字节串,然后是需要自己.decode()解码
data,addr = udp_socket.recvfrom(1024) #参数是接收多少个字节,自己根据需求设定
print("从",addr,"收到:",data.decode()) #打印出谁发的什么内容。因为内容是字节串,所以要.decode()解码
# 发送消息 发给刚才的地址 #这个可以忽略
udp_socket.sendto(b"Thanks",addr)
# 关闭套接字
udp_socket.close()
- 消息发送
n = sockfd.sendto(data,addr)
功能: 发送UDP消息
参数: data 发送的内容 bytes格式,需要.encode()编码后传入
addr 目标ip地址
返回值:发送的字节数 #也可以不要返回值,也能执行
下面是实例:
from socket import *
# 创建udp套接字
udp_socket = socket(AF_INET,SOCK_DGRAM)
# 服务器地址
ADDR = ("172.40.91.124",8888)
# 发送输入的内容
msg = input(">>")
udp_socket.sendto(msg.encode(),ADDR) #传入的是字节码,所以要.encode()编码后传入
# 接收消息 #这里可以不看
data,addr = udp_socket.recvfrom(1024)
print("从服务端收到:",data.decode())
udp_socket.close()
- 关闭套接字
sockfd.close()
功能:关闭套接字
1.2.4 UDP套接字特点
- 可能会出现数据丢失的情况
比如udp_socket.recvfrom(4),但是客户端传了7个字节。就会导致少接收3个字节。 - 传输过程简单,实现容易
- 数据以数据包形式表达传输
是以1个字节1个字节去传输的 - 数据传输效率较高
这个是相对于TCP来说的
1.3 TCP 传输方法
1.3.1 TCP传输特点
-
面向连接的传输服务
- 传输特征 : 提供了可靠的数据传输,可靠性指数据传输过程中无丢失,无失序,无差错,无重复。
- 可靠性保障机制(都是操作系统网络服务自动帮应用完成的):
- 在通信前需要建立数据连接
- 确认应答机制
- 通信结束要正常断开连接
-
三次握手(建立连接)
- 客户端向服务器发送消息报文请求连接
- 服务器收到请求后,回复报文确定可以连接
- 客户端收到回复,发送最终报文连接建立
- 四次挥手(断开连接)
- 主动方发送报文请求断开连接
- 被动方收到请求后,立即回复,表示准备断开
- 被动方准备就绪,再次发送报文表示可以断开
- 主动方收到确定,发送最终报文完成断开
1.3.2 TCP服务端
- 创建套接字
和udp相比,只是把参数SOCK_DGRAM换成了SOCK_STREAM
sockfd=socket.socket(family,type)
功能:创建套接字
参数:family 网络地址类型 AF_INET表示ipv4
type 套接字类型 SOCK_STREAM 表示tcp套接字 (也叫流式套接字)
返回值: 套接字对象
下面是实际用法:用法1
import socket
sockfd=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
实例用法:用法2
from socket import *
sockfd=socket(AF_INET,SOCK_STREAM)
- 绑定地址 (与udp套接字相同)
- 设置监听
sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小 #看下面的解释
参数 : 监听队列大小。一般设置5,10,15 #设置5已经够用了,排队数据建立的很快的
我:解释:
将套接字设置为监听套接字:我理解是执行三次握手的第二步的一小步,向客户端响应可以建立连接。。。
确定监听队列大小:就是如果同一时间被多个客户端请求了。这么多客户端肯定会执行三次握手。这个时候就会开辟一个队列,让他们排队去执行三次握手。(当然执行速度很快,一般参数给5就行了)
- 处理客户端连接请求
connfd,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求。可以理解为接受连接。看不懂可以看我下面解释
返回值: connfd 客户端连接套接字。之后的收发消息就用这个调用了,用完也要。close()
addr 连接的客户端地址。自动获取到了
我理解是执行三次握手的第二步剩余的部分,sockfd.listen(n)设置了可以被连接, connfd,addr = sockfd.accept()这一步就设置了像客户端发送可以被连接。随后客户端确定连接,就三次握手完毕连接上了
- 接收消息(完整版模板)
from socket import *
sockfd = socket(AF_INET,SOCK_STREAM)#ipv4,tcp。参数不写也行,默认就是ipv4,tcp
sockfd.bind(('0.0.0.0',8888))#ip,端口号
sockfd.listen(5)#设置监听套接字,排队为5
print('等待建立链接')
connfd, addr = sockfd.accept()#接受连接
print(addr,'已链接')
#接收
data = connfd.recv(1024)#接收,设置为1024字节
data = data.decode()
print('收到',data)
#发送,可以不看
connfd.send(b"Thanks#")
connfd.close()#关闭客户端连接套接字
sockfd.close()#关闭套接字
- 关闭套接字 (与udp套接字相同)
循环单次接收消息(完整版模板)。客户端断开,服务端不断
from socket import *
from time import sleep
sockfd = socket(AF_INET, SOCK_STREAM) # ipv4,tcp。参数不写也行,默认就是ipv4,tcp
sockfd.bind(('0.0.0.0', 8888)) # ip,端口号
sockfd.listen(5) # 设置监听套接字,排队为5
while True:
print('等待建立链接')
connfd, addr = sockfd.accept() # 接受连接
print(addr, '已链接')
while True:
# 接收
data = connfd.recv(1024) # 接收,设置为1024字节
if not data or data == b'##': # ##是设置的主动断开,也可以不要或者换成其他的
break
print('收到', data.decode())
# 发送,可以不看
connfd.send(b"Thanks#")
sleep(0.1) # 控制发送速度
connfd.close() # 关闭客户端连接套接字
sockfd.close()
循环一对多接收消息(完整版模板)。客户端多个去连接。
"""
tcp 客户端流程
每次访问服务端都重新建立连接
"""
from socket import *
# 服务器地址
server_address = ("127.0.0.1",8888)
while True:
msg = input(">>")
if not msg:
break
tcp_socket = socket()
tcp_socket.connect(server_address)
tcp_socket.send(msg.encode())
data = tcp_socket.recv(1024)
print("从服务端收到:",data.decode())
tcp_socket.close()
###########################################################################
"""
tcp服务端 同时处理多个客户端
"""
from socket import *
# 创建tcp套接字
tcp_socket = socket(AF_INET,SOCK_STREAM)
tcp_socket.bind(("0.0.0.0",8888))
tcp_socket.listen(5)
# 每次接收一个消息都要 连接--断开
while True:
# 等待处理客户端连接
connfd,addr = tcp_socket.accept()
data = connfd.recv(1024)
print("收到:",data.decode())
connfd.send(b"Thanks")
connfd.close()
# 关闭套接字
tcp_socket.close()
1.3.3 TCP客户端
- 发送消息(完整版模板)
from socket import *
sockfd = socket(AF_INET,SOCK_STREAM)#ipv4,tcp。参数不写也行,默认就是ipv4,tcp
sockfd.connect(('192.168.137.129',8888))#连接服务器ip地址。参数:传入元组,ip和端口号,
#发送
a = input('>>')
sockfd.send(a.encode())
#接收,可以不看
data = sockfd.recv(1024)
print("从服务端收到:",data.decode())
sockfd.close()#关闭套接字
注意: 防止两端都阻塞,recv send要配合
- 关闭套接字
1.3.4 TCP套接字细节(TCP细节,TCP细节,TCP细节)
- tcp连接中当一端退出(或直接退出),另一端会阻塞在recv接收,此时recv会立即接收一个空。
所以如果有循环,最好有这个空的判断
data = a.recv(1024)
if not data:
break
-
tcp连接中如果A已经断开或退出,B仍然用send向A发送数据。则会报错BrokenPipeError
-
一个服务端可以同时连接多个客户端,也能够重复被连接
tcp粘包问题(只有tcp才会有粘包问题,看详细解释 和 解决方法)
-
产生原因
- 为了解决数传输过程sen()发的快,recv()收的慢,发送的东西来不及接收,就暂时堆在缓冲区等待接收。。。。但是实际网络工作过程比较复杂,会导致消息收发速度不一致。。。。 tcp以字节流方式进行数据传输,在接收时不区分消息边界(这个看下面的解释)
tcp以字节流方式进行数据传输,
打个比方,UDP是把水冻成了冰块发送,去发送整个数据包。服务端一次全部接收过来,如果recvfrom(7),接收后就只要7个字节,其他不要了。
打个比方,TCP像水流一样发送,按照recv(n)接收n个字节。。。如果客服端发了7个字节,服务端recv(3)接收就会分批3,3,1接收完。。。如果客户端发3次消息,分别是2个字节,1个字节,5个字节,服务端recv(10)大于3次总发送的字节,就会一次性把3次消息全部接收过来,前提是3次消息都在缓存区。这样就造成了粘包问题
- 带来的影响
- 如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
粘包问题处理方法
- 添加个消息边界,比如加个#号用于分割
send("发送的内容#")
- 控制发送的速度,在
send("发送的内容")
发送后,加个睡眠sleep(0.1)
注意sleep睡眠不要在接收大量数据场景下使用。
1.3.5 TCP与UDP对比
-
传输特征
- TCP提供可靠的数据传输,但是UDP则不保证传输的可靠性
- TCP传输数据处理为字节流,而UDP处理为数据包形式
- TCP传输需要建立连接才能进行数据传,效率相对较低,UDP比较自由,无需连接,效率较高
-
套接字编程区别
- 创建的套接字类型不同
- tcp套接字会有粘包,udp套接字有消息边界不会粘包
- tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
- tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom
-
使用场景
- tcp更适合对准确性要求高,传输数据较大的场景
- 文件传输:如下载电影,访问网页,上传照片
- 邮件收发
- 点对点数据传输:如点对点聊天,登录请求,远程访问,发红包
- udp更适合对可靠性要求没有那么高,传输方式比较自由的场景
- 视频流的传输: 如直播,视频聊天
- 广播:如网络广播,群发消息
- 实时传输:如游戏画面
- 在一个大型的项目中,可能既涉及到TCP网络又有UDP网络
- tcp更适合对准确性要求高,传输数据较大的场景
1.4 数据传输过程
1.4.1 传输流程
- 发送端由应用程序发送消息,逐层添加首部信息,最终在物理层发送消息包。
- 发送的消息经过多个节点(交换机,路由器)传输,最终到达目标主机。
- 目标主机由物理层逐层解析首部消息包,最终到应用程序呈现消息。
1.4.2 TCP协议首部信息
-
源端口和目的端口 各占2个字节,分别写入源端口和目的端口。
-
序号 占4字节。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。例如,一报文段的序号是301,而接待的数据共有100字节。这就表明本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。
-
确认号 占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
-
确认ACK(ACKnowledgment) 仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。
-
同步SYN(SYNchronization) 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1,因此SYN置为1就表示这是一个连接请求或连接接受报文。
-
终止FIN(FINis,意思是“完”“终”) 用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
2. 多任务编程
2.1 多任务概述
-
cpu轮询机制 : cpu都在多个任务之间快速的切换执行,切换速度在微秒级别,其实cpu同时只执行一个任务,但是因为切换太快了,从应用层看好像所有任务同时在执行。
-
多核CPU:现在的计算机一般都是多核CPU,比如四核,八核,我们可以理解为由多个单核CPU的集合。这时候在执行任务时就有了选择,可以将多个任务分配给某一个cpu核心,也可以将多个任务分配给多个cpu核心,操作系统会自动根据任务的复杂程度选择最优的分配方案。
- 并发 : 多个任务如果被分配给了一个cpu内核,那么这多个任务之间就是并发关系,并发关系的多个任务之间并不是真正的"同时"。
- 并行 : 多个任务如果被分配给了不同的cpu内核,那么这多个任务之间执行时就是并行关系,并行关系的多个任务时真正的“同时”执行。
-
什么是多任务编程
多任务编程即一个程序中编写多个任务,在程序运行时让这多个任务一起运行,而不是一个一个的顺次执行,实现多任务编程的方法 : 多进程编程,多线程编程
-
多任务意义
-
提高了任务之间的配合,可以根据运行情况进行任务创建。
比如: 你也不知道用户在微信使用中是否会进行视频聊天,总不能提前启动起来吧,这是需要根据用户的行为启动新任务。 -
充分利用计算机资源,提高了任务的执行效率。
-
在任务中无阻塞时只有并行状态才能提高效率
-
在任务中有阻塞时并行并发都能提高效率
-
-
2.2 进程(Process)
定义: 程序在计算机中的一次执行过程。程序是一个可执行的文件,是静态的占有磁盘。进程是一个动态的过程描述,占有计算机运行资源,有一定的生命周期。
-
进程状态
-
三态
就绪态 : 进程具备执行条件,等待系统调度分配cpu资源运行态 : 进程占有cpu正在运行 等待态 : 进程阻塞等待,此时会让出cpu
-
五态 (在三态基础上增加新建和终止)
新建 : 创建一个进程,获取资源的过程 终止 : 进程结束,释放资源的过程
-
-
进程命令
-
查看进程信息
ps -aux
- USER : 进程的创建者
- PID : 操作系统分配给进程的编号,大于0的整数,系统中每个进程的PID都不重复。PID也是重要的区分进程的标志。
- %CPU,%MEM : 占有的CPU和内存
- STAT : 进程状态信息,S I 表示阻塞状态 ,R 表示就绪状态或者运行状态
- START : 进程启动时间
- COMMAND : 通过什么程序启动的进程
-
进程树形结构
pstree
- 父子进程:在Linux操作系统中,进程形成树形关系,任务上一级进程是下一级的父进程,下一级进程是上一级的子进程。
-
进程编程模板
-
使用模块 : multiprocessing
-
创建流程
【1】 将需要新进程执行的事件封装为函数
【2】 通过模块的Process类创建进程对象,关联函数
【3】 通过进程对象调用start启动进程
-
主要类和函数使用
Process()
功能 : 创建进程对象
参数 : 都是关键字传参
target 绑定要执行的函数名
args 元组形式,用于给target函数位置传参
kwargs 字典形式,给target函数键值传参
daemon bool值,daemon=True,父进程结束,子进程立马就跟着结束
用法实例
from multiprocessing import Process #
def func(a, b): #创建进程函数
print('这是执行程序,%s' % a)
print('这是执行程序,%s' % b)
# 传参方式1:target=函数名,args传参必须是元组传入
#daemon看需求用不用传参,daemon=True,父进程结束,子进程立马就跟着结束
c = Process(target=func, args=(2, '小七'))
# 传参方式2:target=函数名,kwargs传参以字典关键字的形式传入
c = Process(target=func, kwargs={"a": 2, "b": '小七'})
# 传参方式3:target=函数名,元组和字典结合使用.args=(2,)加个逗号是一个值,为了它是元组
c = Process(target=func, args=(2,),kwargs={"b": '小七'})
c.start() #启动进程
#备注:如果没参数就不传了,传进程函数名target=func就行了
#下面这个是小函数,自己根据需求选择用不用
#不要和daemon一起用
c.join()#无参数:等待子进程全部执行完毕再执行
c.join(7)#有参数:等待子进程执行7秒后就执行
print('下面是父进程继续要执行的程序')
a.start()
功能 : 启动进程
注意 : 启动进程此时target绑定函数开始执行,该函数作为新进程执行内容,此时进程真正被创建
进程执行现象理解 (难点)
- 新的进程是原有进程的子进程,子进程复制父进程全部内存空间代码段,一个进程可以创建多个子进程。
- 子进程只执行指定的函数,其余内容均是父进程执行内容,但是子进程也拥有其他父进程资源。
- 各个进程在执行上互不影响,也没有先后顺序执行关系。
- 进程创建后,各个进程空间独立,相互没有影响。
- multiprocessing 创建的子进程中无法使用标准输入(即无法使用input)。
2.2.3 进程处理细节
- 进程相关函数
os.getpid()
功能: 获取一个进程的PID值
返回值: 返回当前进程的PID
os.getppid()
功能: 获取父进程的PID号
返回值: 返回父进程PID
import sys #是需要导包的
sys.exit("表示退出时打印内容")
功能:退出进程。比如:子进程要写sys.exit()就立马退出了这个子进程
参数:字符串 也可以不给参数。不给结束就不打印,给了结束就打印
多个子进程编程模板(终极模板+小函数)
from multiprocessing import Process #
import os
def a1():
print('这是子进程1')
print(os.getppid()) #父进程id
print(os.getpid()) #子进程id,也是自己的id
def a2():
print('这是子进程2')
def a3():
print('这是子进程3')
#同时运行多个子进程
for i in [a1,a2,a3]: #把函数名放在数组里,便利带入Process
a = Process(target=i)
a.start()
#####################################
#下面是结合jion确定全部子进程运行结束
list_a = [] #空数组
for i in [a1,a2,a3]:
a = Process(target=i)
list_a.append(a) #把子进程对象分别放入数组里
a.start()
for i in list_a: #便利子进程对象
a.join() #分别确认子进程结束
print('子进程确认全部结束')
##############另一种写法,结合了前面两种#################
#函数只写一个就行了,上面的代码除了导包,其他都不用
from time import sleep
def qq(a,b):
sleep(a)
print('这是子进程1',b)
for i in [(2,'77'),(1,'7'),(5,'777')]: #数据驱动函数
a = Process(target=qq,args=i) #函数是固定的,传参按照便利传
list_a.append(a) #把子进程对象分别放入数组里
a.start()
for i in list_a:
a.join()
print('子进程确认全部结束')
-
孤儿进程和僵尸进程
-
孤儿进程: 父进程先于子进程退出时,子进程会成为孤儿进程,孤儿进程会被系统自动收养,成为孤儿进程新的父进程,并在孤儿进程退出时释放其资源。
-
僵尸进程: 子进程先于父进程退出,父进程又没有处理子进程的退出状态,此时子进程就会成为僵尸进程。
特点: 僵尸进程虽然结束,但是会存留部分进程资源在内存中,大量的僵尸进程会浪费系统资源。Python模块当中自动建立了僵尸处理机制,每次创建新进程都进行检查,将之前产生的僵尸处理掉,而且父进程退出前,僵尸也会被自动处理。
-
创建进程类编程
- 创建步骤
【1】 继承Process类
【2】 重写__init__
方法添加自己的属性,使用super()加载父类属性
【3】 重写run()方法 - 使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
实例代码
from multiprocessing import Process #导包
class AAA(Process): #继承
def __init__(self,a):
self.a = a
super().__init__() #加载父类属性
def aa(self):
pass
def run(self): #重写run()方法
#这里也可以调用self.aa函数
print('run方法是进程内容,使用start会自动执行run方法了')
a = AAA(7)
a.start()
进程间通信
- 常用进程间通信方法:消息队列,套接字等。
- 消息队列使用
- 通信原理: 在内存中开辟空间,建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信。
- 实现方法
from multiprocessing import Queue
q = Queue(maxsize=0)
功能: 创建队列对象
参数:最多存放消息个数,不写默认最大化
返回值:队列对象
q.put(data)
功能:向队列存入消息
参数:data 要存入的内容
q.get()
功能:从队列取出消息
返回值: 返回获取到的内容
######################### 下面几个是小函数 #################################
q.full() 判断队列是否为满
q.empty() 判断队列是否为空
q.qsize() 获取队列中消息个数
q.close() 关闭队列
######################### 下面实例用法代码 #################################
from multiprocessing import Process,Queue
q = Queue(7) #1,队列设置为7,不写参数也行
def func(a): #子进程接
a += 1
q.put(a) #2,子进程推(push)值
a = Process(target=func,args=(7,)) #3.父进程传值
a.start()
a.join() #等待子进程执行完毕
c = q.get() #4,拉值
print(c)
**群聊聊天室 **
功能 : 类似qq群功能
【1】 有人进入聊天室需要输入姓名,姓名不能重复
【2】 有人进入聊天室时,其他人会收到通知:Lucy 进入了聊天室
【3】 一个人发消息,其他人会收到: Lucy : 一起出去玩啊。
【4】 有人退出聊天室,则其他人也会收到通知 : Lucy 退出了聊天室
【5】 扩展功能:服务器可以向所有用户发送公告: 管理员消息: 大家好,欢迎进入聊天室。
2.3 线程 (Thread)
2.3.2 多线程编程
-
线程模块: threading
-
创建方法
【1】 创建线程对象
from threading import Thread
#其实和
t = Thread()
功能:创建线程对象
参数:target 绑定线程函数
args 元组 给线程函数位置传参
kwargs 字典 给线程函数键值传参
daemon bool值,主线程推出时该分支线程也推出
#下面是实例代码
from threading import Thread
def a(b):
print(b,'这是线程')
s = Thread(target=a,args=(7,))
s.start()
【2】 启动线程
t.start()
【3】等待分支线程结束
p.join(a)
功能:阻塞等待分支线程退出
参数a:最长等待时间,不写参数就等待执行完再执行下面的程序
2.3.3 创建线程类
-
创建步骤
【1】 继承Thread类
【2】 重写__init__
方法添加自己的属性,使用super()加载父类属性
【3】 重写run()方法 -
使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
3.下面是实例代码
from threading import Thread
class A(Thread):
def __init__(self,b):
super().__init__()
self.b = b
def run(self):
print(self.b,'调用start自动执行run方法')
a = A(7)
a.run()#7 调用start自动执行run方法
2.3.4 线程同步互斥
-
线程通信方法: 线程可以直接修改全局变量进行通信
-
共享资源争夺
- 共享资源:多线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。
- 影响 : 对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机制协调操作顺序。
-
同步互斥机制
- 同步 : 同步是一种协作关系,为完成操作,线程间形成一种协调,按照必要的步骤有序执行操作。
- 互斥 : 互斥是一种制约关系,当一个进程或者线程占有资源时会进行加锁处理,此时其他进程线程就无法操作该资源,直到解锁后才能操作。
-
线程Event
这里我理解是同步
from threading import Event
e = Event() 创建线程event对象
e.wait() 阻塞等待e被set #参数不写数字就一直阻塞
e.set() 设置e,使wait结束阻塞
e.clear() 使e回到未被设置状态
e.is_set() 查看当前e是否被设置
#########################################################################################
下面是实例代码
from threading import Thread,Event
msg = "" # 通信变量
e = Event() # 创建同步互斥对象
def func():
global msg
msg = "777"
e.set() #使wait结束阻塞
t = Thread(target=func)
t.start()
e.wait() #阻塞等待。等待e.set()执行后,再执行下面的代码
if msg == "777":
print("正确")
else:
print("错误")
- 线程锁 Lock
这里我理解是互斥
from threading import Lock
lock = Lock() 创建锁对象
lock.acquire() 上锁 如果lock已经上锁再调用会阻塞
lock.release() 解锁
#########################################################################################
lock.acquire() 上锁
中间写被锁的代码
lock.release() 解锁
下面是实例代码
from threading import Thread , Lock
lock = Lock() # 锁对象
a = b = 0 # 共享资源
def func():
while True:
lock.acquire() #上锁
if a != b:
print("a = %d,b = %d"%(a,b))
lock.release() #解锁
t = Thread(target=func)
t.start()
while True:
lock.acquire() #上锁
a += 1
b += 1
lock.release() #解锁
2.3.5 死锁
-
死锁产生条件
-
互斥条件:指线程使用了互斥方法,使用一个资源时其他线程无法使用。
-
请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,在获取到新的资源前不会释放自己保持的资源。
-
不剥夺条件:不会受到线程外部的干扰,如系统强制终止线程等。
-
环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,如 T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。
-
-
如何避免死锁
- 逻辑清晰,不要同时出现上述死锁产生的四个条件
- 通过测试工程师进行死锁检测
2.3.6 GIL问题
-
什么是GIL问题 (全局解释器锁)
由于python解释器设计中加入了解释器锁,导致python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。
-
导致后果
Python线程不会出现并行执行
在无阻塞的事件执行中不能提高效率因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行多阻塞任务时可以提升程序效率,其他情况并不能对效率有所提升。
-
关于GIL问题的处理
-
尽量使用进程完成无阻塞的并发行为
-
不使用c作为解释器 (可以用Java C#)
Guido的声明:http://www.artima.com/forums/flat.jsp?forum=106&thread=214235
-
-
结论
- GIL问题与Python语言本身并没什么关系,属于解释器设计的历史问题。
- 在无阻塞状态下,多线程程序程序执行效率并不高,甚至还不如单线程效率。
- Python多线程只适用于执行有阻塞延迟的任务情形。
2.3.7 进程线程的区别联系
- 区别联系
- 两者都是多任务编程方式,都能使用计算机多核资源
- 进程的创建删除消耗的计算机资源比线程多
- 进程空间独立,数据互不干扰,有专门通信方法;线程使用全局变量通信
- 一个进程可以有多个分支线程,两者有包含关系
- 多个线程共享进程资源,在共享资源操作时往往需要同步互斥处理
- Python线程存在GIL问题,但是进程没有。
- 使用场景
- 任务场景:一个大型服务,往往包含多个独立的任务模块,每个任务模块又有多个小独立任务构成,此时整个项目可能有多个进程,每个进程又有多个线程。
- 编程语言:Java,C#之类的编程语言在执行多任务时一般都是用线程完成,因为线程资源消耗少;而Python由于GIL问题往往使用多进程。
3. 网络并发模型
3.1 网络并发模型概述
- 什么是网络并发
在实际工作中,一个服务端程序往往要应对多个客户端同时发起访问的情况。如果让服务端程序能够更好的同时满足更多客户端网络请求的情形,这就是并发网络模型。 - 循环网络模型问题
循环网络模型只能循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。这样的网络模型虽然简单,资源占用不多,但是无法同时处理多个客户端请求就是其最大的弊端,往往只有在一些低频的小请求任务中才会使用。
3.2 多进程/线程并发模型
多进程/线程并发模中每当一个客户端连接服务器,就创建一个新的进程/线程,客户端退出就销毁该进程/线程。
-
模型特点
- 优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。
- 缺点: 资源消耗较大
- 适用情况:客户端请求较复杂,需要长时间占有服务器。
-
创建流程
- 创建网络套接字
- 等待客户端连接
- 有客户端连接,则创建新的进程/线程具体处理客户端请求
- 主进程/线程继续等待处理其他客户端连接
- 如果客户端退出,则销毁对应的进程/线程
基于多进程的网络并发模板
"""
基于多进程的网络并发模型
重点代码 !!
"""
from socket import *
from multiprocessing import Process
import sys
# 服务器绑定地址
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)
# 处理客户端具体请求 (根据业务逻辑编写)
def handle(connfd):
while True:
data = connfd.recv(1024)
if not data:
break
print(data.decode())
connfd.send(b"OK")
connfd.close()
# 并发网络服务搭建
def main():
# 创建tcp套接字
sock = socket()
sock.bind(ADDR)
sock.listen(5)
print("Listen the port %d" % PORT)
# 循环等待客户端连接
while True:
try:
connfd,addr = sock.accept()
print("Connect from",addr)
except KeyboardInterrupt:
sock.close()
sys.exit("服务端退出")
# 为连接上的客户端创建子进程
p = Process(target=handle,args=(connfd,),daemon=True)
p.start()
if __name__ == '__main__':
main()
基于多线程的网络并发模板
"""
基于多线程的并发模型
重点代码
"""
from socket import *
from threading import Thread
# 线程类
class ThreadServer(Thread):
def __init__(self, connfd):
self.connfd = connfd
super().__init__(daemon=True)
def run(self):
#如果需求大,可以再创建个类,处理具体客户端请求,用这个方法去调用就行
while True:
data = self.connfd.recv(1024)
if not data:
break
print(data.decode())
self.connfd.send(b"OK")
self.connfd.close()
# tcp模型
class TcpServer:
"""
tcp 套接字创建
"""
def __init__(self, host="", port=0):
self.host = host
self.port = port
self.address = (host, port)
self.sock = self._create_socket()
def _create_socket(self):
sock = socket()
sock.bind(self.address)
return sock
# 启动函数
def serve_forever(self):
self.sock.listen(5)
print("Listen the port %d" % self.port)
while True:
connfd, addr = self.sock.accept()
print("Connect from", addr)
# 为客户端创建线程
t = ThreadServer(connfd)
t.start()
if __name__ == '__main__':
server = TcpServer(host="0.0.0.0", port=8888)
server.serve_forever()
3.3 IO并发模型
3.3.1 IO概述
-
什么是IO
存在读写数据操作行为,比如终端输入输出 ,文件读写,数据库修改和网络消息收发等。 -
程序分类
- IO密集型程序:在程序执行中有大量IO操作,而运算操作较少。消耗cpu较少,耗时长。
- 计算密集型程序:程序运行中运算较多,IO操作相对较少。cpu消耗多,执行速度快,几乎没有阻塞。
-
IO分类:阻塞IO ,非阻塞IO,IO多路复用等。
3.3.2 阻塞IO
- 定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。
- 效率:阻塞IO效率很低。但是由于逻辑简单所以是默认IO行为。
- 阻塞情况
- 因为某种执行条件没有满足造成的函数阻塞
e.g. accept input recv - 处理IO的时间较长产生的阻塞状态
e.g. 网络传输,大文件读写
- 因为某种执行条件没有满足造成的函数阻塞
3.3.3 非阻塞IO
- 定义 :通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。
- 设置套接字为非阻塞IO
实例代码sockfd.setblocking(bool) 功能:设置套接字为非阻塞IO 参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞
from socket import *
from time import ctime,sleep
sock = socket()
sock.bind(("0.0.0.0",8888))
sock.listen(5)
# 将套接字设置非阻塞
sock.setblocking(False)
file = open("my.log",'a')
while True:
try:
connfd,addr = sock.accept()
print(addr,'连接了')
except BlockingIOError as e: #捕获指定异常
print('如果try阻塞了,就执行我这里,我执行完毕再去看try,如果还在阻塞继续执行我')
print('下面是自己写的东西,2秒执行写入一次时间和捕获的异常')
msg = "%s : %s\n"%(ctime(), e)
file.write(msg)
sleep(2)
else:
data = connfd.recv(1024)
print(data.decode())
- 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。
实例代码sockfd.settimeout(sec) 功能:设置套接字的超时时间 参数:设置的时间
from socket import *
from time import ctime,sleep
sock = socket()
sock.bind(("0.0.0.0",8888))
sock.listen(5)
# 设置超时时间
sock.settimeout(2)
file = open("my.log",'a')
while True:
try:
connfd,addr = sock.accept()
print(addr,'连接了')
except timeout as e: #捕获指定异常
print('如果try阻塞了,最多阻塞两秒,就执行我这里,我执行完毕再去看try,如果还在阻塞,最多两秒...')
print('下面是自己写的东西,2秒执行写入一次时间和捕获的异常')
msg = "%s : %s\n"%(ctime(), e)
file.write(msg)
else:
data = connfd.recv(1024)
print(data.decode())
3.3.4 IO多路复用
-
定义
同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
-
具体方案
- select方法 : Windows Linux Unix
- epoll方法: Linux
select 方法
from select import select
rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能: 监控IO事件,阻塞等待IO发生
参数: rlist 列表 读IO列表,添加等待发生的或者可读的IO事件
wlist 列表 写IO列表,存放要可以主动处理的或者可写的IO事件
xlist 列表 异常IO列表,存放出现异常要处理的IO事件
timeout 超时时间,超出这个时间,就执行下面的代码(不是必填项)
返回值: rs 列表 rlist中准备就绪的IO
ws 列表 wlist中准备就绪的IO
xs 列表 xlist中准备就绪的IO
select实例代码1,这个是解决服务端连接多个客户端,一步步看,更容易理解
from socket import *
from select import select
soke = socket()#tcp套接字
soke.bind(('0.0.0.0', 8888))
soke.listen(5)
# 存放要监控的IO对象
rlist = [soke]
wlist = []
xlist = []
# 循环监控IO发生,并处理发生的IO事件
while True:
rs, ws, xs = select(rlist, wlist, xlist)
for i in rs:#便利
# 当rlist监控的IO增加,再就绪时要分情况讨论
if i is soke: #如果是客户端链接 soke是创建的TCP套接字对象
fd, arrd = i.accept()
print(arrd, '已链接')
# 连接的客户端套接字加入到监控列表
rlist.append(fd)
else:#如果是客户端发消息
data = i.recv(1024)
if not data:#处理客户端断开
print(i, '退出链接')
rlist.remove(i)
i.close()
continue #跳出这次
print(data.decode())
i.send(b"OK")
select实例代码2,这个是解决客户端发的数据过多,其他客户端需要排队等。。IO多路复用多和非阻塞IO配合
实例代码1解决了处理多个客户端,但是如果有 a b c d 四个客户端发消息,a发的数据过于大,bcd3个就要等待,那么如何解决这个阻塞呢?看下面代码
其实原理很简单,就是
soke.setblocking(False)#设置套接字非阻塞
fd.setblocking(False)#设置连接的客户端非阻塞
from socket import *
from select import select
soke = socket()#tcp套接字
soke.bind(('0.0.0.0', 8888))
soke.listen(5)
# 存放要监控的IO对象
rlist = [soke]
wlist = []
xlist = []
# IO多路复用多和非阻塞IO配合
soke.setblocking(False)#设置套接字非阻塞
# 循环监控IO发生,并处理发生的IO事件
while True:
rs, ws, xs = select(rlist, wlist, xlist)
for i in rs:#便利
# 当rlist监控的IO增加,再就绪时要分情况讨论
if i is soke: #如果是客户端链接 soke是创建的TCP套接字对象
fd, arrd = i.accept()
print(arrd, '已链接')
# 设置连接的客户端非阻塞
fd.setblocking(False)
# 连接的客户端套接字加入到监控列表
rlist.append(fd)
else:#如果是客户端发消息
data = i.recv(1024)
if not data:#处理客户端断开
print(i, '退出链接')
rlist.remove(i)
i.close()
continue #跳出这次
print(data.decode())
i.send(b"OK")
select方法:写IO列表 和 异常IO列表,该怎么用?和实例代码1,2的逻辑一样,rs, ws, xs,用哪个就便利哪个,都用就都便利
epoll方法
from select import *
ep = epoll()
功能 : 创建epoll对象
返回值: epoll对象
ep.register(fd,event)
功能: 注册关注的IO事件
参数:fd 要关注的IO对象
event 要关注的IO事件类型
常用类型EPOLLIN 读IO事件(就是select方法的rlist一样的意思)
EPOLLOUT 写IO事件 (就是select方法的wlist)
EPOLLERR 异常IO (就是select方法的xlist)
e.g. ep.register(sockfd,EPOLLIN|EPOLLERR)#类型可以写一个或多个,用|符号隔开就行
ep.unregister(fd)
功能:取消对IO的关注
参数:IO对象或者IO对象的fileno
#fileno就是描述符号,都是数字。这么的来的?就是 IO对象.fileno() 就可以返回
events = ep.poll()
功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IO
events格式 [(fileno,event),()....]
每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
epoll实例代码1,这个是解决服务端连接多个客户端,一步步看,更容易理解
from socket import *
from select import *
soke = socket()
soke.bind(('0.0.0.0', 8888))
soke.listen()
#查找字典 始终跟关注的IO保持一致
map = {
'键是IO的fileno': '值是IO对象',
soke.fileno(): soke
}
ep = epoll()#创建对象
ep.register(soke,EPOLLIN)#关注soke的IO
while True:
events = ep.poll() #监控的IO事件发生
#events的类型是[(fileno,event),()....],元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
#所以用ij分开接收第一项第二项
for i,j in events:
if i == soke.fileno(): #如果是客户端连接
fd, addr = map[i].accept()
print(addr, '链接上')
ep.register(fd, EPOLLIN) #把客户端套接字设置关注IO
map[fd.fileno()] = fd #把客户端套接字 存入map字典
else: #如果是客户端发消息
data = map[i].recv(1023).decode()
if not data:
print(map[i], '断开链接')
ep.unregister(i) #客户端套接字断开移除关注
map[i].close() #关闭客户端套接字
del map[i] #从map字典里删除客户端套接字
continue
print(data)
map[i].send(b'OK')
epoll实例代码2,这个是解决客户端发的数据过多,其他客户端需要排队等。。IO多路复用多和非阻塞IO配合
实例代码1解决了处理多个客户端,但是如果有 a b c d 四个客户端发消息,a发的数据过于大,bcd3个就要等待,那么如何解决这个阻塞呢?看下面代码
其实原理很简单,就是
soke.setblocking(False)#设置套接字非阻塞
fd.setblocking(False)#设置连接的客户端非阻塞
from socket import *
from select import *
soke = socket()
soke.bind(('0.0.0.0', 8888))
soke.listen()
#查找字典 始终跟关注的IO保持一致
map = {
'键是IO的fileno': '值是IO对象',
soke.fileno(): soke
}
# IO多路复用多和非阻塞IO配合
soke.setblocking(False)#设置tcp套接字soke为非阻塞
ep = epoll()#创建对象
ep.register(soke,EPOLLIN)#关注soke的IO
while True:
events = ep.poll() #监控的IO事件发生
#events的类型是[(fileno,event),()....],元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
#所以用ij分开接收第一项第二项
for i,j in events:
if i == soke.fileno(): #如果是客户端连接
fd, addr = map[i].accept()
print(addr, '链接上')
# IO多路复用多和非阻塞IO配合
fd.setblocking(False)#设置客户端套接字fd为非阻塞
ep.register(fd, EPOLLIN) #把客户端套接字设置关注IO
map[fd.fileno()] = fd #把客户端套接字 存入map字典
else: #如果是客户端发消息
data = map[i].recv(1023).decode()
if not data:
print(map[i], '断开链接')
ep.unregister(i) #客户端套接字断开移除关注
map[i].close() #关闭客户端套接字
del map[i] #从map字典里删除客户端套接字
continue
print(data)
map[i].send(b'OK')
epoll实例代码3,写IO列表 和 异常IO列表,该怎么用?我在实例代码1,2的基础上改写一小段代码。很简单改一下 if 判断就行了
if i == sock.fileno():
pass# 处理客户端连接
elif j == EPOLLIN:
pass # 处理客户端发送消息
elif j == EPOLLOUT:# 写
map[i].send(b"OK")
ep.unregister(i)
ep.register(i, EPOLLIN)
- select 方法与epoll方法对比
- epoll 效率比select要高
- epoll 同时监控IO数量比select要多
- epoll 支持EPOLLET触发方式
select : 支持操作系统全面
同时监控的IO数量 1024个,效率没有epoll高
epoll : 支持IO数量无限制,执行效率高
只支持Linux系统
水平触发:当事件发生,如果不处理会一直通过IO多
路复用刚返回,直到处理为止
边缘触发:epoll支持,当事件发生,如果不处理
则不再提示,等下此再有IO就绪时再一
起提示
3.3.5 IO并发模型
利用IO多路复用等技术,同时处理多个客户端IO请求。
-
优点 : 资源消耗少,能同时高效处理多个IO行为
-
缺点 : 只针对处理并发产生的IO事件
-
适用情况:HTTP请求,网络传输等都是IO行为,可以通过IO多路复用监控多个客户端的IO请求。
-
网络并发服务实现过程
【1】将套接字对象设置为关注的IO,通常设置为非阻塞状态。
【2】通过IO多路复用方法提交,进行IO监控。
【3】阻塞等待,当监控的IO有事件发生时结束阻塞。
【4】遍历返回值列表,确定就绪的IO事件类型。
【5】处理发生的IO事件。
【6】继续循环监控IO发生。
4. web服务
4.1.2 网页访问流程
- 客户端(浏览器)通过tcp传输,发送http请求给服务端
- 服务端接收到http请求后进行解析
- 服务端处理请求内容,组织响应内容
- 服务端将响应内容以http响应格式发送给浏览器
- 浏览器接收到响应内容,解析展示
4.1.2 HTTP请求
- 请求行 : 具体的请求类别和请求内容
GET / HTTP/1.1
请求类别 请求内容 协议版本
请求类别:每个请求类别表示要做不同的事情
GET : 获取网络资源
POST :提交一定的信息,得到反馈
HEAD : 只获取网络资源的响应头
PUT : 更新服务器资源
DELETE : 删除服务器资源
- 请求头:对请求的进一步解释和描述
Accept-Encoding: gzip
- 空行
- 请求体: 请求参数或者提交内容
4.1.3 HTTP响应
- 响应行 : 反馈基本的响应情况
HTTP/1.1 200 OK
版本信息 响应码 附加信息
响应码 :
1xx 提示信息,表示请求被接收
2xx 响应成功
3xx 响应需要进一步操作,重定向
4xx 客户端错误
5xx 服务器错误
- 响应头:对响应内容的描述
Content-Type: text/html
Content-Type: image/jpeg
- 空行
- 响应体:响应的主体内容信息
5. 高并发技术探讨
5.1 高并发问题
-
衡量高并发的关键指标
-
响应时间(Response Time) : 接收请求后处理的时间
-
同时在线用户数量:同时连接服务器的用户的数量
-
每秒查询率QPS(Query Per Second): 每秒接收请求的次数
-
每秒事务处理量TPS(Transaction Per Second):每秒处理请求的次数(包含接收,处理,响应)
-
吞吐量(Throughput): 响应时间+QPS+同时在线用户数量
-
-
多大的并发量算是高并发
-
没有最高,只要更高
比如在一个小公司可能QPS2000+就不错了,在一个需要频繁访问的门户网站可能要达到QPS5W+
-
C10K问题
早先服务器都是单纯基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程占用操作系统资源多,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的,这就是著名的C10k问题。创建的进程线程多了,数据拷贝频繁, 进程/线程切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!
-
5.2 更高并发的实现
为了解决C10K问题,现在高并发的实现已经是一个更加综合的架构艺术。涉及到进程线程编程,IO处理,数据库处理,缓存,队列,负载均衡等等,这些我们在后面的阶段还会学习。此外还有硬件的设计,服务器集群的部署,服务器负载,网络流量的处理等。
实际工作中,应对更庞大的任务场景,网络并发模型的使用有时也并不单一。比如多进程网络并发中每个进程再开辟线程,或者在每个进程中也可以使用多路复用的IO处理方法。