基于 Python 及 MySQL 的 multi-paxos 算法的模拟复现
实验题目:基于 Python 及 MySQL 的 multi-paxos 算法的模拟复现
实验过程(包括实验环境、实验内容的描述、完成实验要求的知识或技能):
实验环境:
Win10,开发平台的名称及版本:python3.8 (pycharm),MySQL 数据库
实验内容:
.实验相关算法的简介:
Paxos 算法解决的问题正是分布式一致性问题,即一个分布式系统中的各个进程如何就某个值(决议)达成一致。
Paxos 算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。它利用大多数 (Majority) 机制保证了 2F+1 的容错能力,即 2F+1 个节点的系统最多允许 F 个节点同时出现故障。
一个或多个提议进程 (Proposer) 可以发起提案 (Proposal),Paxos 算法使所有提案中的某一个提案,在所有进程中达成一致。系统中的多数派同时认可该提案,即达成了一致。最多只针对一个确定的提案达成一致。
Paxos 将系统中的角色分为提议者 (Proposer),决策者 (Acceptor),和最终决策学习者 (Learner):
Proposer: 提出提案 (Proposal)。Proposal 信息包括编号 (Proposal ID) 和提议的值 (Value)。
Acceptor:参与决策,回应 Proposers 的提案。收到 Proposal 后可以接受提案,若 Proposal 获得多数 Acceptors 的接受,则称该 Proposal 被批准。
Learner:不参与决策,从 Proposers/Acceptors 学习最新达成一致的提案(Value)。
在多副本状态机中,每个副本同时具有 Proposer、Acceptor、Learner 三种角色。
Paxos 算法中的角色
Paxos 算法(basic-paxos)通过一个决议分为两个阶段(Learn 阶段之前决议已经形成):
第一阶段:Prepare 阶段。Proposer 向 Acceptors 发出 Prepare 请求,Acceptors 针对收到的 Prepare 请求进行 Promise 承诺。
第二阶段:Accept 阶段。Proposer 收到多数 Acceptors 承诺的 Promise 后,向 Acceptors 发出 Propose 请求,Acceptors 针对收到的 Propose 请求进行 Accept 处理。
第三阶段:Learn 阶段。Proposer 在收到多数 Acceptors 的 Accept 之后,标志着本次 Accept 成功,决议形成,将形成的决议发送给所有 Learners。
Multi-Paxos 只是一种思想,不是算法。Multi-Paxos 算法则是一种统称而已。它是指基于 Multi-Paxos 思想,通过多个 Basic Paxos 实例来实现一系列值的共识算法。
Multi-Paxos 实际上是 simple Paxos 实例(插槽)的序列,每个实例都按顺序编号。每个状态转换都被赋予一个“插槽编号”,集群的每个成员都以严格的数字顺序执行转换。为了更改群集的状态(例如,处理传输操作),我们尝试在下一个插槽中就该操作达成一致性。具体来说,这意味着向每个消息添加一个插槽编号,并在每个插槽的基础上跟踪所有协议状态。(simple-paxos 就单个静态值达一致性本身并不实用,我们需要实现的集群系统(银行账户服务)希望就随时间变化的特定状态(账户余额)达成一致。所以需要使用 Paxos 就每个操作达成一致)
通俗理解 Paxos 流程
Basic-paxos 流程
客户端将请求发送给提议者 proposer,提议者 proposer 不进行决策(即不进行事件的通过或者不通过),proposer 主要的功能是对请求打包请求给 acceptor(包括编号等等),将请求发送给 acceptor,acceptor 来进行决策,并且将决策发送给 learner 及 proposer,learner 进行日志存储及执行操作,并将操作结果返回给 Client.
具体的代码实现(时间较紧,故此次复现有点过于粗糙....代码注释也未写入...)
先导入了一些相关的库,上面的库主要用于一些数据的产生,以及判断等等,下面的则是用于显示界面(三台服务器)的显示(如下图,制作比较简陋)及数据库的操作。
创建了 Client1 类来作为服务器 1(其他两个服务器类似),第一个函数用来继承自己的函数及实例化,第二个 reception 函数用来接收从服务器传来的消息,第三个 handle 则是来执行数据库的操作(这个其实是 Learner 的职责,只不过因为便于显示数据操作是否成功,即请求是否成功的返回等等。)
接收来自服务器的数据(message 为消息,action 为进行什么样的操作)
handle 为接收数据并且连接数据库,并且进行相应的操作,接收的数据为 c,数据 c 是经过 propos 和 acceptor 打包决策之后的数据,数据 c 的类型为元组,数据格式为(时间戳,message,action,时钟),时间戳(格林尼治 1973 年??时间到今天的时间)相当于编号,messa,action 为请求,时钟则是为了避免并发性信息(例:服务器一和服务器二同时对一个数据进行请求时,先执行提交时间早的,然后在规定的时钟内还未执行完的话执行失败,规定时间执行成功则成功。)
其他两个服务器大同小异(如下图),一开始打算增加一个状态机和使用多线程来反映他们每个服务器的状态,状态机的设计以及需要统筹全局的多个函数及变量,开发时间不太够实现......
Proposer 类,即提议者,他的主要功能便是打包数据,并且将数据发送给 Acceptor 决策者,这里的打包用的是元组类型,元组的类型是的,不可变,所以用来存储不容易丢失。
c++
if 'message1' in globals().keys(): 加这一行代码的原因是当服务器不请求时,代码也能正常运行。
Accpetor 选举,这里我理解的 multi-paxos 的选举机制应该是和 zookeeper 的 ZAB 机制差不多的,看到文章说 multi-paxos 是多层 basic-paxos 嵌套的结果,但是 basic-paxos 的基本原理也是通过广播进行一半原则(即同意超过一半),所以才说节点为 2k+1 时,容错为 k,这里我用的是五个节点,即五个投票者,当其中一个挂掉的时候,其他也能再选举一个 leader,而这里用来模拟选举的则是通过随机数来进行选取,而真实的选举则可能是通过对节点的性能等等因素来进行 master 选举
一个选举节点,只有选举到他,即 leader,master 为他时,才会执行 if 里面的语句,即来担当 master 节点的职责。
进行选举的代码,第一次先执行一遍 voter1 到 5,将他们的选举结果进行分析,查看 leader(master)人数,如果是大于等于 2 的,即多个 leader,则这一轮选举失败,进行重新选举,直到选举出一个 leader 的时候,其他节点的意见,都得服从 leader 的意见,leader 来执行请求的决策,最后再把他导向 Learner 类。
提到 paxos 的选举就不得不提一提 chubby(底层的一致性是基于 paxos 算法实现的)
扩展:
Google Chubby 是一种松耦合式的分布式系统锁服务,GFS 和 Big-Table 都是通过它来解决分布式协作、master 节点选举、元数据存储等一系列同分布式锁相关的问题。Chubby 提供了粗粒度分布式锁服务。它的整个系统结构主要由服务端和客户端两部分组成,客户端通过 RPC 调用和服务端进行通信。它的底层一致性是以 Paxos 为基础的。
Chubby 中的 master 是唯一的提议者,这样就不会出现多个提议者同时提交议案从而造成议案冲突的情况。
那么,master 节点是如何产生的呢?在 Chubby 集群(Chubby cell)中,副本服务器通常采用 Basix Paxos 协议来进行投票选举出 master。选举出了 master,Chubby 就会在一段时间内不再将其它服务器作为 master 节点了,这段时间称为租期(Lease)。在运行过程中,master 节点会通过不断续租的方式来延长租期。比如,几天内都是同一个节点作为 master 节点。如果 master 节点故障了,那么会在租期到期后, 其他节点又发起新一轮投票选举出新的 master 节点。新 master 节点将执行新的租期。
Chubby 的客户端首先过向记录有 Chubby 服务端机器列表的 DNS 来请求获取所有的 Chubby 服务器列表,然后逐一发起请求询问该服务器是否是 master,在这个询问过程中,那些非 master 的服务器,则会将当前 master 所在的服务器标志反馈给客户端,这样客户端就能很快速的定位到 master 服务器了。
为了实现强一致性,只要该 master 服务器正常运行,那么客户端就会将所有的请求都发送到该 master 服务器上。所有的读请求和写请求都是由 master 来处理。
这次的复现和 chubby 这里实现的还差着一个 master 租期,即 master 过了一段时间便会重新一轮选举,而 master 也会通过一些方法来提高租期。这里只实现了程序一次运行选举一次,(当然,一开始可以一次请求执行一次选举,但是节点段时间不会故障,便不用这种耗费资源的方法来执行)
最后便是我们的 Learner 了!他来执行的操作是保存日志,以及进行操作,还及得一开始在服务器中定义的 handle 么,他终于在这里运行了!(将程序导向 leader,leader 再将它导向 handle)其实这里的执行顺序应该有一点错误,即先执行日志的写入再进行数据库的写入,如同 hive 仓库一样?但是我没有百度到他的准确执行顺序,所以这里也便不做相应的修改了。
实验结果
这里当点击服务器二的写入时,会有一点的延迟,即时钟的效果,这样就可以避免并发,但是他最终还是会响应到你的操作。
写入成功的提示
相应的选举情况,以及进行的操作
数据库中的数据一查也是写入成功的(因为我点击了按钮多次,所以他也进行了多次写入)
服务器一的日志
因为是最后测试的时候才在不同的日志文件里运行,而我又将以前的也复制进去了,所以日志文件前面部分才会相同
一、实验中的问题
实验中出现了许多问题,比如:将他们各个模块写好之后,不知道怎么连接到一块,但是这些还是一些小问题,最令人头疼的便是逻辑问题,即如果两个类互相使用,则会变成一个死循环,以及如何进行选举。
下图就是他只能进行第一段的选举,即在服务器一上面进行了选举之后,使用服务器二来进行操作,选举便会进入死循环,最终用了一个约束条件来使他成功了。
总结与感悟
每一次实验都是一次不错的实践,让自己更多的去了解一些相关的内容,便会越觉得这些算法的实现都是一些先驱经过长时间的思考即实践才最终实现的,而每一次实验不止在增长见识,也是对思想的一次指引,当自己通过自己的想法把算法的思路想通了,便离理解不远了,最终再通过自己用代码形式展现出来,便会觉得收获到了一些东西
代码附录
```c++
— — coding = utf-8 - -
@Time :2021/11/3018:18
@File :Multi-Paxos.py
@Software: PyCharm
'''
////////////////////////////////////////////////////////////////////
//
ooOoo
//
// o8888888o //
// 88" . "88 //
// (| ^
^ |) //
// O\ = /O //
// ____/
---'\____ //
// .' \\| |//
. //
// / \||| : |||// \ //
// /
||||| -:- |||||- \ //
// | | \\ - /// | | //
// | _| ''---/'' | | //
// \ .-_
-
/-. / //
//
. .' /--.--\
. . ___ //
// ."" '<
.___\_<|>_/___.' >'"". //
// | | :
- `.;
\ _ /
;.
/ -
: | | //
// \ \
-. \_ __\ /__ _/ .-
/ / //
// ========
-.____
-.
_
/___.-
____.-'======== //
//
=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永无BUG 永不修改 //
////////////////////////////////////////////////////////////////////
'''
导入库
import random import sys import time import numpy
ui显示库
import PySide2 import pymysql from PySide2 import QtWidgets from PySide2.QtUiTools import QUiLoader
class Client1: def init (self): super(Client1, self). init () # 子类继承父类init方法 self.ui = QUiLoader().load('服务器1.ui') # 登录界面路径加载 self.ui.pushButton.clicked.connect(self.reception) self.ui.pushButton.clicked.connect(Propser.pack_command)
def reception(self):
global message1
message1 = self.ui.lineEdit.text()
global action1
action1 = self.ui.lineEdit_2.text()
def handle(self, c):
lj = pymysql.connect(host="localhost", user="root", passwd="z12j34y56", database="Paxos",
charset="utf8") # 连接数据库
# 时钟暂停时间
time.sleep(c[3])
if c[2] == '读':
cursor = lj.cursor() # 定义游标
sql = "select * from data where data_list like '%{}%'".format(c[1]) # sql语句
# print(sql)
cursor.execute(sql) # 执行sql语句
data = cursor.fetchall() # 得到其中一条数据
# print(data)
if data: # 数据存在
self.ui.tableWidget.setRowCount(0) # 不设置限制
self.ui.tableWidget.insertRow(0)
for row, form in enumerate(data): # 循环
for column, item in enumerate(form): # 循环
# 将数据写入tableWidget
self.ui.tableWidget.setItem(row, column, PySide2.QtWidgets.QTableWidgetItem(str(item)))
column += 1
row_position = self.ui.tableWidget.rowCount()
self.ui.tableWidget.insertRow(row_position)
lj.commit() # 数据库操作完成
QtWidgets.QMessageBox.information(self.ui, "提示", "读取成功",
QtWidgets.QMessageBox.Yes)
if c[2] == '写':
cursor = lj.cursor() # 定义游标
sql = "insert into data(data_list) values('{}') ".format(c[1]) # sql语句
cursor.execute(sql) # 执行sql语句
lj.commit()
QtWidgets.QMessageBox.information(self.ui, "提示", "写入成功",
QtWidgets.QMessageBox.Yes)
if c[2] == '删':
cursor = lj.cursor() # 定义游标
sql = "delete from data where data_list = '{0}' ".format(c[1]) # sql语句
cursor.execute(sql) # 执行sql语句
lj.commit()
QtWidgets.QMessageBox.information(self.ui, "提示", "删除成功",
QtWidgets.QMessageBox.Yes)
lj.close()
# return message, action
class Client2: def init (self): super(Client2, self). init () # 子类继承父类init方法 self.ui = QUiLoader().load('服务器2.ui') # 登录界面路径加载
self.ui.pushButton.clicked.connect(self.reception)
self.ui.pushButton.clicked.connect(Propser.pack_command)
def reception(self):
global message2
message2 = self.ui.lineEdit.text()
global action2
action2 = self.ui.lineEdit_2.text()
def handle(self,c):
lj = pymysql.connect(host="localhost", user="root", passwd="z12j34y56", database="Paxos",
charset="utf8") # 连接数据库
# 时钟暂停时间
time.sleep(c[3])
if c[2] == '读':
cursor = lj.cursor() # 定义游标
sql = "select * from data where data_list like '%{}%'".format(c[1]) # sql语句
# print(sql)
cursor.execute(sql) # 执行sql语句
data = cursor.fetchall() # 得到其中一条数据
# print(data)
if data: # 数据存在
self.ui.tableWidget.setRowCount(0) # 不设置限制
self.ui.tableWidget.insertRow(0)
for row, form in enumerate(data): # 循环
for column, item in enumerate(form): # 循环
# 将数据写入tableWidget
self.ui.tableWidget.setItem(row, column, PySide2.QtWidgets.QTableWidgetItem(str(item)))
column += 1
row_position = self.ui.tableWidget.rowCount()
self.ui.tableWidget.insertRow(row_position)
lj.commit() # 数据库操作完成
if c[2] == '写':
cursor = lj.cursor() # 定义游标
sql = "insert into data(data_list) values('{}') ".format(c[1]) # sql语句
print(sql)
cursor.execute(sql) # 执行sql语句
QtWidgets.QMessageBox.information(self.ui, "提示", "写入成功",
QtWidgets.QMessageBox.Yes)
lj.commit()
if c[2] == '删':
cursor = lj.cursor() # 定义游标
sql = "delete from data where data_list = '{0}' ".format(c[1]) # sql语句
cursor.execute(sql) # 执行sql语句
QtWidgets.QMessageBox.information(self.ui, "提示", "删除成功",
QtWidgets.QMessageBox.Yes)
lj.commit()
lj.close()
class Client3: def init (self): super(Client3, self). init () # 子类继承父类init方法 self.ui = QUiLoader().load('服务器3.ui') # 登录界面路径加载
self.ui.pushButton.clicked.connect(self.reception)
self.ui.pushButton.clicked.connect(Propser.pack_command)
def reception(self):
global message3
message3 = self.ui.lineEdit.text()
global action3
action3 = self.ui.lineEdit_2.text()
def handle(self, c):
lj = pymysql.connect(host="localhost", user="root", passwd="z12j34y56", database="Paxos",
charset="utf8") # 连接数据库
# 时钟暂停时间
time.sleep(c[3])
if c[2] == '读':
cursor = lj.cursor() # 定义游标
sql = "select * from data where data_list like '%{}%'".format(c[1]) # sql语句
# print(sql)
cursor.execute(sql) # 执行sql语句
data = cursor.fetchall() # 得到其中一条数据
# print(data)
if data: # 数据存在
self.ui.tableWidget.setRowCount(0) # 不设置限制
self.ui.tableWidget.insertRow(0)
for row, form in enumerate(data): # 循环
for column, item in enumerate(form): # 循环
# 将数据写入tableWidget
self.ui.tableWidget.setItem(row, column, PySide2.QtWidgets.QTableWidgetItem(str(item)))
column += 1
row_position = self.ui.tableWidget.rowCount()
self.ui.tableWidget.insertRow(row_position)
lj.commit() # 数据库操作完成
if c[2] == '写':
cursor = lj.cursor() # 定义游标
sql = "insert into data(data_list) values('{}') ".format(c[1]) # sql语句
print(sql)
cursor.execute(sql) # 执行sql语句
QtWidgets.QMessageBox.information(self.ui, "提示", "写入成功",
QtWidgets.QMessageBox.Yes)
lj.commit()
if c[2] == '删':
cursor = lj.cursor() # 定义游标
sql = "delete from data where data_list = '{0}' ".format(c[1]) # sql语句
cursor.execute(sql) # 执行sql语句
QtWidgets.QMessageBox.information(self.ui, "提示", "删除成功",
QtWidgets.QMessageBox.Yes)
lj.commit()
lj.close()
class Propser: def init (self): super(Propser, self). init () # 子类继承父类init方法 self.pack_command()
def pack_command(self):
if 'message1' in globals().keys():
if message1:
t_sleep = random.randint(1, 10)
# time.sleep(t_sleep)
global c1
c1 =(time.time(),message1,action1,t_sleep)
print(c1)
Accpetor().vote()
if 'message2' in globals().keys():
if message2:
t_sleep = random.randint(1, 10)
# time.sleep(t_sleep)
global c2
c2 = (time.time(), message2,action2, t_sleep)
print(c2)
Accpetor().vote()
if 'message3' in globals().keys():
if message3:
t_sleep = random.randint(1, 10)
# time.sleep(t_sleep)
global c3
c3 = (time.time(), message3,action3, t_sleep)
Accpetor().vote()
class Accpetor: #选举leader,multi 和basic不同的便是multi是选举leader来保持一致性的, # 而basic则是直接向accpetor进行广播,对voter进行统计,超半数同意则成功
def __init__(self):
super(Accpetor, self).__init__() # 子类继承父类init方法
def voter1(self):
#这里用随机数来模拟选举
global r1
r1 = random.randint(1, 5)
if 'voter_num' in globals().keys():
if voter_num == 1:
#try:
if 'c1' in globals().keys():
if c1:
Client1.handle(c1)
if 'c2' in globals().keys():
if c2:
Client2.handle(c2)
if 'c3' in globals().keys():
if c3:
Client3.handle(c3)
return r1
def voter2(self):
# 这里用随机数来模拟选举
global r2
r2 = random.randint(1, 5)
if 'voter_num' in globals().keys():
if voter_num == 2:
if 'c1' in globals().keys():
if c1:
Client1.handle(c1)
if 'c2' in globals().keys():
if c2:
Client2.handle(c2)
if 'c3' in globals().keys():
if c3:
Client3.handle(c3)
return r2
def voter3(self):
# 这里用随机数来模拟选举
global r3
r3 = random.randint(1, 5)
if 'voter_num' in globals().keys():
if voter_num == 3:
if 'c1' in globals().keys():
if c1:
Client1.handle(c1)
if 'c2' in globals().keys():
if c2:
Client2.handle(c2)
if 'c3' in globals().keys():
if c3:
Client3.handle(c3)
return r3
def voter4(self):
# 这里用随机数来模拟选举
global r4
r4 = random.randint(1, 5)
if 'voter_num' in globals().keys():
if voter_num == 4:
if 'c1' in globals().keys():
if c1:
Client1.handle(c1)
if 'c2' in globals().keys():
if c2:
Client2.handle(c2)
if 'c3' in globals().keys():
if c3:
Client3.handle(c3)
return r4
def voter5(self):
# 这里用随机数来模拟选举
global r5
r5 = random.randint(1, 5)
if 'voter_num' in globals().keys():
if voter_num == 5:
if 'c1' in globals().keys():
if c1:
Client1.handle(c1)
if 'c2' in globals().keys():
if c2:
Client2.handle(c2)
if 'c3' in globals().keys():
if c3:
Client3.handle(c3)
return r5
def vote(self):
if 'voter_num' not in globals().keys():
self.voter1()
self.voter2()
self.voter3()
self.voter4()
self.voter5()
all_voter = [r1,r2,r3,r4,r5]
# print("all_voter",all_voter)
bcount = numpy.bincount(all_voter)
print("本次选举状况:",bcount)
count = 0
print("本次选举leader的人数:",bcount.max())
for i in bcount:
if i == bcount.max():
count +=1
print("leader数:",count)
if count >= 2:
self.voter1()
self.voter2()
self.voter3()
self.voter4()
self.voter5()
self.vote()
global voter_num
if count < 2:
for i in range(len(bcount)):
if bcount[i] == bcount.max():
voter_num = i
Learner().execute()
print("master是:",voter_num)
print("完成")
class Learner:
def __init__(self):
super(Learner, self).__init__() # 子类继承父类init方法
# self.accpetor = Accpetor()
def execute(self):
if voter_num == 1:
Accpetor().voter1()
if 'c1' in globals().keys():
if c1:
f = open("log1.txt", "a")
f.write(str(c1))
f.close()
if 'c2' in globals().keys():
if c2:
f = open("log2.txt", "a")
f.write(str(c2))
f.close()
if 'c3' in globals().keys():
if c3:
f = open("log3.txt", "a")
f.write(str(c3))
f.close()
if voter_num == 2:
Accpetor().voter2()
if 'c1' in globals().keys():
if c1:
f = open("log1.txt", "a")
f.write(str(c1))
f.close()
if 'c2' in globals().keys():
if c2:
f = open("log2.txt", "a")
f.write(str(c2))
f.close()
if 'c3' in globals().keys():
if c3:
f = open("log3.txt", "a")
f.write(str(c3))
f.close()
if voter_num == 3:
Accpetor().voter3()
if 'c1' in globals().keys():
if c1:
f = open("log1.txt", "a")
f.write(str(c1))
f.close()
if 'c2' in globals().keys():
if c2:
f = open("log2.txt", "a")
f.write(str(c2))
f.close()
if 'c3' in globals().keys():
if c3:
f = open("log3.txt", "a")
f.write(str(c3))
f.close()
if voter_num == 4:
Accpetor().voter4()
if 'c1' in globals().keys():
if c1:
f = open("log1.txt", "a")
f.write(str(c1))
f.close()
if 'c2' in globals().keys():
if c2:
f = open("log2.txt", "a")
f.write(str(c2))
f.close()
if 'c3' in globals().keys():
if c3:
f = open("log3.txt", "a")
f.write(str(c3))
f.close()
if voter_num == 5:
Accpetor().voter5()
if 'c1' in globals().keys():
if c1:
f = open("log1.txt", "a")
f.write(str(c1))
f.close()
if 'c2' in globals().keys():
if c2:
f = open("log2.txt", "a")
f.write(str(c2))
f.close()
if 'c3' in globals().keys():
if c3:
f = open("log3.txt", "a")
f.write(str(c3))
f.close()
if name == ' main ':#任务执行 app = QtWidgets.QApplication(sys.argv)#实例化一个应用对象(打开一个类似于窗口的东西) Client1 = Client1() # 令wm = 服务器界面 Client1.ui.show() # 展示登录界面
Client2 = Client2() # 令wm = 登录函数
Client2.ui.show() # 展示登录界面
Client3 = Client3() # 令wm = 登录函数
Client3.ui.show() # 展示登录界面
sys.exit(app.exec_()) # 程
```
参考文献
- 基于云计算的复杂系统仿真模型服务化技术研究(国防科技大学·熊思齐)
- 搜索引擎中网络爬虫的研究与实现(西安电子科技大学·段兵营)
- 推荐系统综合仿真平台执行容器的研究与实现(电子科技大学·吴争妍)
- 基于MySQL集群实现的高性能数据库架构设计(上海交通大学·朱红)
- 多协议实时仿真网络设计与数据可视化应用(北京邮电大学·唐燕艳)
- 基于J2EE的海量数据虚拟存储管理平台的设计与实现(电子科技大学·马志祥)
- Web数据库系统应用技术的研究(天津大学·高云)
- 面向多任务、多通道并行爬虫的技术研究(哈尔滨工业大学·李学凯)
- 银行决策支持系统中数据挖掘的研究与实现(中南大学·周四新)
- 多无人机系统虚拟仿真平台的设计与实现(天津大学·朱康瑞)
- Web漏洞扫描系统中的智能爬虫技术研究(杭州电子科技大学·黄亮)
- 仿真支撑平台数据管理和网络通信的设计与实现(武汉理工大学·骆彬)
- 多无人机系统虚拟仿真平台的设计与实现(天津大学·朱康瑞)
- 村镇环境预警系统中间件的设计与实现(天津大学·李佳)
- 基于SAN的存储管理软件的设计与实现(西北工业大学·可彦)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:源码项目助手 ,原文地址:https://m.bishedaima.com/yuanma/35967.html