
Distributed Systems
Final Project
分布式文件系统
Student ID: 21307381 Student Name: LJW
Date: 2023.12.17
Lectured by: Pengfei Chen
Distributed Systems Course
Sun Yat-sen University
题目
设计⼀个分布式文件系统。该⽂件系统可以是 client-server 架构,也可以是 P2P ⾮集中式架构。 要求⽂件系统具有基本的访问、打开、删除、缓存等功能,同时具有⼀致性、⽀持多⽤户特点。 在设计过程中能够体现在分布式课程中学习到的⼀些机制或者思想,例如 Paxos 共识、缓存更新机制、访问控制机制、并⾏扩展等。 实现语⾔不限, 要求提交代码和实验报告,实验报告模板稍后在课程⽹站下载,提交时间为考试之后⼀周内。
题目要求:
基本要求: (1)、编程语言不限,选择自己熟悉的语⾔,但是推荐用Python或者 Java 语言实现; (2)、文件系统中不同节点之间的通信方式采用 RPC 模式,可选择 Python 版本的 RPC、gRPC 等; (3)、文件系统具备基本的文件操作模型包括:创建、删除、访问等功能; (4)、作为文件系统的客户端要求具有缓存功能即⽂件信息⾸先在本地存储搜索,作为缓存的介质可以是内存也可以是磁盘⽂件; (5)、为了保证数据的可用性和文件系统性能,数据需要创建多个副本,且在正常情况下,多个副本不在同⼀物理机器,多个副本之间能够保持⼀致性(可选择最终⼀致性即延迟⼀致性也可以选择瞬时⼀致性即同时写); (6)、⽀持多用户即多个客户端,⽂件可以并行读写(即包含文件锁); (7)、对于上述基本功能,可以在本地测试,利⽤多个进程模拟不同的节点,需要有相应的测试命令或者测试用例,并有截屏或者video ⽀持;
加分项: (1)、加⼊其它⾼级功能如缓存更新算法; (2)、Paxos 共识方法或者主副本选择算法等; (3)、访问权限控制; (4)、其他⾼级功能;
分布式文件系统总体设计
本系统架构设计主要参考自CS4032-Distributed-File-System。系统中有一个LockServer,一个DirectoryServer,多个FileServer组成服务器集群,可以有多个用户,每个用户可以开启多个Client。LockServer和DirectoryServer可以使用FileServer中互相通信的方法扩展为服务器集群,以便提高可用性(availability)。为了更加专注于分布式系统的设计,在本次实验中将数据保存在对应的txt文件中,模拟后台数据库。使用gRPC进行服务器之间的通信,每种类型的服务器都有自己的proto文件定义消息和提供的服务,用来和客户端或其他的文件进行交互。
- DirectoryServer是一个目录服务器,用于告诉Client现在有哪些FileServer是可用的,即向客户端提供可用的文件服务器信息,以便Client知道从哪个FileServer下载或上传文件。
- LockServer实现了互斥锁,文件有一个Client正在编辑时,不允许其他Client访问,即确保同一时间只有一个Client可以访问文件。
- FileServer是文件服务器集群,用于存储文件的多个副本,并确保它们之间的一致性。
- Client是客户端,可以选择一个FileServer作为主服务器,之后可以将服务器上的文件下载到cache中,也可以完成将cache中文件上传到服务器等功能。
运行流程描述
当客户端需要访问特定文件时,它首先会向DirectoryServer发送请求以获取可用的FileServer列表。DirectoryServer会返回一组可用的FileServer地址或其他相关信息,以便客户端可以选择一个合适的服务器进行文件操作。一旦客户端确定了目标FileServer,它可以与该服务器建立连接并发送请求。如果文件存在,客户端将向LockServer发送加锁请求。LockServer会检查文件的锁定状态,如果文件已经被其他客户端锁定,则客户端需要等待直到文件变为可用。一旦获得了锁定权限,客户端可以从FileServer下载文件到本地缓存中进行编辑。客户端可以在本地缓存中对文件进行修改,并可以随时保存更改。当客户端完成对文件的编辑并准备上传时,它可以向FileServer发送上传请求。FileServer会接受上传的文件,并确保在集群中的其他副本也得到更新,以保持一致性。一旦上传完成,客户端可以释放对文件的锁定,并向LockServer发送解锁请求,使其他客户端能够访问该文件。整个过程中,LockServer、DirectoryServer和FileServer之间的通信和协调确保了文件的互斥访问和一致性存储。客户端通过与这些服务器的交互,实现了对文件的下载、编辑和上传等功能,提供了灵活且可靠的文件操作体验。
实现功能
支持副本的瞬时一致性、支持多个账号多用户,也就是用户A的账号可以在多台不同机器上登陆,用户A使用文件服务器时,用户B的账号也可以多用户使用文件服务器、支持⽂件系统具备基本的⽂件操作(具体见下面表格)。
指令 | 功能 |
---|---|
help | 展示可用指令 |
upload | 从cache向服务器上传文件 |
download | 从服务器下载文件到cache |
touch | 本地cache和服务器中同时创建文件 |
touchoc | 只在服务器中创建文件 |
remove | 从服务器中删除文件 |
edit | 从服务器中获取最新版本的文件下载到cache中编辑 |
close | 关闭正在编辑的文件 |
ls | 展示服务器当前路径下文件 |
lse | 展示本地正在编辑的文件 |
cache | 展示cache中文件 |
pwd | 展示当前路径 |
mkdir | 在服务器中创建文件夹 |
cd | 进入某个文件夹 |
cd .. | 返回上一级文件夹 |
支持的加分项:
-
缓存更新机制:每次在本地编辑文件时都是最新的版本,即实现单调写一致性。
- 访问权限控制:用户A无法查看用户B的文件信息,以保护文件的安全性和隐私
- 其他的高级功能:pwd,cd,cd ..,mkdir,实现了服务器crash后的恢复机制。
运行效果
分布式文件系统详细设计
DirectoryServer
目录服务器DirectoryServer用于管理文件服务器(File Server)的注册和注销,并提供查询在线的文件服务器的功能。所以目录服务器既要与FileServer通信,让FileServer向DirectoryServer注册和注销,也要与Client通信,通知Client目前在线的FileServer,所以DirectoryServer的protobuf设计如下:
1
2
3
4
5
6
7
8
service DirServer {
// 1.将已注册的File Server返回Client
rpc AllFserver(DEmpty) returns (FileServerList){}
// 2.File Server开启时调用,在Directory Server中注册
rpc FserverTurnOn(FserverON_msg) returns (DirisSuccess) {}
// 3.File Server关闭时调用,在Directory Server中删除
rpc FserverTurnOff(FserverOff_msg) returns (DirisSuccess) {}
}
定义一个名为Dserver
的类,继承自DirectoryServer_pb2_grpc.DirServerServicer
,用这个类实现gRPC服务端的接口。类的构造函数__init__
初始化了目录服务器的相关参数,包括IP地址、端口号、目录服务器数据存放根路径、目录日志文件名等。
在Dserver
类中定义了三个gRPC接口函数,以实现protobuf中定义好的service:
AllFserver
:用于获取所有在线的文件服务器列表。FserverTurnOn
:用于将文件服务器注册到目录服务器上。FserverTurnOff
:用于将文件服务器从目录服务器注销。
此外,Dserver
类还定义了一些辅助函数,包括:
DserverON
:打印目录服务器正在工作状态。DserverOff
:打印目录服务器崩溃状态。AllFServerAlive
:获取所有在线的文件服务器列表。AddFserver
:将文件服务器添加到目录服务器的日志中。DeleteFserver
:从目录服务器的日志中删除文件服务器。
定义了一个RunDirServer
函数,用于启动目录服务器。在该函数中,首先创建了Dserver
对象,并调用其DserverON
方法说明目录服务器正在工作。然后,检查目录根路径是否存在,如果不存在则创建一个新的目录,说明服务器是第一次工作。接着,检查目录日志文件是否存在,如果存在则将其重命名为历史文件,用于故障后恢复,并创建一个新的目录日志文件。然后,创建一个gRPC服务器,并将Dserver
对象添加为其服务端接口的实现。指定服务器监听的地址和端口,并启动服务器。最后,通过一个无限循环保持服务器运行,直到捕获到键盘中断信号(Ctrl+C),用以模仿服务器崩溃,此时调用DserverOff
方法打印崩溃状态。
LockServer
互斥锁服务器用于确保同一时间只有一个Client可以访问文件,所以只需要和Client通信即可,LockServer的protobuf设计如下:
1
2
3
4
5
6
service LockServer {
// 加锁
rpc lockfile(LockFileInfo) returns (LockisSuccess){}
// 解锁
rpc unlockfile(UnLockFileInfo) returns(LockisSuccess){}
}
定义 LockServer
类,并且该类继承自 LockServer_pb2_grpc.LockServerServicer
这个 gRPC 服务类。
在LockServer
类中定义了三个gRPC接口函数,以实现protobuf中定义好的service:
lockfile
:用于请求锁定文件,当文件已经被锁定时会抛出异常。unlockfile
:用于释放文件的锁定状态。
此外,LockServer
类还定义了一些辅助函数,包括:
LserverON
:打印锁服务器正在工作状态。LserverOff
:打印锁服务器崩溃信息。AllLockFIle
:读取锁日志文件内容,返回一个字典,存储了所有的文件锁定信息。Lock
:锁定文件,将文件路径、文件名、客户端 ID 写入锁日志文件。UnLock
:释放文件锁定,从锁日志文件中删除相应的记录。
定义了一个RunLockServer
函数,用于启动锁服务器。在该函数中,首先创建了LockServer
对象,并调用其LserverON
方法说明锁服务器正在工作。然后,检查锁根路径是否存在,如果不存在则创建一个新的目录,说明服务器是第一次工作。接着,检查锁日志文件是否存在,如果存在则将其重命名为历史文件,用于故障后恢复,并创建一个新的锁日志文件。然后,创建一个gRPC服务器,并将LockServer
对象添加为其服务端接口的实现。指定服务器监听的地址和端口,并启动服务器。最后,通过一个无限循环保持服务器运行,直到捕获到键盘中断信号(Ctrl+C),用以模仿服务器崩溃,此时调用LserverOff
方法打印崩溃状态。
在客户端上传文件时,先会对文件上锁,然后主文件服务器收到上传文件后,不会立刻返回成功信息,而是先将这个文件同步到所有在线的副本服务器上,然后才会释放对这个文件的锁,这样可以防止在同步过程中又被打开修改了。
FileServer
FileServer是系统的核心,要处理Client发过来的文件处理请求,需要与DirectoryServer通信,向DirectoryServer注册和注销,此时FileServer是以客户端的身份,远程调用DirectoryServer的接口。但是同时也要和Client通信,处理Client发过来的请求,此时FileServer就要以服务器的身份,提供接口给Client调用了。并且FileServer之间也要通信,用于同步更新副本的状态。根据需要完成的文件处理指令,FileServer的protobuf设计如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
service FileServer {
// 与Client通信
rpc success_connect(CLIENTID) returns(FEmpty){}
rpc download(Download_Request) returns (File) {}
rpc upload(Upload_Request) returns (FileisSuccess) {}
rpc remove(Remove_Request) returns (FileisSuccess){}
rpc list(List_Request) returns(List_Reply){}
rpc pwd(CLIENTID) returns(PWD_Reply){}
rpc mkdir(Mkdir_Request) returns(FileisSuccess){}
// 与其他FileServer通信
rpc upload_to_other_Fserver(Upload_Request) returns (FileisSuccess) {}
rpc remove_from_other_Fserver(Remove_Request) returns (FileisSuccess){}
}
定义了Fserver
类,并且继承自FileServer_pb2_grpc.FileServerServicer
类,这个类中定义了文件服务器可以提供的gRPC服务,包括:
success_connect
:每当客户端连接到文件服务器时,将调用此服务。它会向Directory Server请求在线文件服务器的列表,并在所有当前在线的文件服务器上为该用户创建一个文件夹。download
:客户端可以调用此方法从文件服务器下载文件。文件服务器会返回文件内容。upload
:客户端可以调用此方法将文件上传到主文件服务器。remove
:客户端可以调用此方法从文件服务器删除文件。list
:客户端可以调用此方法列出文件服务器上指定目录下的所有文件和子目录。pwd
:客户端可以调用此方法获取当前工作目录。mkdir
:客户端可以调用此方法在文件服务器上创建新文件夹。upload_to_other_Fserver
:此时主服务器以客户端的身份,调用该函数,用来将文件上传到其他的副本服务器,每一个文件服务器都有可能作为主服务器,也有可能是副本服务器,由用户连接的时候选择。remove_from_other_Fserver
:此时主服务器以客户端的身份,调用该函数,用来将文件从其他的副本服务器中删除,每一个文件服务器都有可能作为主服务器,也有可能是副本服务器,由用户连接的时候选择。
此外,FileServer
类还定义了一些辅助函数,包括:
Connect2Dserver
:这个函数用于与目录服务器建立连接。它会创建一个gRPC通道,然后使用该通道创建一个目录服务器的存根(Stub)对象,以便后续与目录服务器进行通信。FserverON
: 这个函数用于将文件服务器注册到目录服务器上。它会以客户端的身份调用目录服务器中函数:发送一个包含文件服务器ID、IP地址和端口号的消息,并等待目录服务器的响应。如果注册成功,就打印注册成功的消息;否则,打印注册失败的消息。FserverOff
: 这个函数用于将文件服务器从目录服务器中注销。它会以客户端的身份调用目录服务器中函数:发送一个包含文件服务器ID的消息,并等待目录服务器的响应。如果移除成功,就打印移除成功的消息;否则,打印移除失败的消息。Upload2OtherFserver
: 这个函数用于主服务器将文件同步上传到其他副本文件服务器。首先,它会获取所有在线的文件服务器列表。然后,对于每个非主服务器,主服务器会以客户端身份会创建一个与该服务器的通信通道,并使用该通道创建一个文件服务器的存根对象。接下来,它会将要上传的文件路径转换成相对路径,并调用存根对象的上传方法将文件传输到其他文件服务器。如果传输过程中发生错误,会打印错误消息并继续传输其他文件。最后,打印成功同步上传的消息。DelFromOtherFserver
: 这个函数用于将文件同步从其他文件服务器中删除。首先,它会获取所有在线的文件服务器列表。然后,对于每个非主服务器,主服务器会以客户端身份创建一个与该服务器的通信通道,并使用该通道创建一个文件服务器的存根对象。接下来,它会调用存根对象的删除方法,从其他文件服务器中删除指定路径的文件。如果删除成功,会打印删除成功的消息;否则,打印删除失败的消息。RemoveFile
: 这个函数用于删除文件服务器上的文件。它会检查指定路径的文件是否存在,如果存在就使用os.remove()
函数将其删除。SaveFile
: 这个函数用于保存从客户端接收到的文件。它会将接收到的文件数据写入到文件服务器上指定路径的文件中,并返回保存的文件路径。GetFile
: 这个函数用于从文件服务器上获取文件。它会读取指定路径的文件,并将文件数据封装成gRPC的文件对象返回给客户端。GetCurServerFile
: 这个函数用于获取当前文件服务器上的文件,并将其转换为适合在其他服务器上保存的格式。它会读取源文件,将文件数据封装成gRPC的上传请求对象,并指定目标路径作为相对路径。然后,将封装好的请求对象用于在其他文件服务器上,用以上传文件。
定义了一个RunFileServer
函数,用于启动文件服务器。在该函数中,首先创建了Fserver
对象,并传入文件服务器的ID、IP地址和端口号作为参数。调用Connect2Dserver()
函数与目录服务器建立连接,连接目录服务器成功后调用FserverON()
函数将文件服务器注册到目录服务器上。检查文件根路径是否存在,如果不存在则创建一个新的目录,说明服务器是第一次工作。然后,创建一个gRPC服务器,并将FileServer
对象添加为其服务端接口的实现。指定服务器监听的地址和端口,并启动服务器。最后,通过一个无限循环保持服务器运行,直到捕获到键盘中断信号(Ctrl+C),用以模仿服务器崩溃,此时调用FserverOff
将当前文件服务器从目录服务器上移除。
Client
用gRPC远程调用以上三个服务器的接口即可,具体实现如下:
主要功能函数包括:
(1)help
:输出可用的命令说明。
(2)upload2MainFServer
:将本地文件上传到主服务器,由主服务器负责同步到所有副本服务器上,使用了FileServer RPC服务。
(3)download
:从服务器上下载文件到本地缓存,使用了FileServer RPC服务。
(4)touch
:在本地和服务器上创建一个文件,使用了upload2MainFServer函数。
(5)touchOnlyCloud
:仅在服务器上创建一个文件,使用了upload2MainFServer函数和os.remove函数。
(6)remove
:删除服务器上的文件,使用了FileServer RPC服务。
(7)edit
:编辑服务器上的文件,先将其下载到本地缓存,然后再上传回服务器,使用了FileServer RPC服务。
(8)close
:关闭正在编辑的文件,将其同步到服务器上,使用了upload2MainFServer函数和unlockfile函数。
(9)ListAllFile
:列出当前服务器路径下的所有文件和目录,使用了FileServer RPC服务。
(10)ListEditingFile
:列出所有正在编辑的文件。
(11)mkdir
:在服务器上创建一个目录,使用了FileServer RPC服务。
(12)cd
:进入指定的目录,使用了ListAllFile函数。
(13)cdb
:返回上一级目录。
除了以上主要的功能函数外,还有一些辅助函数用以辅助实现上述功能:
(1)lockfile
:使用LockServer RPC服务锁定正在编辑的文件。
(2)unlockfile
:使用LockServer RPC服务解锁正在编辑的文件。
(3)ExtractFile
:将本地文件读取为字节流。
(4)SaveFile
:将字节流写入本地文件。
(5)Connect2Dserver(self)
:这个函数用于连接到目录服务器。然后,通过创建一个目录服务器的Stub对象(DS_pb2_grpc.DirServerStub
),客户端可以使用该Stub对象进行后续的RPC调用。
(6)Connect2Lserver(self)
:这个函数用于连接到锁服务器。类似于Connect2Dserver
函数,通过创建一个锁服务器的Stub对象(LS_pb2_grpc.LockServerStub
),客户端可以使用该Stub对象进行后续的RPC调用。
(7)Connect2Fserver(self)
:这个函数用于连接到文件服务器。首先,它调用了Connect2Lserver
函数,以确保在连接到文件服务器之前没有并行写冲突。接下来,它调用了Connect2Dserver
函数,以获取所有正在运行的文件服务器列表。然后,打印出所有文件服务器的信息,并要求用户选择一个主文件服务器。当用户输入主文件服务器的ID后,函数根据选择的文件服务器ID从文件服务器列表中获取对应的IP地址和端口号。然后,通过不安全信道连接到选定的主文件服务器,并创建一个文件服务器的Stub(FileServer_pb2_grpc.FileServerStub
),以便后续的RPC调用。最后,函数向文件服务器发送成功连接的请求,并获取当前路径信息。
最后定义一个RunClient
函数,通过输入不同的命令调用上述函数,从而实现与文件服务器进行交互。
效果展示
运行系统前:
首先注册两个文件服务器,三个客户端,运行起来后: DirectoryServer默认IP是locolhost,Port是8080 LockServer默认IP是locolhost,Port是8081 FileServer默认IP是locolhost,Port从10001开始(由用户输入)
服务器文件:
客户端cache:
help
touchoc
仅在服务器创建文件1.txt
创建成功并同步到所有在线的服务器。
download
从服务器下载1.txt
可以看到下载成功,cache中已经有了1.txt
touch
服务器和本地同时创建文件,可以看到服务器和cache同时有了2.txt
remove
从服务器中删除2.txt,下图中可以看到服务器中已经没有了2.txt,但是不会影响客户端本地的2.txt
upload
上面删除2.txt后,从本地上传2.txt到服务器
edit加锁和缓存更新机制
看蓝色框内的客户端1,客户端编辑时会自动从服务器中下载最新版本的文件,客户端1(蓝色框)在编辑客户端2(红色框)就不可编辑(加了互斥锁)
lse
close和单调写一致性
每次编辑完后都将最新的更新到服务器,保证下次编辑是基于最新版的文件
每次打开都会写入打开实现模拟编辑后效果,可以看cache中文件,每次写都基于上一次最新的版本(以下是多次打开后结果展示):
pwd
显示当前路径,只显示在文件服务器中的相对路径
mkdir
cd和cd..
可以用cd进入hello文件夹,然后cd..退回到上一级文件夹
服务器崩溃后重启
上述操作运行完毕后
将它们全部关闭后重新启动,可以看到会将当时的directory记录保存在history_{crash时间}文件中,用来故障后恢复信息。


