0. 前言
0.1 进程间通信的必要性
进程间空间独立,资源不共享,此时若需要在进程间数据传输,就需要用特定的手段进行数据通信。
0.2 常用的进程间通信方式
(1)管道(拓展:命名管道);(2)消息队列;(3)共享内存;(4)信号量;(5)套接字。
1. 管道通信(Pipe)
1.1 通信原理
在内存中开辟管道空间,生成管道操作对象,多个进程使用同一个管道对象进行读写,从而实现进程间的通信。
1.2 实现方法
from multiprocessing import Pipe
fd1,fd2 = Pipe(duplex = True)
功能:创建管道
参数:默认表示双向管道
如果duplex为False,表示单向管道
返回值:表示管道两端的读写对象
如果是双向管道均可读写
如果是单向管道,则fd1只读,fd2只写
fd.recv()
功能:从管道获取内容
返回值:从管道获取到的数据
fd.send(data)
功能: 向管道写入内容
参数: 要写入的数据
1.3 示例代码
"""
pipe.py 管道操作
注意:
1. multiprocessing中提供的通信只用于亲缘关系进程间通信
2. 管道在父进程中创建,子进程从父进程中获取管道对象
"""
from multiprocessing import Process, Pipe
# app1可以使用app2提供的信息登录
def app1(fd1):
print("启动app1,请求app2授权登录")
# 写管道
fd1.send("app1 可以用你的账号登录吗?")
# 阻塞等待读取管道
data = fd1.recv()
# 如果管道中有数据(fd2发送了数据)
if data:
print("登录成功:", data)
def app2(fd2):
print("启动app2")
request = fd2.recv() # 阻塞等待读取管道
if request:
print(request)
fd2.send(("授权登录!")) # 发送python数据类型
if __name__ == '__main__':
fd1, fd2 = Pipe()
p1 = Process(target=app1, args=(fd1,))
p2 = Process(target=app2, args=(fd2,))
p2.start()
p1.start()
p1.join()
p2.join()
1.4 命名管道
相应文章:
(1)Python进程间通信之命名管道
注:FIFO – First Input First Output 先入先出
(2)Python进程间通信之命名管道(Windows)
代码示例
# server
import win32file
import win32pipe
PIPE_NAME = r'\\.\pipe\test_pipe'
PIPE_BUFFER_SIZE = 65535
while True:
named_pipe = win32pipe.CreateNamedPipe(PIPE_NAME,
win32pipe.PIPE_ACCESS_DUPLEX,
win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_WAIT | win32pipe.PIPE_READMODE_MESSAGE,
win32pipe.PIPE_UNLIMITED_INSTANCES,
PIPE_BUFFER_SIZE,
PIPE_BUFFER_SIZE, 500, None)
try:
while True:
try:
win32pipe.ConnectNamedPipe(named_pipe, None)
data = win32file.ReadFile(named_pipe, PIPE_BUFFER_SIZE, None)
if data is None or len(data) < 2:
continue
print 'receive msg:', data
except BaseException as e:
print "exception:", e
break
finally:
try:
win32pipe.DisconnectNamedPipe(named_pipe)
except:
pass
# client
import win32pipe, win32file
import time
PIPE_NAME = r'\\.\pipe\test_pipe'
file_handle = win32file.CreateFile(PIPE_NAME,
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
win32file.FILE_SHARE_WRITE, None,
win32file.OPEN_EXISTING, 0, None)
try:
for i in range(1, 11):
msg = str(i)
print 'send msg:', msg
win32file.WriteFile(file_handle, msg)
time.sleep(1)
finally:
try:
win32file.CloseHandle(file_handle)
except:
pass
2. 消息队列(Queue)
2.1 通信原理
在内存中建立队列模型,进程通过队列将消息存入,或者从队列中取出消息,从而完成进程间的通信。
2.2 实现方法
from multiprocessing import Queue
q = Queue(maxsize=3)
功能:创建队列对象
参数:最多存放消息个数
返回值:队列对象
q.put(data, [block, timeout])
功能:向队列存入消息
参数:
data -- 要存入的内容
block -- 设置是否阻塞 False为非阻塞
timeout -- 超时检测
返回值:返回获取到的内容
q.full() -- 判断队列是否为满
q.empty() -- 判断队列是否为空
q.qsize() -- 获取队列中消息个数
q.close() -- 关闭队列
2.3 示例代码
"""
消息队列演示
注意: 消息存入与取出的关系为: 先入先出
"""
from multiprocessing import Queue, Process
from random import randint
def request(q):
for i in range(10):
x = randint(1, 100)
y = randint(1, 100)
print('write: ', (x, y))
q.put((x, y)) # 写消息队列
def handle(q):
read_data = []
while not q.empty():
data = q.get() # 读消息队列
print('read: ', (data[0], data[1]))
print("x + y= ", data[0] + data[1])
read_data.append(data)
print('read_data---', read_data)
if __name__ == '__main__':
# 创建队列
q = Queue(15) # 最多存储15个消息(该数≥request(q)中的range(10),可以保证通信顺畅,不丢失数据)
p1 = Process(target=request, args=(q,))
p2 = Process(target=handle, args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
q.close()
3. 共享内存
3.1 通信原理
在内存中开辟一块空间,进程可以写入内容和读取内容,从而完成通信,但是每次写入内容会覆盖之前内容。
3.2 实现方法
from multiprocessing import Value, Array
obj = Value(ctype,data)
功能:开辟共享内存
参数:ctype 表示共享内存空间类型 'i' 'f' 'c'
data 共享内存空间初始数据
返回值:共享内存对象
obj.value 对该属性的修改查看即对共享内存读写
obj = Array(ctype,data)
功能: 开辟共享内存空间
参数: ctype 表示共享内存数据类型
data 整数则表示开辟空间的大小,其他数据类型表示开辟空间存放的初始化数据
返回值:共享内存对象
Array共享内存读写: 通过遍历obj可以得到每个值,直接可以通过索引序号修改任意值。
* 可以使用obj.value直接打印共享内存中的字节串
3.3 示例代码
"""
value.py 开辟共享内存空间
注意: 共享内存中只能有一个值
"""
from multiprocessing import Process, Value
import time
import random
def man(money):
for i in range(30):
time.sleep(0.2)
# 修改共享内存
money.value += random.randint(1, 1000)
def girl(money):
for i in range(30):
time.sleep(0.15)
# 修改共享内存
money.value -= random.randint(100, 800)
if __name__ == '__main__':
# 创建共享内存
money = Value('i', 5000)
p1 = Process(target=man, args=(money,))
p2 = Process(target=girl, args=(money,))
p1.start()
p2.start()
p1.join()
p2.join()
print("一个月余额:", money.value) # 读取共享内存
"""
array.py
共享内存中存放列表,字节串
"""
from multiprocessing import Process, Array
def fun(msg):
# 共享内存对象可以迭代
for i in msg:
print(i)
msg[0] = b'y' # 修改共享内存
if __name__ == '__main__':
# 创建共享内存
# shm = Array('i',[1,2,3,4])
# shm = Array('i',4) # 开辟4个整型的列表空间
msg = Array('c', b'hello')
p = Process(target=fun, args=(msg, ))
p.start()
p.join()
for i in msg:
print(i)
print(msg.value) # b'yello' 整体打印字节串
4. 信号量(信号灯集)
4.1 通信原理
给定一个数量,对多个进程可见。多个进程都可以操作该数量增减,并根据数量值决定自己的行为。
4.2 实现方法
from multiprocessing import Semaphore
sem = Semaphore(num)
功能:创建信号量对象
参数:信号量的初始值
返回值:信号量对象
sem.acquire() -- 将信号量减1,当信号量为0时阻塞
sem.release() -- 将信号量加1
sem.get_value() -- 获取信号量数量
4.3 示例代码
"""
sem.py
信号量演示
注意:信号量可以当作是一种资源,执行任务时需要消耗信号量资源,
这样可以控制进程执行行为
"""
from multiprocessing import Process,Semaphore
from time import sleep
import os
# 任务函数(系统中最多能够同时运行3个该任务)
def handle(sem):
sem.acquire() # 消耗一个信号量
print("%s 执行任务" % os.getpid())
sleep(2)
print("%s拯救了宇宙" % os.getpid())
sem.release() # 增加一个信号量
if __name__ == '__main__':
# 创建信号量资源
sem = Semaphore(3) # 信号量资源有3个
jobs = []
for i in range(10):
p = Process(target=handle, args=(sem,))
jobs.append(p)
p.start()
for i in jobs:
i.join()
print(sem.get_value()) # 3
5. socket套接字
5.1 套接字介绍
-
套接字 : 实现网络编程进行数据传输的一种技术手段
-
Python实现套接字编程:import socket
-
套接字分类
流式套接字(SOCK_STREAM): 以字节流方式传输数据,实现tcp网络传输方案。(面向连接–tcp协议–可靠的–流式套接字)
数据报套接字(SOCK_DGRAM):以数据报形式传输数据,实现udp网络传输方案。(无连接–udp协议–不可靠–数据报套接字)
5.2 tcp套接字编程
5.2.1 服务端流程
5.2.2 示例代码
# 服务器端
from socket import *
from time import ctime
HOST = ''
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST,PORT)
tcpSerSock = socket(AF_INET,SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5)
while True:
print('waiting for connection...')
tcpCliSock, addr = tcpSerSock.accept()
print('...connnecting from:', addr)
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
#tcpCliSock.send('[%s] %s' %(bytes(ctime(),'utf-8'),data))
tcpCliSock.send(('[%s] %s' % (ctime(), data)).encode())
tcpCliSock.close()
tcpSerSock.close()
5.2.3 具体流程介绍
- 创建套接字
sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)
功能: 创建套接字
参数: socket_family 网络地址类型 AF_INET表示ipv4
socket_type 套接字类型 SOCK_STREAM(流式) SOCK_DGRAM(数据报)
proto 通常为0 选择子协议
返回值:套接字对象
- 绑定地址
本地地址 : ‘localhost’ , ‘127.0.0.1’
网络地址 : ‘172.40.91.185’
自动获取地址: ‘0.0.0.0’
sockfd.bind(addr)
功能: 绑定本机网络地址
参数: 二元元组 (ip,port) ('0.0.0.0',8888)
- 设置监听
sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小
- 等待处理客户端连接请求
connfd,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求
返回值: connfd 客户端连接套接字
addr 连接的客户端地址
- 消息收发
data = connfd.recv(buffersize)
功能 : 接受客户端消息
参数 :每次最多接收消息的大小
返回值: 接收到的内容
n = connfd.send(data)
功能 : 发送消息
参数 :要发送的内容 bytes格式
返回值: 发送的字节数
- 关闭套接字
sockfd.close()
功能:关闭套接字
5.2.4 客户端流程
5.2.5 示例代码
# 客户端
from socket import *
HOST = '127.0.0.1' # or 'localhost'
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST,PORT)
tcpCliSock = socket(AF_INET,SOCK_STREAM)
tcpCliSock.connect(ADDR)
while True:
data1 = input('>')
#data = str(data)
if not data1:
break
tcpCliSock.send(data1.encode())
data1 = tcpCliSock.recv(BUFSIZ)
if not data1:
break
print(data1.decode('utf-8'))
tcpCliSock.close()
5.2.6 具体流程介绍
- 创建套接字
注意:只有相同类型的套接字才能进行通信
- 请求连接
sockfd.connect(server_addr)
功能:连接服务器
参数:元组 服务器地址
- 收发消息
注意: 防止两端都阻塞,recv send要配合
- 关闭套接字
5.2.7 tcp 套接字数据传输特点
- tcp连接中当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
- tcp连接中如果一端已经不存在,仍然试图通过send发送则会产生BrokenPipeError
- 一个监听套接字可以同时连接多个客户端,也能够重复被连接
5.2.8 网络收发缓冲区
- 网络缓冲区有效的协调了消息的收发速度
- send和recv实际是向缓冲区发送接收消息,当缓冲区不为空recv就不会阻塞。
5.2.9 tcp粘包
原因:tcp以字节流方式传输,没有消息边界。多次发送的消息被一次接收,此时就会形成粘包。
影响:如果每次发送内容是一个独立的含义,需要接收端独立解析此时粘包会有影响。
处理方法
- 人为的添加消息边界
- 控制发送速度
5.2.10 Socket tcp 文件传输代码示例
server.py
import socket
import struct
import json
import hashlib
server = socket.socket()
server.bind(("127.0.0.1", 7000))
server.listen(5)
while 1:
request, address = server.accept()
print('接收链接', address)
while 1:
cmd = request.recv(4)
if cmd.decode('utf8') == 'exit':
break
json_len = struct.unpack('i', cmd)[0]
json_data = json.loads(request.recv(json_len))
action = json_data['action']
filename = json_data['filename']
filelen = json_data['filelen']
received_len = 0
print(json_data)
md5 = hashlib.md5()
with open('data/'+filename, 'wb') as f:
while received_len < filelen:
if filelen - received_len >= 1024:
readlen = 1024
else:
readlen = filelen - received_len
filedata = request.recv(readlen)
f.write(filedata)
md5.update(filedata)
received_len += len(filedata)
print(f'已完成{received_len}/总大小{filelen}')
print('传输完成')
recv_md5 = request.recv(32)
if recv_md5.decode('utf-8') == md5.hexdigest():
print('文件正常传输')
else:
print('文件异常')
request.close()
print('断开链接', address)
client.py
import socket
import struct
import json
import os
import hashlib
client = socket.socket()
client.connect(('127.0.0.1', 7000))
while 1:
cmd = input(">>>")
if cmd == 'exit':
client.send(cmd.encode('utf8'))
break
action, filename = cmd.split()
command = {'action':action,
'filename':os.path.basename(filename),
'filelen':os.path.getsize(filename),}
json_data = json.dumps(command).encode('utf8')
header = struct.pack('i', len(json_data))
client.send(header)
client.send(json_data)
md5 = hashlib.md5()
with open(filename, 'rb') as f:
for line in f:
client.send(line)
md5.update(line)
client.send(md5.hexdigest().encode('utf-8'))
print("发送完成")
client.close()
5.2.11 Socket tcp 大型数组传输代码示例(思路仅供参考,不推荐,因为接收速度太慢)
server.py
import time
from socket import *
import struct
import json
import hashlib
import numpy as np
server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 7000))
server.listen(5)
while 1:
client, address = server.accept()
print('接收链接:', address)
cmd0 = client.recv(1) # 是否关闭服务端
# 关闭服务端和客户端
# print('cmd0---', cmd0)
if cmd0.decode('utf-8') == '1':
client.close()
server.close()
exit(0)
else:
cmd1 = client.recv(4)
# print('cmd1---', cmd1)
json_len = struct.unpack('i', cmd1)[0]
json_data = json.loads(client.recv(json_len).decode('utf-8'))
print('json_data---', json_data['array_len'])
array_len = json_data['array_len']
array_type = json_data['array_dtype']
print(np.dtype(array_type))
received_len = 0
received_array = b''
result = []
while received_len < array_len:
if array_len - received_len >= 8192:
r_len = 8192
else:
r_len = array_len - received_len
# r_time_0 = time.time()
received_data = client.recv(r_len)
# r_time_1 = time.time()
# print('r_time: ', r_time_1-r_time_0)
# a_time_0 = time.time()
# result.append(received_data)
received_array += received_data # 该步慢了容易出问题~接收得快,但拼接慢
# a_time_1 = time.time()
# print('add time: ', a_time_1-a_time_0)
received_len += len(received_data) # 这一部很关键,说明recv()的不一定等于r_len
# percent = received_len/array_len*100
# print(percent)
# received_array = ''.join(str(result))
print(len(received_array))
# received_array = bytes(received_array)
# print('len(received_array)---', len(received_array))
# print('received_array---', received_array)
print('received_len---', received_len)
print('array len ', array_len)
# array = np.fromstring(received_array, dtype=np.float64)
array = np.fromstring(received_array, dtype=np.dtype(array_type))
# array = received_array
shape = json_data['array_shape']
print(array.size)
array = array.reshape(shape)
# print(array)
client.close()
print('断开链接', address)
client.py
from socket import *
import struct
import json
import os
import numpy as np
import argparse
import sys
def manage_client(cmd, array):
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 7000))
# 发送是否退出服务端和客户端的指令
client.send(cmd[0].encode('utf-8'))
wanted_class = cmd[1]
# 将数组统一为np.float64,方便计算数组占的内存大小和后续检测
# array = array.astype(np.float64)
# array = array.squeeze()
array_shape = array.shape
# array_len = array_shape[0] * array_shape[1] * 8
# print(array_len)
b_array = array.tostring()
command = {'wanted_class': wanted_class,
'array_shape': array_shape,
'array_dtype': array.dtype.name,
# 'array_len': array_len,
'array_len': len(b_array),
}
json_data = json.dumps(command).encode('utf-8')
header = struct.pack('i', len(json_data))
print('len(json_data)', len(json_data))
print('header---', header)
client.send(header)
client.send(json_data)
# 传输数组
client.send(b_array)
print("发送完成")
client.close()
if __name__ == '__main__':
# array = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float64)
array = np.load('npys/demo.npy')
print(array.size)
# array = np.ones((100, 200), np.int32)
# 3种情况
# 1. 关闭服务端、客户端 python client.py -e 1
# 2. 执行某操作 python client.py -e 0 -c dog
import sys
sys.argv = ['client.py', '-e', '0', '-c', 'dog']
parser = argparse.ArgumentParser()
parser.add_argument("-e", "--exit", default="1", help="ask the server to exit, 0--not exit, 1--exit")
parser.add_argument("-c", "--class_p", default=None, help="wanted class to predict")
args = parser.parse_args()
cmd = [args.exit, args.class_p]
manage_client(cmd, array)
涉及新知识点汇总:
1. struct
原理: 将一组简单数据进行打包,转换为bytes格式发送。或者将一组bytes格式数据,进行解析。
接口使用:
Struct(fmt)
功能: 生成结构化对象
参数:fmt 定制的数据结构
import struct
struct.pack(v1,v2,v3....)
功能: 将一组数据按照指定格式打包转换为bytes
参数:要打包的数据
返回值: bytes字节串
struct.unpack(bytes_data)
功能: 将bytes字节串按照指定的格式解析
参数: 要解析的字节串
返回值: 解析后的内容
struct.pack(fmt,v1,v2,v3...)
struct.unpack(fmt,bytes_data)
2. tostring,tobytes,fromstring, frombuffer
# 把数组array转为bytes二进制类型,通过len(new_array)可以获得new_array的占内存大小;
new_array = array.tostring()
tobytes() --同tostring()
# 字符串转为bytes:
a = 'hello world!'
bytes_a = b'hello world!'
# 由bytes转为数组,需注明数组元素类型
result = numpy.fromstring(new_array, dtype = numpy.int8)
frombuffer – 顺藤摸瓜-mnist数据集的补充,二进制buffer读取,numpy的frombuffer方法等
3. json
# json.dumps()用于将字典形式的数据转化为字符串,json.loads()用于将字符串形式的数据转化为字典,
a = {}
# 字典形式数据 --> 字符串
json_data = json.dumps(a)
# 字符串形式数据 --> 字典
dict_data = json.loads(json_data)
4. hashlib
转载:https://blog.csdn.net/syousetu/article/details/107065140
import hashlib #导入hashlib模块
md = hashlib.md5() # 获取一个md5加密算法对象
md.update('how to use md5 in hashlib?'.encode('utf-8')) # 制定需要加密的字符串
print(md.hexdigest()) # 获取加密后的16进制字符串
5. sys.argv
在xxx.py文件中使用argparse.ArgumentParser(),通过终端python xxx.py -a yyy -b zzz运行,
可替换为:在xxx.py文件中使用argparse.ArgumentParser()之前添加sys.argv = ['xxx.py', '-a', 'yyy', '-b', 'zzz'],然后pycharm直接run即可
5.3 UDP套接字编程
5.3.1 服务端流程
5.3.2 示例代码
"""
udp_server.py udp服务端
"""
from socket import *
# 创建UDP套接字
sockfd = socket(AF_INET,SOCK_DGRAM)
# 绑定地址
server_addr = ('0.0.0.0', 8888)
sockfd.bind(server_addr)
# 循环收发
while True:
data,addr = sockfd.recvfrom(1024)
print("收到的消息:", data.decode())
sockfd.sendto(b'Thanks', addr)
# 关闭套接字
sockfd.close()
5.3.3 具体流程介绍
- 创建数据报套接字
sockfd = socket(AF_INET,SOCK_DGRAM)
- 绑定地址
sockfd.bind(addr)
- 消息收发
data,addr = sockfd.recvfrom(buffersize)
功能: 接收UDP消息
参数: 每次最多接收多少字节
返回值: data 接收到的内容
addr 消息发送方地址
n = sockfd.sendto(data,addr)
功能: 发送UDP消息
参数: data 发送的内容 bytes格式
addr 目标地址
返回值:发送的字节数
- 关闭套接字
sockfd.close()
5.3.4 客户端流程
5.3.5 示例代码
"""
udp_client.py udp客户端
"""
from socket import *
# 服务器地址
ADDR = ('127.0.0.1',8888)
# 创建套接字
sockfd = socket(AF_INET,SOCK_DGRAM)
# 循环发送消息
while True:
data = input("Msg:")
if not data:
break
sockfd.sendto(data.encode(),ADDR)
msg,addr = sockfd.recvfrom(1024)
print("From server:",msg.decode())
sockfd.close()
- 创建套接字
- 收发消息
- 关闭套接字
总结 :tcp套接字和udp套接字编程区别
- 流式套接字是以字节流方式传输数据,数据报套接字以数据报形式传输
- tcp套接字以字节流方式传输,会有粘包,udp套接字以数据报形式,有消息边界不会粘包
- tcp套接字保证消息的完整性,udp套接字则不能
- tcp套接字依赖listen accept建立连接才能收发消息,udp套接字则不需要
- tcp套接字使用send,recv收发消息,udp套接字使用sendto,recvfrom
5.4 socket套接字属性
示例代码
"""
套接字属性介绍
"""
from socket import *
# 创建套接字对象
s = socket(AF_INET,SOCK_STREAM)
# 设置端口立即重用
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('172.40.91.143',8888))
s.listen(3)
c,addr = s.accept()
print(s.type) # 套接字类型
print(s.family) # 地址类型
print(s.getsockname()) # 绑定的地址
print(s.fileno()) # 文件描述符
print(c.getpeername()) # 获取连接端的地址
c.recv(1024)
【1】 sockfd.type 套接字类型
【2】 sockfd.family 套接字地址类型
【3】 sockfd.getsockname() 获取套接字绑定地址
【4】 sockfd.fileno() 获取套接字的文件描述符
【5】 sockfd.getpeername() 获取连接套接字客户端地址
【6】 sockfd.setsockopt(level,option,value)
功能:设置套接字选项
参数: level 选项类别 SOL_SOCKET
option 具体选项内容
value 选项值