socket实现多机协同
三个通信任务,从易到难
从使用socket建立单节点server和client连接,到收发文件,到多机协同完成计算任务。
1、双节点之间互发文字消息
-
基本要求
1、启动时server端创建侦听器,等待client连接。
2、完成连接后,双方进入双向通信状态,可以互发文字消息。
3、任何一方发出“QUIT”(大小写不敏感)就终止通信,双方终止程序运行。 -
思路
使用socket编程,首先引入socket包
import socket
在server端,首先创建套接字,绑定ip地址和想要监听的端口。这里在单机进行测试,指定ip为本机,端口8000
HOST = '127.0.0.1'
PORT = 8000
ADDR = (HOST, PORT)
创建套接字,指定发送协议,下面这样写法指定使用TCP/IP协议进行消息传输
# 创建套接字socket. AF_INET--使用ipv4地址族 SOCK_STREAM--用流式套接字
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
绑定ip、监听端口
# 绑定ip地址、监听端口
serversocket.bind(ADDR)
保持端口处于监听状态
# 开始tcp监听.希望在队列中累积多达 5 个(通常的最大值)连接请求后再拒绝外部连接
serversocket.listen(5)
一旦有节点连接到这个地址,server端就会收到连接请求
# 获得连接请求,需要知道连接上来主机的地址和端口
client, client_port = serversocket.accept()
上面已经建立了双向通信,下面完成第2项任务,互发文字。
互发消息这里就实现为,一方发送消息后,就等待另一方发送,必须轮流发送消息。
如果消息内容包含QUIT(不区分大小写),就终止通信。
while True:
# 发送服务端的数据,手动输入数据
msg = input("请输入服务端要发送的数据:")
if 'QUIT' in msg.upper():
print("由于发送了QUIT消息,服务端关闭连接...")
serversocket.close()
break
else:
client.sendall(msg.encode())
print("服务端发送了一条数据...正在等待客户端发送数据...")
# 接受数据
data = client.recv(1024)
if 'QUIT' in data.decode().upper():
print("由于收到了QUIT消息,服务端关闭连接...")
serversocket.close()
# print("data", type(data))
print("接收到来自客户端的信息:", data.decode())
首先发送,但发送之前要对字符串进行编码,使用encode()方法;接收到消息也是,如果想要str类型,就必须先解码,使用decode()方法:
# 给client发送,调用client,不是serversocket的方法
# sendall() 方法会确保将所有数据发送完毕,而不需要多次调用。
# sendall() 方法只接受bytes类型,因此需要对字符串编码转化
client.sendall(msg.encode())
以上是server端。
接收端类似,如下代码:
import socket
HOST = '127.0.0.1'
PORT = 8000
ADDR = (HOST, PORT)
def start_client():
# 创建套接字
clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 打开tcp连接
clientsocket.connect(ADDR)
print("客户端绑定了ip和端口...")
while True:
# 如果连接还在保持状态,发送请求数据
msg = input("请输入客户端要发送的数据:")
if 'QUIT' in msg.upper():
print("客户端发送了QUIT消息,客户端关闭连接...")
clientsocket.close()
break
else:
clientsocket.sendall(msg.encode())
print("客户端发送了数据...正在等待服务端发送数据...")
# 接收服务端发回来的数据.recv缓冲区大小
data = clientsocket.recv(1024)
print("服务端返回的数据是:", data.decode())
print("连接已断开,结束连接...")
clientsocket.close()
if __name__ == '__main__':
start_client()
2、建立通信并发送python代码,获取他机执行结果
-
基本功能
1、建立两节点的通信。
2、从节点1向节点2发送一个Python语言编写的源程序A,节点2执行程序A,并向节点1返回计算结果。 -
要求
1、程序A可在节点2上独立完成运行,无需其它条件支持。 -
实现
在任务1的基础上,client考虑如何发送整个文件,server考虑如何接收整个文件,并把文件保存在本地执行,最后返回本地执行结果给server端,完成通信任务。
client发送文件,按行发送。
filename = "hello.py"
with open(filename, 'rb') as f:
print("成功读入文件:%s" % filename)
for i in f:
print(i)
client.sendall(i)
print("内容已发送:%s" % i)
print("文件 %s 已发送" % filename)
server端接收消息,并写到自己本地的文件
filename = "recv.py"
# 这个地方要收到完整的文件代码,后面才能正常执行返回正确的结果!!
with open(filename, 'w', encoding='utf-8') as f:
data = conn.recv(1024)
f.write(data.decode())
f.close()
print("服务端接收文件完成!")
然后server本地执行,并把返回值发送给client。
res = subprocess.Popen('python ' + filename, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = res.stdout.read()
print("在服务端执行结果是:", result.decode())
# 注意是conn. 不是server.
conn.sendall(result)
print("执行结果已经成功返回!")
3、多机通信
1. 求集合的最大值
要求:集合以文件形式给出,由控制节点提供。
这里设有一个控制节点,若干个计算节点。
计算思路是,讲集合文件发送给每一个计算节点,每一个计算节点根据自己的节点编号,计算集合中相应分片中的最大值,讲自己节点的最大值返回给控制节点。最后控制节点从若干个局部最大值中求一个全局最大值。思路和实现类似前面的任务2,主要考虑如何分片。
假设:
ds = 数据集合
n = len(ds)
m = 计算节点数
k = 当前节点编号(初始化的时候得到不同的值)
# 分片思路,在实现时,需要再关注一下边界值
max = ds[n//m*k]
for i in range(n//m*k, n//m*(k+1)):
if ds[i] > max:
max = ds[i]
就是按照节点编号、计算节点总数和集合总大小划分并分配(Map)。
server给每一个节点发送集合文件代码如下:
# 1. 发送ds文件
filename = "ds.txt"
# 给每个节点都发送ds文件
file_content = ""
for compute_socket in compute_sockets:
# file_size = os.popen(filename.decode()).read()
# compute_socket.sendall(str(file_size).encode())
# 发送文件给计算节点
with open(filename, 'r') as file:
for i in file:
file_content += i
compute_socket.sendall(str(len(file_content)).encode())
print("发送file_size:", len(file_content))
compute_socket.sendall(file_content.encode())
# 这里清空 file_content 否则后面会叠加发送
file_content = ""
print("内容已发送:%s" % file_content)
print("文件%s已发送给全部节点" % filename)
client接收集合文件,并保存到本地写如文件,然后从文件中读到ds数组中,代码如下:
# 接下来接收ds文件,保存到文件
recv_filename = "recv" + str(myid) + ".txt"
recv_data_size = int(client.recv(1024).decode())
current_file_size = 0
print("recv_data_size:", recv_data_size)
with open(recv_filename, 'w+', encoding='utf-8') as f:
while current_file_size < recv_data_size:
data = client.recv(1024).decode()
print("接收到data:", data)
current_file_size += len(data)
data = data.replace("\r\n", "\n")
print("对data去掉换行,加上换行:", data)
print("current_file_size:", current_file_size)
f.write(data)
f.close()
print("服务端接收文件完成!")
global ds
# 如果是用逗号分隔的一行数字组成的集合,按行读,再存到ds
with open(recv_filename, 'r') as f:
for line in f:
if line != '\n':
ds.append(int(line.strip()))
f.close()
print("ds:", ds)
print("Task1...计算全局最大值")
res = client.sendall(str(max_(ds, len(ds), m, myid)).encode())
print("节点编号是%s的节点已发送局部最大值是%d" % (data_id, max_(ds, len(ds), m, myid)))
client分片计算局部最大值:
def max_(ds, n, m, myid):
'''
返回ds集合该分片的最大值
:param ds: 集合
:param n: 集合长度
:param m: 总计算节点数
:param myid: 当前节点编号
'''
start = n // m * (myid - 1)
print("Task1全局最大值start:", start)
end = n // m * myid
# 确保最后一个节点计算剩余的所有元素
if myid == m:
end = n - 1
print("Task1全局最大值end:", end)
maximum = ds[start] # 用于记录当前分片中的最大值
for i in range(start, end):
if ds[i] > maximum:
maximum = ds[i]
return maximum
server接收各个局部最大值,归约(Reduce)得到全局最大值:
# 接收计算节点返回的结果
max_results = []
for compute_socket in compute_sockets:
result = compute_socket.recv(1024).decode()
max_results.append(int(result))
print("max_results:", max_results)
global_max = max(max_results)
print('最终的集合最大值计算结果:', global_max)
2. 求2~max范围内质数的个数
要求:测定加速比
加速比(Speedup)是用于衡量并行计算的性能提升程度的指标。它定义为并行计算所花费的时间与串行计算所花费的时间之比。加速比可以表示为以下公式:
S
=
T
serial
T
parallel
S = \frac{T_{\text{serial}}}{T_{\text{parallel}}}
S=TparallelTserial
T
serial
T_{\text{serial}}
Tserial表示串行计算所花费的时间,
T
parallel
T_{\text{parallel}}
Tparallel是并行计算所花费的时间。
在本次计算任务中,只需要在每个计算节点一并返回串行计算时间,把所有计算节点的串行时间求和作为串行计算花费的时间;并行计算花费时间是从控制节点发送max到计算完全部质数个数的时间。
控制节点:
# 2. 下面发送质数prime_max
prime_max = input("请输入prime_max:")
prime_results = []
count_prime = 0
start_time = time.time()
for compute_socket in compute_sockets:
compute_socket.sendall(prime_max.encode())
print("已发送prime_max...")
for compute_socket in compute_sockets:
prime_result = compute_socket.recv(1024).decode()
print("receive partial prime_max=",prime_result)
prime_results.append(int(prime_result))
count_prime += int(prime_result) # 或者直接用sum函数
end_time = time.time()
print('最终统计的质数个数:', count_prime)
# 2. 计算加速比
parallel_time = end_time - start_time
for compute_socket in compute_sockets:
serial_time = compute_socket.recv(1024).decode()
serial_times.append(float(serial_time))
serial_sum = sum(serial_times)
print('最终串行运行时间是%.6f秒' % float(serial_sum))
print('最终并行运行时间是%.6f秒:' % float(parallel_time))
if parallel_time <= 0:
print("并行时间几乎为零...不计算加速比")
else:
print('最终统计的加速比是%.6f:' % float(serial_sum / parallel_time))
计算节点:
def is_prime(num):
if num <= 1:
return False
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
return False
return True
def count_primes(prime, m, k):
count = 0
# 首先分片
start = prime // m * (k - 1)
print("Task2质数个数start:", start)
end = prime // m * k
# 确保最后一个节点计算剩余的所有元素
if k == m:
end = prime
print("Task2质数个数end:", end)
for num in range(start, end + 1):
if is_prime(num):
count += 1
return count
3. 求集合中与最大值互质的次大值
计算过程复杂度
数据依赖性
映射与归约的轮次
加速比
控制节点:
# 3. 计算与最大值互质的次大值
prime_second_results = []
for compute_socket in compute_sockets:
compute_socket.sendall(str(global_max).encode()) # 全局最大值
print("已发送全局最大值...")
for compute_socket in compute_sockets:
prime_second_result = compute_socket.recv(1024).decode()
prime_second_results.append(int(prime_second_result))
print("prime_second_results:", prime_second_results)
prime_second_result = max(prime_second_results)
print('最终的集合最大值计算结果:', prime_second_result)
计算节点:
# 3. 接收全局最大值,计算和他互质的次大值
global_max = int(client.recv(1024).decode())
print("Task3...")
print("接收到全局最大值:", global_max)
prime_second_result = max_coprime(ds, len(ds), m, myid, global_max)
client.sendall(str(prime_second_result).encode())
print("已发送task3结果:", prime_second_result)
# 判断两数是否互质
def is_coprime(a, b):
return math.gcd(a, b) == 1
# ds 表示数据集, m:总节点数, k表示当前节点编号
def max_coprime(ds, n, m, myid, global_max):
start = n // m * (myid - 1)
print("Task3互质的次大值start:", start)
end = n // m * myid
# 确保最后一个节点计算剩余的所有元素
if myid == m:
end = n - 1
print("Task3互质的次大值end:", end)
maximum = ds[start] # 用于记录当前分片中的最大值
for i in range(start, end):
if ds[i] > maximum and is_coprime(ds[i], global_max):
maximum = ds[i]
if maximum >= 2:
return maximum
else:
return 1
# 判断两数是否互质
def is_coprime(a, b):
return math.gcd(a, b) == 1
# ds 表示数据集, m:总节点数, k表示当前节点编号
def max_coprime(ds, n, m, myid, global_max):
start = n // m * (myid - 1)
print("Task3互质的次大值start:", start)
end = n // m * myid
# 确保最后一个节点计算剩余的所有元素
if myid == m:
end = n - 1
print("Task3互质的次大值end:", end)
maximum = ds[start] # 用于记录当前分片中的最大值
for i in range(start, end):
if ds[i] > maximum and is_coprime(ds[i], global_max):
maximum = ds[i]
if maximum >= 2:
return maximum
else:
return 1
调试过程
在一台电脑上,可以模拟四台机器协同工作。只需要开多个终端多次运行compute_node.py即可。
在多台电脑上,注意修改控制节点的ip地址(HOST)。最好关闭控制节点和所有计算节点的防火墙,ping通就可以在各个节点开始运行了。首先运行控制节点打开监听,再连接上各个计算节点就可以。
运行结果
控制节点:
计算节点(3个):
节点1:
节点2:
节点3:
问题总结
1. ds文件数据量较大时,无法全部发送、接收。
由于缓冲区大小有限,且网络不稳定造成延迟,接收端收到ds集合数据时可能已经错过本次接收,把数据放到了下次接收的内容中,导致错误。
解决思路:让接收端保持接收,直到接收到的数据大小等于发送的数据大小。
这里参考了博客socket–接受大数据。
完整代码
计算节点:
# -*- encoding:utf-8 -*-
import math
import socket
import sys
import time
HOST = 'localhost'
PORT = 8000
ADDR = (HOST, PORT)
ds = []
myid = -1
m = 3 # 需要改, 三个计算节点,一个控制节点
prime_max = 2
def client_send():
global prime_max
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 注意客户端是连接connect ,服务端是bind
client.connect(ADDR)
print("客户端连接了ip和端口")
# 接收节点编号
data_id = client.recv(1024).decode()
print("接收到自己的节点编号是: ", data_id)
myid = int(data_id)
# 设置超时时间为 10 秒
timeout = 30
client.settimeout(timeout)
# 接下来接收ds文件,保存到文件
recv_filename = "recv" + str(myid) + ".txt"
recv_data_size = int(client.recv(1024).decode())
current_file_size = 0
print("recv_data_size:", recv_data_size)
with open(recv_filename, 'w+', encoding='utf-8') as f:
while current_file_size < recv_data_size:
data = client.recv(1024).decode()
print("接收到data:", data)
current_file_size += len(data)
data = data.replace("\r\n", "\n")
print("对data去掉换行,加上换行:", data)
print("current_file_size:", current_file_size)
f.write(data)
f.close()
print("服务端接收文件完成!")
global ds
# 如果是用逗号分隔的一行数字组成的集合,按行读,再存到ds
with open(recv_filename, 'r') as f:
for line in f:
if line != '\n':
ds.append(int(line.strip()))
f.close()
print("ds:", ds)
print("Task1...计算全局最大值")
res = client.sendall(str(max_(ds, len(ds), m, myid)).encode())
print("节点编号是%s的节点已发送局部最大值是%d" % (data_id, max_(ds, len(ds), m, myid)))
print("等待接收prime_max...")
print("Task2...计算2~max范围内质数的个数,加速比")
# time.sleep(10)
# 接收prime最大值max
prime_max = int(client.recv(1024).decode()) # 没有算接收的时间,因为是自己手动输入的,延迟大
start_time = time.time()
# 计算质数的个数
prime_res = count_primes(prime_max, m, myid)
client.sendall(str(prime_res).encode())
end_time = time.time()
time.sleep(1)
serial_duration = end_time - start_time
client.sendall(str(serial_duration).encode())
print("节点编号是%s的节点已发送当前分片,其中的质数个数有%d个" % (data_id, prime_res))
print("节点编号是%s的节点计算质数消耗时间是%.6f秒" % (data_id, serial_duration))
# 3. 接收全局最大值,计算和他互质的次大值
global_max = int(client.recv(1024).decode())
print("Task3...计算与最大值互质的次大值")
print("接收到全局最大值:", global_max)
prime_second_result = max_coprime(ds, len(ds), m, myid, global_max)
client.sendall(str(prime_second_result).encode())
print("已发送task3结果:", prime_second_result)
client.close()
def max_(ds, n, m, myid):
'''
返回ds集合该分片的最大值
:param ds: 集合
:param n: 集合长度
:param m: 总计算节点数
:param myid: 当前节点编号
'''
start = n // m * (myid - 1)
print("Task1全局最大值start:", start)
end = n // m * myid
# 确保最后一个节点计算剩余的所有元素
if myid == m:
end = n - 1
print("Task1全局最大值end:", end)
maximum = ds[start] # 用于记录当前分片中的最大值
for i in range(start, end):
if ds[i] > maximum:
maximum = ds[i]
return maximum
def is_prime(num):
if num <= 1:
return False
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
return False
return True
def count_primes(prime, m, k):
count = 0
# 首先分片
start = prime // m * (k - 1)
print("Task2质数个数start:", start)
end = prime // m * k
# 确保最后一个节点计算剩余的所有元素
if k == m:
end = prime
print("Task2质数个数end:", end)
for num in range(start, end + 1):
if is_prime(num):
count += 1
return count
# 判断两数是否互质
def is_coprime(a, b):
return math.gcd(a, b) == 1
# ds 表示数据集, m:总节点数, k表示当前节点编号
def max_coprime(ds, n, m, myid, global_max):
start = n // m * (myid - 1)
print("Task3互质的次大值start:", start)
end = n // m * myid
# 确保最后一个节点计算剩余的所有元素
if myid == m:
end = n - 1
print("Task3互质的次大值end:", end)
maximum = ds[start] # 用于记录当前分片中的最大值
for i in range(start, end):
if ds[i] > maximum and is_coprime(ds[i], global_max):
maximum = ds[i]
if maximum >= 2:
return maximum
else:
return 1
if __name__ == '__main__':
client_send()
控制节点:
import os
import random
import socket
import time
HOST = '127.0.0.1'
PORT = 8000
ADDR = (HOST, PORT)
# 创建socket连接
control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
control_socket.bind(ADDR)
control_socket.listen(4) # 最多接受4个计算节点的连接
compute_sockets = [] # 存储计算节点的连接
node_ids = {} # 存储计算节点的编号
serial_times = []
m = 3 # 需要改一下
with open("ds.txt", "w") as f:
for _ in range(1000):
num = random.randint(10000, 4325375)
f.write(f"{num}\n")
print("等待计算节点连接...")
# 等待计算节点连接
for node_id in range(m):
compute_socket, address = control_socket.accept()
compute_sockets.append(compute_socket)
node_ids[compute_socket] = node_id + 1
print('计算节点', node_id + 1, '已连接:', address)
# 分配节点编号
for compute_socket in compute_sockets:
node_id = node_ids[compute_socket]
compute_socket.send(str(node_id).encode()) # 发送节点编号
# 1. 发送ds文件
filename = "ds.txt"
# 给每个节点都发送ds文件
file_content = ""
for compute_socket in compute_sockets:
# file_size = os.popen(filename.decode()).read()
# compute_socket.sendall(str(file_size).encode())
# 发送文件给计算节点
with open(filename, 'r') as file:
for i in file:
file_content += i
compute_socket.sendall(str(len(file_content)).encode())
print("发送file_size:", len(file_content))
compute_socket.sendall(file_content.encode())
print("内容已发送:%s" % file_content)
# 这里清空 file_content 否则后面会叠加发送
file_content = ""
print("文件%s已发送给全部节点" % filename)
# 接收计算节点返回的结果
max_results = []
for compute_socket in compute_sockets:
result = compute_socket.recv(1024).decode()
max_results.append(int(result))
print("max_results:", max_results)
global_max = max(max_results)
print('最终的集合最大值计算结果:', global_max)
# 2. 下面发送质数prime_max
prime_max = input("请输入prime_max:")
prime_results = []
count_prime = 0
start_time = time.time()
for compute_socket in compute_sockets:
compute_socket.sendall(prime_max.encode())
print("已发送prime_max...")
for compute_socket in compute_sockets:
prime_result = compute_socket.recv(1024).decode()
print("receive partial prime_max=",prime_result)
prime_results.append(int(prime_result))
count_prime += int(prime_result) # 或者直接用sum函数
end_time = time.time()
print('最终统计的质数个数:', count_prime)
# 2. 计算加速比
parallel_time = end_time - start_time
for compute_socket in compute_sockets:
serial_time = compute_socket.recv(1024).decode()
serial_times.append(float(serial_time))
serial_sum = sum(serial_times)
print('最终串行运行时间是%.6f秒' % float(serial_sum))
print('最终并行运行时间是%.6f秒:' % float(parallel_time))
if parallel_time <= 0:
print("并行时间几乎为零...不计算加速比")
else:
print('最终统计的加速比是%.6f:' % float(serial_sum / parallel_time))
# 3. 计算与最大值互质的次大值
prime_second_results = []
for compute_socket in compute_sockets:
compute_socket.sendall(str(global_max).encode()) # 全局最大值
print("已发送全局最大值...")
for compute_socket in compute_sockets:
prime_second_result = compute_socket.recv(1024).decode()
prime_second_results.append(int(prime_second_result))
print("prime_second_results:", prime_second_results)
prime_second_result = max(prime_second_results)
print('最终与集合最大值互质的计算结果:', prime_second_result)
# 关闭连接
for compute_socket in compute_sockets:
compute_socket.close()
control_socket.close()