【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. 基本功能
    1、建立两节点的通信。
    2、从节点1向节点2发送一个Python语言编写的源程序A,节点2执行程序A,并向节点1返回计算结果。

  2. 要求
    1、程序A可在节点2上独立完成运行,无需其它条件支持。

  3. 实现
    在任务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:
节点1
节点2:
节点2
节点3:
节点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()

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值