95 – 702 信息系统管理分布式系统 Distributed Systems for ISM
项目2 客户端 – 服务器计算 Client-Server Computing
五个任务
:checkered_flag: 向Canvas提交一个名为Your_Last_Name_First_Name_Project2.pdf的单个PDF文件,以及一个包含以下五个IntelliJ项目(任务0至4)的单个zip文件。
单个PDF将包含您对标记有方格旗的问题的回答。重要的是,您要清晰地标记每个答案,并提供下面提供的标签。同样重要的是,要准备好展示您的工作代码,以防我们需要验证您的提交。确保在PDF提交的顶部提供您的姓名和电子邮件地址。
五个IntelliJ项目将作为五个zip文件提交,每个文件将是任务0、1、2、3和4的整个IntelliJ项目的zip。除任务1外,每个IntelliJ项目都将包含一个客户端和一个服务器。任务1将包含一个客户端、一个服务器和一个中间的恶意玩家。对于每个项目,压缩整个项目,您需要在IntelliJ中使用“File->Export Project->To Zip”。
当您完成所有工作后,将一个PDF和五个项目zip文件压缩到一个大的zip文件中进行提交。将此最终文件命名为your_andrew_id.zip。
学习目标:
我们的第一个目标是让您能够使用用户数据报协议(UDP)。UDP用于许多互联网应用程序。域名服务(DNS)和动态主机配置协议(DHCP)都使用UDP。大多数视频和音频流量使用UDP。在线游戏和IP语音(VoIP)使用UDP。当我们需要高性能并且不介意偶尔丢失数据包时,我们使用UDP。
另一方面,TCP也被广泛使用。它努力确保在传输过程中不会丢失任何一点信息。超文本传输协议(HTTP)使用TCP。在这个项目中我们不使用TCP。
我们的第二个目标是了解中间恶意玩家的影响。
我们的第三个目标是让您了解远程过程调用(RPC)提供的抽象。我们通过要求您使用代理设计并隐藏通信代码并将其与应用程序代码分开来实现这一点。RPC已经使用了四十年,是许多分布式系统的基础。
我们的第四个目标是学习如何分发独立应用程序。我们使用一个简单的神经网络作为我们的应用程序。我们的意图不是在分布式系统课程中研究神经网络。但是你们中的一些人可能决定深入研究神经网络并将此应用程序作为起点。
可选地,您可以使用大型语言模型(基于神经网络),如ChatGPT或Copilot,来创建您的一些代码。任务0、任务1和任务4必须在没有大型语言模型帮助的情况下完成。考试问题将具体询问关于这三个任务的代码。虽然您可以使用AI工具完成这三个任务,但这完全是可选的。也会有关于这些任务的问题,但这些问题将更具通用性(因为不同的学生可能使用不同的技术来编码这些任务)。
提交注意事项:
当您被要求提交Java代码(在单个pdf上)时,它应该有文档说明。如果代码没有良好的文档说明,将扣除分数。每个重要的代码块都将包含一个注释,描述该代码块的用途。请参阅Canvas/Home/Documentation以获取良好和不良文档的示例。
评分标准
请参阅Canvas上的一般课程评分标准。我们将使用此作业的特定未发布评分标准,但一般评分标准提供了关于此作业如何评估的大致指导。
一些简化:
在接下来的所有内容中,我们关注的是设计服务器来处理一次一个客户端。我们不探讨围绕多个同时访问者的重要问题。如果您编写一个多线程服务器来同时处理多个访问者,那很好,但不是必需的。它不会获得额外的学分。
此外,对于接下来的所有内容,我们假设服务器在客户端运行之前运行。如果您想处理客户端首先运行而没有运行服务器的情况,那很好,但不会获得额外的学分。
在任务1中,我们假设服务器在恶意玩家之前运行,恶意玩家在客户端之前运行。
在这个作业中,您不需要关心数据验证。您可以假设用户输入的数据格式正确。
一般来说,如果这些要求没有明确要求某个功能,那么您不需要提供该功能。不会为额外的功能授予额外的分数。
引用您的来源
如果您使用任何不属于您的代码(包括来自大型语言模型的代码),您需要清楚地引用来源 – 在代码上方包含完整的URL并将其放在复制的代码上方。如果您使用大型语言模型生成代码,请确保说明。请注意引用您的来源。如果您提交的代码不是您自己创建的并且您没有包含适当的引用,那么这将被报告为学术违规。
任务0介绍UDP。将IntelliJ项目命名为“Project2Task0”。
在任务0中,您将对EchoServerUDP.java和EchoClientUDP.java进行一些修改。请注意,
这两个程序是标准的Java,我们不需要在IntelliJ中构建Web应用程序。这两个程序都将放在同一个IntelliJ项目中。
来自Coulouris文本的EchoServerUDP.java
import java.net.*;
import java.io.*;
public class EchoServerUDP{
public static void main(String args[]){
DatagramSocket aSocket = null;
byte[] buffer = new byte[1000];
try{
aSocket = new DatagramSocket(6789);
DatagramPacket request = new DatagramPacket(buffer, buffer.length);
while(true){
aSocket.receive(request);
DatagramPacket reply = new DatagramPacket(request.getData(),
request.getLength(), request.getAddress(), request.getPort());
String requestString = new String(request.getData());
System.out.println("Echoing: "+requestString);
aSocket.send(reply);
}
}catch (SocketException e){System.out.println("Socket: " + e.getMessage());
}catch (IOException e) {System.out.println("IO: " + e.getMessage());
}finally {if(aSocket!= null) aSocket.close();}
}
}
请注意DatagramSocket和DatagramPacket之间的区别。服务器使用DatagramPacket从客户端接收数据(在request对象中)。并且它使用DatagramPacket将数据发送回客户端(在reply对象中)。DatagramPacket始终基于字节数组。因此,要在DatagramPacket中发送消息,我们必须首先将消息转换为字节数组。要从DatagramPacket接收消息,我们必须将字节数组转换为字符串消息(如果我们期望的是字符串消息)。
请注意下面客户端如何做同样的事情。客户端想要发送字符串消息。因此,它从字符串(变量m)中提取字节数组。然后我们使用m来构建DatagramPacket。
当客户端接收回复时,方法reply.getData()返回字节数组 – 我们用它来构建字符串对象。
来自Coulouris文本的EchoClientUDP.java
import java.net.*;
import java.io.*;
public class EchoClientUDP{
public static void main(String args[]){
// args give message contents and server hostname
DatagramSocket aSocket = null;
try {
InetAddress aHost = InetAddress.getByName(args[0]);
int serverPort = 6789;
aSocket = new DatagramSocket();
String nextLine;
BufferedReader typed = new BufferedReader(new InputStreamReader(System.in));
while ((nextLine = typed.readLine())!= null) {
byte [] m = nextLine.getBytes();
DatagramPacket request = new DatagramPacket(m, m.length, aHost, serverPort);
aSocket.send(request);
byte[] buffer = new byte[1000];
DatagramPacket reply = new DatagramPacket(buffer, buffer.length);
aSocket.receive(reply);
System.out.println("Reply from server: " + new String(reply.getData()));
}
}catch (SocketException e) {System.out.println("Socket Exception: " + e.getMessage());
}catch (IOException e){System.out.println("IO Exception: " + e.getMessage());
}finally {if(aSocket!= null) aSocket.close();}
}
}
- 在IntelliJ中使这些程序运行。这两个程序放在同一个Intellij项目中,您将获得两个窗口来与两个程序进行交互。对客户端和服务器进行以下修改。
- 将客户端的“arg[0]”更改为硬编码的“localhost”。
- 记录客户端和服务器。描述每行代码的作用。
- 在客户端的顶部添加一行,以便在启动时通过在控制台上打印消息来宣布“UDP客户端正在运行”。
- 在宣布客户端正在运行后,让客户端提示用户输入服务器端端口号。然后它将使用该端口号联系服务器。目前,输入6789。
- 在服务器的顶部添加一行,以便在启动时宣布“UDP服务器正在运行”。
- 在宣布服务器正在运行后,让服务器提示用户输入服务器应该监听的端口号。当提示时,输入6789。
- 在服务器上,检查requestString的长度并注意它太大。对服务器代码进行修改,以便将请求数据复制到具有正确字节数的数组中。使用此字节数组构建正确大小的requestString。如果没有这些修改,服务器上可能会显示不正确的数据。每次访问时,您的服务器将显示从客户端到达的请求。
- 在客户端上进行同样的操作,以正确处理响应的大小。
- 如果客户端输入命令“halt!”,客户端和服务器都将停止执行。当客户端程序从用户那里收到“halt!”时,它将“halt!”发送到服务器,并在听到服务器的“halt!”消息后,客户端退出。当服务器从客户端收到“halt!”时,它将响应客户端“halt!”,然后退出。
- 在客户端添加一行,以便在退出时宣布。它将“UDP客户端侧退出”写入客户端控制台。
- 在服务器添加一行,以便在退出时进行宣布。服务器仅在客户端告知其这样做时(并且刚刚响应客户端的“halt!”消息)退出。它将“UDP服务器侧退出”写入服务器控制台。
:checkered_flag:在您的单个pdf上,复制您的客户端并清楚地标记为“Project2Task0Client”。
:checkered_flag:在您的单个pdf上,复制您的服务器并清楚地标记为“Project2Task0Server”。
:checkered_flag:截取您的客户端控制台屏幕的屏幕截图。它将包括客户端发送到服务器的五行数据以及客户端对用户“halt!”请求的响应。在您的单个pdf上,将此屏幕截图标记为“Project2Task0ClientConsole”。
:checkered_flag:截取您的服务器控制台屏幕的屏幕截图。它将包括客户端发送的五行数据以及服务器对客户端“halt!”请求的响应。在您的单个pdf上,将此屏幕截图标记为“Project2Task0ServerConsole”。
任务1说明了对UDP的恶意玩家中间攻击。将IntelliJ项目命名为“Project2Task1”。
在任务1中,您将尝试恶意玩家中间攻击。这个恶意玩家不仅仅对窃听客户端和服务器之间的对话感兴趣。它是一个主动的恶意玩家。您可能想通过先处理一个被动的恶意玩家开始 – 一个只窃听并将消息传递给服务器,然后再传递回客户端的玩家。
我们将在一个项目中有三个UDP程序。
将您的恶意玩家命名为EavesdropperUDP.java。您需要按照下面的描述设计并编写EavesdropperUDP.java。
首先,运行在任务0中修改后的EchoServerUDP.java。EchoServerUDP将提示您输入其端口。为EchoServerUDP输入端口6789进行监听。
其次,运行EavesdropperUDP.java。EavesdropperUDP将声明它正在运行,并会询问您两个端口。一个端口将是EavesdropperUDP.java将监听的端口,另一个端口将是服务器的端口号,Eavesdropper.java将伪装成该服务器。我们希望Eavesdropper.java在其控制台上显示(通过它的控制台)所有通过它的消息。我们希望它窃听线路。它将在端口6798上监听,希望一个愚蠢的客户端会犯转录错误。
第三,当您运行EchoClientUDP.java时,为其提供正确的端口(真正服务器的端口)或Eavesdropper正在监听的端口。也就是说,它将与6789或6798一起工作。
Eavesdropper是一个主动攻击者。如果客户端发送包含单词“like”的字符串,窃听者将用单词“dislike”替换“like”。如果“like”作为另一个单词的子字符串包含在内,例如“dislike”,窃听者将不会理会“like”这个字符串。
窃听者只需要将单词“like”的第一次出现替换为单词“dislike”,并且它将不理会服务器的响应。换句话说,当它从客户端收到“like”时,它将“dislike”发送到服务器,然后不理会服务器的响应。客户端将收到“dislike”。
如果客户端发送消息“halt!”,则服务器将像往常一样响应,然后停止执行。客户端在收到服务器的消息后将停止。我们的恶意玩家将永远运行。并且它将其看到的所有内容显示到其控制台。
:checkered_flag:在您的单个pdf上,复制您记录的EavesdropperUDP.java程序。 :checkered_flag:截取显示您的客户端、服务器和窃听者控制台的屏幕截图。该截图将显示客户端发送的几行数据以及服务器对“halt!”请求的响应。它还将显示窃听者控制台 – 显示客户端和服务器之间的整个交互。在您的单个pdf上,将此屏幕截图标记为“Project2Task1ThreeConsoles”。确保显示客户端使用端口6789(正确的服务器)和6798(恶意玩家)。这个想法是提供屏幕截图,证明客户端可以与两个服务器一起工作。您还需要显示单词“like”被替换为单词“dislike”**
在其余任务(任务2至4)中,我们没有为客户端提供停止服务器的能力。我们只在任务0和1中这样做。在其余任务中,服务器将一直运行。在其余任务中,我们不使用窃听者。
任务2说明了使用UDP的代理设计。将IntelliJ项目命名为“Project2Task2”。
对“EchoServerUDP.java”和“EchoClientUDP.java”进行以下修改:
- 将客户端命名为“AddingClientUDP.java”。将服务器命名为“AddingServerUDP.java”。
- 服务器将保存一个整数值sum,初始化为0,并将接收来自客户端的请求 – 每个请求都包括一个要添加到sum的整数值(正数、负数或0)。在每次请求时,服务器将新的sum作为响应返回给客户端。在服务器端控制台,在客户端每次访问时,客户端的请求和新的sum将被显示。
- 在客户端分离关注点。在客户端,所有的通信代码将被放置在一个名为“add”的方法中。换句话说,客户端的主方法将没有与客户端 – 服务器通信相关的代码。相反,主例程将简单地调用一个名为“add”的本地方法。客户端侧的“add”方法将不执行任何加法,而是请求服务器执行加法。“add”方法将封装或隐藏与服务器的所有通信。这是所谓的“代理设计”的一种变体。“add”方法充当服务器的代理。当您的代码在本地“add”方法上进行调用时,您实际上是在进行远程过程调用(RPC)。客户端侧的“add”方法具有以下签名:
public static int add(int i)
- 在服务器上分离关注点。您的监听套接字连接的代码应该与执行加法操作的代码分开。换句话说,实际的算术应该在一个单独的方法中完成。UDP套接字通信代码将调用此方法。
- 编写一个客户端和服务器,其客户端与用户的交互如下:
客户端正在运行。
请输入服务器端口:
6789
3
服务器返回3。
2
服务器返回5。
-1
服务器返回4。
6
服务器返回10。
halt!
客户端侧退出。
如果客户端重新启动(请注意服务器仍在运行),我们有:
客户端正在运行。
请输入服务器端口:
6789
1
服务器返回11。
halt!
客户端侧退出。
- 在服务器上,控制台将显示如下交互:
服务器启动
添加:3到0
返回给客户端的和为3
添加:2到3
返回给客户端的和为5
等等...
注意:UDP消息由字节数组组成。您需要将一个整数放入一个四字节(32位)的字节数组中,然后再发送。在接收时,您需要从字节数组中提取一个整数。您可以使用来自外部源的代码来帮助您完成此操作。但请注意使用清晰的URL引用您的来源。
另一种方法是只传输包含字符串数据的字节数组。您可以使用任何一种方法。
:checkered_flag:在您的单个pdf上,复制您的客户端并清楚地标记为“Project2Task2Client”。
:checkered_flag:在您的单个pdf上,复制您的服务器并清楚地标记为“Project2Task2Server”。
:checkered_flag:截取您的客户端控制台屏幕的屏幕截图。它将包括五个整数输入(1,2,-3,4和5),并显示从服务器返回的和。它还将显示客户端被停止,然后重新运行第二次,输入(6,7,-8,9和10),以及客户端对用户“halt!”请求的响应。在您的单个pdf上,将此屏幕截图标记为“Project2Task2ClientConsole”。