访问权限控制和多用户多客户端
可以看到上面两个是用户1,下面一个是用户2,它们创建文件和编辑文件不会互相影响,即使用户1的客户端1正在编辑1.txt文件,那么只是用户1的客户端2无法编辑,但是用户2的客户端1仍可以创建1.txt并编辑
如何复现
- 首先要确保有gRPC环境:
1
2
3
pip install grpcio
pip install grpcio-tools
pip install protobuf
- 由于gRPC版本可能不同,所以运行前可以先点击Proto文件夹中compile.bat,使用gRPC的工具包,生成gRPC服务类:
1
2
3
start cmd /k "python -m grpc_tools.protoc --python_out=../ --grpc_python_out=../ -I. LockServer.proto"
start cmd /k "python -m grpc_tools.protoc --python_out=../ --grpc_python_out=../ -I. FileServer.proto"
start cmd /k "python -m grpc_tools.protoc --python_out=../ --grpc_python_out=../ -I. DirectoryServer.proto"
- 可以先将ClientData和ServerData中文件清空,然后直接点
RunAll.bat
,就会开启一个目录服务器DirectoryServer、一个锁服务器LockServer和一个文件服务器FileServer1。如果还想开启更多文件服务器可以点击RunFserver.bat
,就会运行一个新的文件服务器FileServer2,以此类推还可以运行更多文件服务器。点击运行RunClient.bat
即可运行一个Client,一个用户(一个UserID)可以开启多个客户端,选择不同的主服务器。
心得体会
在这个项目中,我使用Python和gRPC实现了一个简单但功能比较齐全的分布式文件存储系统。这个系统可以实现文件的上传、下载和删除等功能,同时也具备一定的容错能力和可拓展性。在实现过程中,我遇到了很多挑战,但最终通过不断地调试和优化,成功地完成了这个项目。
由于在作业3中已经用gRPC实现过消息订阅系统,对gRPC比较熟悉了,所以直接选用了gRPC作为RPC的通信工具。一开始想在作业3的消息订阅系统的基础上完成《去中心化的聊天系统》,已经简单完成聊天功能准备实现聊天记录去中心化存储时发现,本质上还是分布式的文件系统,所以干脆转为做《分布式文件系统》,这样之后直接借用分布式文件系统的思想也就可以完成《去中心化的聊天系统》中聊天数据去中心化的操作了。(由于这学期课程作业太多了,所以等寒假时再来完成这一个合并的工作,demo代码也作为附录提交了)
首先,我需要设计整个系统的架构。考虑到分布式系统的特点,我采取了类似于CS4032-Distributed-File-System的设计思路,使用了DirectoryServer和LockServer来辅助实现一致性和互斥性。DirectoryServer负责记录在线的FileServer,而LockServer则负责保证在同一时间只有一个Client可以修改同一个文件。
接下来,我需要实现各个组件之间的通信,这一步的难点在于protobuf的设计。通过在.proto文件中定义消息格式,然后使用gRPC的编译器生成相应的Python代码。protobuf中服务接口,可以边实现功能边添加,不用一次性设计的很完美。由于调试的时候需要多次更新protobuf,所以我干脆写了一个compile.bat
文件一键编译,这样就不用慢慢输入命令行了,运行DirectoryServer和LockServer服务器时同理,写了三个使用率很高的bat文件,大大提高了开发效率。
在实现过程中,我还遇到了一些难点,其中最主要的是如何实现一致性。最初,我考虑让Client从DirectoryServer获取所有在线的FileServer,然后与每个服务器建立通信,然后一一上传并覆盖原有文件。但后来发现这样的效率很低,特别是当客户端和文件服务器之间的距离非常远时,通信消耗会很大。因此,我想到了使用一个主服务器来和客户端通信,客户端只需要将文件上传到主服务器,然后由主服务器负责文件服务器集群中的副本同步和一致性,让上传和同步两个工作解耦。这种方案显然更加高效,并且同时保证了系统的一致性。
总的来说,这个项目不仅让我深入理解了gRPC协议和protobuf的使用,还让我学会了如何解决一些分布式系统中常见的问题,例如一致性和可靠性等。通过不断地测试和调试,我最终成功地实现了一个功能比较齐全的分布式文件存储系统,这对于未来从事分布式系统的研究和开发都具有很大的帮助。
实验建议
就像心得中提到的,我觉得可以将HW3中《用gRPC实现过消息订阅系统》改为《用gRPC实现聊天系统》,然后期末再在HW3基础上实现一个《去中心化的聊天系统》,这样实验似乎更具连贯性。