第一 网络协议简单介绍
1. 目标:编写一个C/S架构的软件
C/S: Client(客户端)--------基于网络----------Server
B/S: Browser(浏览器)-------基于网络----------Server
2. 服务端需要遵循的原则:
2.1. 服务端与客户端都需要有唯一的地址,但是服务端的地址必须固定/绑定
2.2 对外一直提供服务,稳定运行
2.3 服务端应该支持并发
3. 网络(为了数据交互,也就是通信)
网络=底层的物理介质+互联网协议
4. 互联网协议:OSI七层协议的介绍
4.1.物理层:
功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
常见的物理设备:中继器,集线器,双绞线
4.2 数据链路层:
功能: 以太网协议
ethernet规定:
1.一组电信号构成一个包,叫做帧,每一个数据的帧分成报头head和data数据
其中报头为固定字节(18字节),其内容包含源地址,目标地址,数据类型
2.ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址
便是指网卡的地址,即mac地址 通常由12位 16进制表示的
3.在同一网络内,可以通过广播的形式进行通信
常见物理设备:网桥,交换机,网卡
4.3 网络层: (IP协议)
功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址即 IP地址
IP协议:采用的v4版本即ipv4,它规定网络地址由32位2进制表示,范围0.0.0.0-255.255.255.255
一个ip地址通常写成四段十进制数 172.10.10.1
子网掩码:可以通过IP地址和子网掩码按照AND运算的结果判断是否在同一个子网中
IP数据包: 也包含了报头和数据二部分
ARP协议:广播的方式发送数据包,获取目标主机的mac地址(前提是获得IP地址)
如何获得mac地址,首先先确定二个IP地址是否在同一个局域网内,若在,通过广播的形式传包.
常见物理设备:路由器,交换机
4.4 传输层:(TCP和UDP协议)
功能:建立端口到端口的通信
端口(port)即应用程序与网卡关联的编号,端口范围0-65535,0-1023为系统占用端口
ip+mac+port:可以标识全世界范围内独一无二的一个应用软件(基于网络通信)
1.TCP协议:传输控制协议 可靠性:每次发一个包,需要等到对方的回复信息,在进行下一次
三次握手:(建立双向通道)
客户端发请求信息-----标识syn=1+x,序列号seq=x------->服务端确认并回复/服务端发请求信息----
(状态:syn_sent) (状态:listen) (状态:syn_rcvd)
---标识ack=1+x/标识syn=1+y,序列号seq=y------->回到客户端回复确认----标识ack=1+y----->回到服务端
(状态:established) (状态:established)
各个状态表示的意思:
客户端:SYN_SENT---->客户端发送请求等待一个匹配的请求
ESTABLISHED----->表示一个打开的连接,接收到数据可以被投递给用户,连接的数据传输阶段的正常状态
服务端:LISTEN----->等待从客户端的连接请求
SYN_RCVD---->服务端确认请求,并发送连接请求,等待连接请求的确认
ESTABLISHED----->确定建立了服务端到客户端的通道
四次挥手:
客户端发断开连接请求---fin=1--------->服务端确认----ack=1------->客户端(被动断开)
(状态:fin_wait_1) (状态:close_wait) (状态:fin_wait_2)
服务端发断开请求------->fin=1----->客户端确认并回复-----ack=1--->服务端
(状态:last_ack) (状态:time_wait)
各个状态表示的意思:
客户端:FIN_WAIT_1--->断开连接的请求
FIN_WAIT_2--->被动等待服务端断开的请求
TIME_WAIT---->等待足够的时间过去以确保服务端接收到它的连接终止请求的确认
服务端:
CLOSE_WAIT----->等待客户端的终止请求
LASK_ACK----->等待先前发给客户端的连接终止请求的确认
通常是服务端发起断开请求
syn洪水攻击(拒绝服务攻击DOS):模拟大量假的客户端发送syn请求,形成过多的syn_rcvd
半链接池:服务端中 存链接请求信息,是一种队列,限制的是请求数量 backlog
特点: 数据传输可靠性高,传输效率低
2.UDP协议:无需建链接,发数据不用等对方确认
特点: 传输效率高,数据可靠性低
常见物理设备:交换机,路由器
4.5应用层(应用层,表示层,会话层)
功能:用户使用的应用程序 ,规定应用程序的数据格式
补充说明:浏览器URL的解析 默认端口80
url:统一资源定位符:整个互联网的惟一的的资源
https://i.cnblogs.com/EditPosts
组成部分: 浏览器协议 +域名+路径
DNS系统 将域名解析成IP地址
实现网络通信四要素:
本机的IP地址
子网掩码
网关的IP地址
DNS的IP地址
第二 socket(套接字)的介绍
1.定义:
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,为一组接口socket已经为我们封装好了
2.简单C/S的架构编程:
服务端:导入socket模块
from socket import socket, AF_INET, SOCK_STREAM
socket 套接字
AF_INET IPv4
SOCK_STREAM socket流
IP = "127.0.0.1"
PORT = 8888
ADDRESS = (IP, PORT)
BUFSIZE = 1024
print("服务器开启了")
1.创建服务器socket对象
serSocket = socket(AF_INET, SOCK_STREAM)
2.设置服务器地址
serSocket.bind(ADDRESS)
3.设置连接管理队列
serSocket.listen(5)
4.建立客户端服务器连接
conn, addr = serSocket.accept()
print(conn)
print(addr)
5.收发数据
data = conn.recv(BUFSIZE)
print(data.decode("utf-8"))
conn.send("服务器返回的数据".encode("utf-8"))
6.断开客户端
conn.close()
7.关闭服务器---------->没有此项
serSocket.close()
print("服务器关闭了")
------------------------------------------------------------
客户端:(发送的数据都是二进制,注意转换)
from socket import socket, AF_INET, SOCK_STREAM
IP = "127.0.0.1"
PORT = 8888
ADDRESS = (IP, PORT)
BUFSIZE = 1024
print("客户端开启了")
1.创建客户端socket对象
cliSocket = socket(AF_INET, SOCK_STREAM)
2.连接服务器地址
cliSocket.connect(ADDRESS)
print(cliSocket)
3.收发数据
cliSocket.send("客户端发送的数据".encode("utf-8"))
data = cliSocket.recv(BUFSIZE)
print(data.decode("utf-8"))
4.关闭客户端
cliSocket.close()
print("客户端关闭了")
3.应用:
1.基于TCP协议的套接字(通信循环和链接循环)
1.通信循环
1.1添加方式:
在收发消息的前添加while循环语句
1.2解决bug的方法:
客户端在停止运行时服务端会报错(window操作系统)
服务端处理方式:try....except....
操作系统为linux(不会报错,但会一直收到空)
服务端处理方式:在接收数据后加上条件判断(判断接收数据是否为空,为空则跳出循环)
2.链接循环:
在服务端accept()上加上while循环语句
服务端代码:
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn,client_addr=server.accept()
while True:
try:
data=conn.recv(1024)
if len(data)==0:break
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
server.close()
客户端代码:
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8081))
while True:
cmd=input('>>>:').strip()
if len(cmd)==0:continue
client.send(cmd.encode('utf-8'))
data=client.recv(1024)
print(data)
client.close()
2.粘包问题:
2.1 粘包问题就是因为接收方不知道数据之间的界限,不知道一次性需要提取多少字节的数据造成的
2.2 常见类型:
1.数据量小,发送时间间隔短,就会合到一起发送,形成粘包
2.接收发不及时接收缓冲区的数据,造成多个包的接收
2.3 解决粘包的方法:
自定义报头:包头做成字典,字典里包含将要发送的真实数据,然后json序列化.然后用struct将
序列化的数据长度打包成4个字节,
使用struct模块:
struct 模块可以把一个类型 如数字,转换成固定长度的字节bytes
转化成报头固定长度:struct.pack(i,len({})
反转换成包头的长度:struct.unpack(i,s.recv(4)[0]
发送时:
自定义个字典
header={'file_size':1073741824000} 文件的大小
序列化并转换成字节
header_json=json.dumps(header)
header_bytes=header.encode('utf-8')
用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack(i,len(header_bytes))
先发送报头
conn.send(head_len_bytes)
再发送报头的字节格式
conn.send(header_bytes)
再发送真实的数据
conn.send(真实的内容)
接收时:先接收报头长度,用struct提取出来,根据取出长度收取报头内容然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
先收报头4个bytes,得到报头长度的字节格式
head_len_bytes=s.recv(4)
提取报头的长度
x=struct.unpack(i,head_len_bytes)[0]
按照报头长度x,收取报头的bytes格式
head_bytes=s.recv(x)
反序列化
head=json.loads(head_bytes.decode('utf-8)')
获取真实信息的长度
head_len=head['file_size']
2.4解决粘包的案例:
服务端代码:
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8081))
server.listen(5)
while True:
conn,add=server.accept()
while True:
try:
cmd=conn.recv(1024)
if cmd==0:break
obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout=obj.stdout.read()
stderr=obj.stderr.read()
head_dict={'total_size':len(stdout)+len(stderr)}
head_bytes=json.dumps(head_dict).encode('utf-8')
head_bytes_len=struct.pack('i',len(head_bytes))
conn.send(head_bytes_len)
conn.send(head_bytes)
conn.send(stderr)
conn.send(stdout)
except ConnectionResetError:
break
conn.close()
客户端代码:
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8081))
while True:
cmd=input('请输入指令:').strip()
if not cmd :continue
client.send(cmd.encode('utf-8'))
head_lize=struct.unpack('i',client.recv(4))[0]
head_dic=json.loads(client.recv(head_lize).decode('utf-8'))
total_size=head_dic['total_size']
cmd_res=b''
recv_size=0
while recv_size<total_size:
data=client.recv(1024)
recv_size+=len(data)
cmd_res+=data
print(cmd_res.decode('gbk'))
2. 基于udp协议的套接字
也称为数据报协议,无粘包现象,最大可发送字节512,一般用于查找时间,DNS系统上
服务端代码:
from socket import *
server=socket(AF_INET,SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
while True:
data,client_add=server.recvfrom(1024)
print(data)
server.sendto(data.upper(),client_add)
客户端代码:
from socket import *
client=socket(AF_INET,SOCK_DGRAM)
while True:
msg=input('>>>>:').strip()
client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
data,server_add=client.recvfrom(1024)
print(data)
第三 socketserver模块
应用:
1基于TCP协议实现并发:
self.server即套接字对象
self.request即一个链接(conn)
self.client_address即客户端地址
具体代码:
服务端:
import socketserver
class MyTCPhandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
data=self.request.recv(1024)
if len(data)==0:break
print('>>>收到客户端信息%s'%data)
self.request.send(data.upper())
except ConnectionResetError:
break
self.request.close()
if __name__ == '__main__':
server=socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTCPhandler)
server.serve_forever()
客户端不需要修改
2基于UDP协议实现并发
self.client_address即客户端地址
具体代码:
服务端:
import socketserver
class MyUDPhandler(socketserver.BaseRequestHandler):
def handle(self):
data,sock=self.request ---->是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象)
print(data)
sock.sendto(data.upper(),self.client_address)
if __name__ == '__main__':
server=socketserver.ThreadingUDPServer(('127.0.0.1',8080),MyUDPhandler)
server.serve_forever()
第四 进程知识点
1.进程的理论知识点:
进程:一个正在进行/运行的程序,进程是用来描述程序运行过程的虚拟概念
进程和程序的区别:
程序:是一堆代码
进程:是程序运行的过程
进程的概念起源于操作系统,进程是操作系统最核心的概念
操作系统:
1.操作系统的定义:
操作系统是一个协调,管理,控制计算机硬件资源与软件资源的一段控制程序
2.功能:
1.将复杂的硬件操作功能封装成简单的功能提供给用户或应用程序使用
2.将多进程对硬件的竞争变得有序化
3.操作系统的发展史:
1.第一代计算机:没有操作系统的概念,所有的程序设计都是直接操控硬件
优点:独享整个资源
缺点:浪费计算机资源,一个时间段内只有一个人用
注意: 同一时刻程序的运行时串行的(一个任务完整的运行完后,再运行下一个程序)
2.第二代计算机:批处理系统
优点:批处理,节省机时
缺点:需要人工参与,仍然是串行,程序的调试不方便
3.第三代计算机:
多道技术:
定义:是多个程序,多道技术的实现是为了解决多个程序竞争或者说共享同一个资源(比如cpu)的有序调度问题
解决方式:多路复用
空间上的复用(资源共享):多个任务复用内存空间
时间上的复用:
1.一个任务占用CPU时间过长会被操作系统强行剥夺走CPU的执行权限:比起串行来执行效率降低了
2.一个任务遇到IO操作也会被操作系统强行剥夺走cpu的执行权限,比起串行来,执行效率提升了
并发:多个任务看起来是同时运行的
2. 开启进程的两种方式(*****)
通过multiprocessing模块导入Process
window: creatprocess接口 ,linux :fork接口
开启进程的第一种方式:
def task(name):
print('%s开始运行>>>>'%name)
time.sleep(3)
print('%s结束运行'%name)
if __name__ == '__main__':------>在window中Process必须放在if __name__=='__main__'下面
p=Process(target=task,args=('egon',),name='子进程')
p.start()
print('主程序运行中>>>')
开启进程的第二种方式:(自定义类)
class MyProcess(Process):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):------------------->固定格式
print('%s开始运行'%self.name)
time.sleep(3)
print('%s运行结束'%self.name)
if __name__=='__main__':
p=MyProcess('egon')
p.start()-------------->start会自动调用run()
print('主进程开始运行')
3. 进程对象的join方法(*****)
join()方法:让主进程在原地等待,等待子进程运行完毕,不会影响子进程的执行
主动回收僵尸进程资源
4. 进程之间内存空间隔离(*****)
在建子进程的时候会将主进程的数据copy给子进程
x=100
def task():
global x
x=10
print(x)
if __name__ == '__main__':
p=Process(target=task,)
p.start()
time.sleep(1)
print(x)
5. 进程对象其他相关的属性或方法
Process类的使用:
p=Process(target='',args='',name='')-->实例化产生一个对象,表示一个子进程的任务
参数介绍:
1.target:表示调用的对象,即子进程需要执行的任务
2.args: 表示调用对象的位置参数,为一个元组,必须要有逗号,kwargs是一个字典的形式
3.name: 为子进程的名字
方法介绍:
p.start() 启用进程,并调用p.run()方法
p.terminate() 强制终止子进程
p.is_alive() 子进程是否存活 ,返回值为bool
属性介绍:
p.name 进程的名字
p.pid 进程的id
查看进程的id有二种方式:
current_process().pid
os.getpid()
os.getppid()获得父类的id
6. 僵尸进程与孤儿进程
僵尸进程:
一个进程使用了fork创建的子进程,如果子进程退出了,父进程并没有调用获取子进程的状态信息,
仍会存在系统中,这种称为僵尸进程.
有害:会占用进程id
孤儿进程:
一个父进程退出后,而他的子进程仍在运行,这些进程称为孤儿进程,
这些进程将会被init进程收养,完成收集状态信息工作
无害
7. 守护进程
守护进程:
本质就是一个"子进程",该"子进程"的生命周期<=被守护进程的生命周期
p.daemon=True 方式表示,需要放在p.start()方法前
8. 互斥锁(自定义锁)
进程之间数据时不共享的,但共享一套文件或者硬件是可以的,
而共享带来的是竞争,竞争带来的结果是错乱,如何控制,进行加锁
使用multiprocessing 模块中Lock
lock=Lock()
lock.acquire() ə'kwaɪə
进程
lock.release()
来控制,将并发改成串行,牺牲了运行效率,但保证了数据的安全
一般加在修改共享数据的那一小部分代码
9. 进程间通信IPC机制
IPC机制:指进程间的通信
分为管道和队列
一般推荐的是队列:
使用方式:
导用模块:from multiprocessing import Queue
q=Queue(maxsize) maxsize指允许可存放的最大数
其方法有:
q.put() 将数据插入到队列中去 二个参数 blocked(默认值为True) 和timeout(超时)
q.get() 从队列中获取一个值,二个参数 blocked(默认值为True) 和timeout(超时)
10. 生产者消费者模型(******)
1.什么是生产者消费模型
生产者:代指生产数据任务
消费者:代指处理数据任务
2.工作方式:
生产者--->队列<-----消费者
3.为何要用:
当程序中出现二类任务:一类是生产数据,另一类是处理数据,就可以引入生产者消费者模型
从而实现生产者与消费者的解耦合,平衡生产力和消费力,提升效率
ps:在使用生产者与消费者模型的时候需要注意消费者进程运行完的问题
处理的方式:
p.join()结束生产者的进程,在消费者功能内添加队列末尾值q.put(None)的判断条件
匹配就结束进程(生产者进程结束后,在队列中增加None)
第五 线程知识点(*****)
1.线程的理论;
1.什么是线程:
进程其实就是一个资源单位,而进程内的线程才是CUP上的执行单位
线程其实就是代码的执行过程
2.为何要用:
线程的特点:
1.同一个进程下的多个线程共享该进程内的资源
2.创建线程的开销要远远小于进程
2. 开启线程的两种方式(****)
方式一:
from threading import Thread
import time
def task():
print('程序开始运行')
time.sleep(1)
print('程序结束运行')
if __name__ == '__main__':
t=Thread(target=task)
t.start()
print('主线程运行结束')
方式二:
class Mythread(Thread):
def run(self):
print('程序开始运行')
time.sleep(1)
print('程序结束运行')
if __name__ == '__main__':
t=Mythread()
t.start()
print('主线程运行结束')
3. 线程对象其他相关的属性或方法
threading模块中Thread实例化
方法:
t.isalive -->判断线程是否存活
t.getName()--->获取线程的名字
t.setName()--->设置线程的名字
t.join()--->让主线程等待,子进程运行完
属性:
t.daemon--->值为True 定义某一个线程守护主线程
t.name--->线程的名
threading模块中提供了一些其他的方法:
threading.currentThread() 返回当前线程的变量
threading.activeCount() 返回正在运行的线程数量
4. 守护线程
使用t.daemon=True 定义某一个线程守护主线程 加在t.start()前
重点: 无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
线程中:运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕
,主线程才算运行完毕,此时守护线程也一并结束
进程中:只要主进程运行完毕,守护进程也一并j
5. 线程互斥锁(同进程一样)
互斥锁:把多个任务对共享数据的修改由并发变成串行,牺牲效率,提高数据安全
线程之间是共享同一个进程内的数据,而多个线程需要对数据进行处理时需要进行加锁处理
使用multiprocessing 模块中Lock
lock=Lock()
lock.acquire() ə'kwaɪə
线程中需要修改数据的代码
lock.release()
来控制,将并发改成串行,牺牲了运行效率,但保证了数据的安全
一般加在修改共享数据的那一小部分代码
6. GIL全局解释器锁(******)
1.GIL的定义:
GIL本质上就是一把互斥锁,相当于执行权限,每个进程内都会存在一把GIL,同一个进程内的多个线程必须抢到GIL才能使用cpython
解释器来执行自己的代码,即同一个进程下的多个线程无法实现并行,但可以实现并发
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
2.为何要有GIL
因为Cpython解释器的垃圾回收机制(不是线程安全)对线程是不安全(数据的不安全)
垃圾回收机制 (引用计数为0时python解释器会自动清除)
3.有了GIL,如何实现并发:
python并发为假的并发:
因为有了GIL解释器锁,为了解决垃圾回收数据不安全的问题,
导致同一个进程下的多线程不能并行但可以实现并发(只有一个可以运行)
多核优势:CUP计算效率的提升
计算密集型:应该使用多进程(多核优势,运行效率高)
IO密集型:应该使用多线程
ps:GIL 和自定义所Lock
GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,
比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据
运行时先要抢到GIL锁,运行解释器执行python代码,而后再使用自定义锁
4. 死锁与递归锁
死锁:
是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象
解决方式:
使用递归锁 from threading import Rlock
mutexA=mutexB=Rlock()---->链式赋值
可多次acquire(),每acquire一次计数加1,每release一次计数减1,只有当计数为0的时候,其他线程才可以使用
5. 信号量:Semaphore
from threading import Semaphore
import time,random
sm=Semaphore(5)-------->最大信号量
def task(name):
sm.acquire()
print('%s正在上厕所'%name)
time.sleep(random.randint(1,3))
sm.release()
if __name__ == '__main__':
for i in range(10):
t=Thread(target=task,args=('路人%s'%i,))
t.start()
6. Event事件
程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作需要用到event
from threading 中导入event
方法有:
event.isSet()--->返回线程的状态值
event.wait()--->如果event.isset()为False 表示阻塞
event.set()--->如果event状态值为True 表示所有阻塞将进入就绪状态
举例:
from threading import Thread,Event
import time
event=Event()
def ligth():
print('红灯亮着')
time.sleep(2)
event.set()
def car (name):
print('%s等绿灯亮'%name)
event.wait()
time.sleep(2)
print('%s正在通行'%name)
if __name__ == '__main__':
t=Thread(target=ligth)
t.start()
for i in range(10):
t=Thread(target=car,args=('车%s'%i,))
t.start()
7. 线程queue
导入import queue
三种方式:
q=queue.Queu---->先进先出e()
q=queue.LiFoQueue()--->后进先出
q=queue.PriorityQueue()---->按照优先级,数字越小优先级越高
方法:
q.get()
q.put()
第六 进程池\线程池:
定义::池的功能是限制启动进程或线程的数量,进程\线程池就是存放指定数量的进程\线程
何时用:当并发任务远远超过计算的承受能力,即无法一次性开启过多的进程数或线程数时
怎么用:
导入模块:
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
提交任务的二种方式:
同步调用:
提交完一个任务后,就在原地等待,等待任务完成拿到结果后,再执行下一行代码,
会导致任务是串行
异步调用:
提交完一个任务后,不在原地等待结果,直接执行下一行代码,会导致任务是并发执行
结果future对象会在任务运行完毕后自动传给回调函数future.add_done_callback(函数名)
该函数会在任务运行完毕后自动触发,然后接收一个参数future对象
--->提交完任务(绑定一个回调函数)后不原地等待,直接运行下一行代码,等到任务运行结束有返回值自动触发回调的函数的运行
程序运行的状态:
阻塞:IO阻塞
非阻塞:运行,就绪
计算密集型中使用:进程池
计算IO密集型时使用:线程池
实例:使用进程池编写一个下载网站的内容长度的程序:(异步提交任务的方式)
from concurrent.futures import ThreadPoolExecutor ,ProcessPoo lExecutor
import os ,time,request
def get(url):
print('%s获取%s'%(os.getpid(),url))
response=requests.get(url)
if response.status_code==200:
res=response.text
else:
res='下载失败'
return res
def parse(future):
res=future.result()
print('%s解析结果为%s'%(os.getip(),len(res)))
if __name__=='__main__':
urls=[]
t=ThreadPoolExecutor(4)
for url in urls:
future=t.submit(get,url)
future.add__done__callback(parse)
t.shutdown(wait=True)
第七 协程:
定义:
单线程实现并发 (切换+状态保存)
协程的概念是程序员意淫出来的,对操作系统而言只有进程和线程而言(操作系统最小量化的单位为线程)
(在单线程下实现多个任务间遇到IO就切换)就可以降低单线程的IO时间,从而最大限度的提升单线程的效率
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率
2.为何用:
单线程下实现多个任务间遇到IO时,用协程的方式可以提高线程的效率(yield 和next())
3.如何用:
固定模块格式:
from gevent import monkey;monkey.patch_all()------>
需要将from gevent import monkey;monkey.patch_all()放到文件的开头
------>以上代码是一个补丁用来识别time.sleep(),或者其他IO阻塞
from gevent import spawn
模块gevent介绍:
用法:
g1=spawn(fun1,args)---->创建一个协程对象
g2=spawn(fun2,args)
g1.join()------>等待g1结束
g2.join()----->等待g2结束
具体事例:单线程下并发多个客户端任务
服务端:
def server(ip,port,backlog=5):
server=socket(AF_INET,SOCK_STREAM)
server.bind((ip,port))
server.listen(backlog)
while True:
conn,client_add=server.accept()
print(client_add)
spawn(task,conn)
def task(conn):
while True:
try:
data=conn.recv(1024)
if len(data)==0:break
print(data)
conn.send(data.upper())
except ConnectionResetError:
break
# server('127.0.0.1',8080)
if __name__ == '__main__':
g=spawn(server,'127.0.0.1',8080)
g.start()
g.join()
客户端
from threading import Thread
def client(ip,port):
client=socket(AF_INET,SOCK_STREAM)
client.connect((ip,port))
count=0
while True:
msg=input('输入内容').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print(data.decode('utf-8')+'接收次数%s'%count)
count+=1
if __name__ == '__main__':
for i in range(100):
t=Thread(target=client,args=('127.0.0.1',8080))
t.start()