继续致力于 Windows 控制台实用程序的自定义实现的系列文章,我们忍不住要谈谈 TFTP(简单文件传输协议)——一种简单的文件传输协议。
和上次一样,让我们简单回顾一下理论,看看实现与所需功能类似的功能的代码,并对其进行分析。 更多细节 - 下切
我不会复制粘贴参考信息,传统上可以在文章末尾找到这些信息的链接,我只会说,TFTP 的核心是 FTP 协议的简化变体,其中访问控制设置具有已经被删除了,实际上这里除了接收和传输文件的命令之外什么也没有。 然而,为了使我们的实现更加优雅并适应当前编写代码的原则,语法略有改变 - 这不会改变操作原则,但界面,恕我直言,变得更加逻辑和结合了 FTP 和 TFTP 的优点。
特别是,启动时,客户端请求服务器的 IP 地址和打开自定义 TFTP 的端口(由于与标准协议不兼容,我认为让用户能够选择端口是适当的),之后连接发生后,客户端可以发送命令之一 - get 或 put,以接收文件或向服务器发送文件。 所有文件都以二进制模式发送以简化逻辑。
为了实现该协议,我传统上使用 4 个类:
- TFTP客户端
- TFTP服务器
- TFTP客户端测试仪
- TFTP服务器测试仪
由于测试类仅用于调试主要类,因此我不会分析它们,但代码将在存储库中;可以在文章末尾找到其链接。 现在我来看看主要的类。
TFTP客户端
此类的任务是通过 ip 和端口号连接到远程服务器,从输入流(在本例中为键盘)读取命令,解析它,将其传输到服务器,并且取决于您是否需要发送或接收文件、传输或获取文件。
用于启动客户端连接到服务器并等待来自输入流的命令的代码如下所示。 此处使用的许多全局变量在文章之外的程序全文中进行了描述。 由于它们的琐碎性,我不会引用它们,以免文章过多。
public void run(String ip, int port)
{
this.ip = ip;
this.port = port;
try {
inicialization();
Scanner keyboard = new Scanner(System.in);
while (isRunning) {
getAndParseInput(keyboard);
sendCommand();
selector();
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
让我们回顾一下这段代码中调用的方法:
这里发送文件 - 使用扫描仪,我们将文件的内容呈现为字节数组,我们将其一一写入套接字,然后关闭它并再次打开它(不是最明显的解决方案,但是它保证资源的释放),之后我们会显示一条有关成功传输的消息。
private void put(String sourcePath, String destPath)
{
File src = new File(sourcePath);
try {
InputStream scanner = new FileInputStream(src);
byte[] bytes = scanner.readAllBytes();
for (byte b : bytes)
sout.write(b);
sout.close();
inicialization();
System.out.println("nDonen");
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
此代码片段描述从服务器检索数据。 一切都变得微不足道,只有第一段代码令人感兴趣。 为了准确了解需要从套接字读取多少字节,您需要知道传输的文件有多少。 服务器上的文件大小表示为长整数,因此这里接受 4 个字节,随后将其转换为一个数字。 这不是一个非常 Java 的方法,它与 SI 非常相似,但它解决了它的问题。
然后一切都很简单 - 我们从套接字接收已知数量的字节并将它们写入文件,之后我们显示一条成功消息。
private void get(String sourcePath, String destPath){
long sizeOfFile = 0;
try {
byte[] sizeBytes = new byte[Long.SIZE];
for (int i =0; i< Long.SIZE/Byte.SIZE; i++)
{
sizeBytes[i] = (byte)sin.read();
sizeOfFile*=256;
sizeOfFile+=sizeBytes[i];
}
FileOutputStream writer = new FileOutputStream(new File(destPath));
for (int i =0; i < sizeOfFile; i++)
{
writer.write(sin.read());
}
writer.close();
System.out.println("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
如果在客户端窗口中输入了除 get 或 put 以外的命令,则会调用 showErrorMessage 函数,表明输入不正确。 由于琐碎,我就不引用了。 更有趣的是接收和分割输入字符串的函数。 我们将扫描仪传递给它,我们期望从中收到一行由两个空格分隔的行,其中包含命令、源地址和目标地址。
private void getAndParseInput(Scanner scanner)
{
try {
input = scanner.nextLine().split(" ");
typeOfCommand = input[0];
sourcePath = input[1];
destPath = input[2];
}
catch (Exception e) {
System.out.println("Bad input");
}
}
发送命令——将从扫描器输入的命令传输到套接字并强制发送
private void sendCommand()
{
try {
for (String str : input) {
for (char ch : str.toCharArray()) {
sout.write(ch);
}
sout.write(' ');
}
sout.write('n');
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
选择器是一个函数,它根据输入的字符串确定程序的操作。 这里的一切都不是很漂亮,使用的技巧也不是最好的,在代码块之外强制退出,但主要原因是 Java 中缺少一些东西,比如 C# 中的委托、C++ 中的函数指针或 at至少可怕的 goto 可以让你完美地实现这一点。 如果你知道如何让代码更优雅一点,欢迎在评论中批评。 在我看来,这里需要一个字符串委托字典,但是没有委托......
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}
TFTP服务器
总的来说,服务器的功能与客户端的功能不同,仅在于命令不是来自键盘,而是来自套接字。 有些方法大体相同,所以我不会引用它们,我只会触及差异。
首先,使用 run 方法,该方法接收端口作为输入,并在永恒循环中处理来自套接字的输入数据。
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}
put 方法包装了 writeToFileFromSocket 方法,该方法打开文件的写入流并从套接字写入所有输入字节,并在写入完成时显示一条消息,指示传输已成功完成。
private void put(String source, String dest){
writeToFileFromSocket();
System.out.print("nDonen");
};
private void writeToFileFromSocket()
{
try {
FileOutputStream writer = new FileOutputStream(new File(destPath));
byte[] bytes = sin.readAllBytes();
for (byte b : bytes) {
writer.write(b);
}
writer.close();
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
get 方法检索服务器文件。 正如程序客户端部分已经提到的,要成功传输文件,您需要知道它的大小,存储在一个长整数中,因此我将其拆分为 4 个字节的数组,逐字节传输它们到套接字,然后,在客户端接收并将它们组装成数字后,我传输组成文件的所有字节,从文件的输入流读取。
private void get(String source, String dest){
File sending = new File(source);
try {
FileInputStream readFromFile = new FileInputStream(sending);
byte[] arr = readFromFile.readAllBytes();
byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
sout.write(bytes[i]);
sout.flush();
for (byte b : arr)
sout.write(b);
}
catch (Exception e){
System.out.println(e.getMessage());
}
};
getAndParseInput 方法与客户端中的相同,唯一的区别是它从套接字读取数据而不是从键盘读取数据。 代码位于存储库中,就像选择器一样。
在这种情况下,初始化被放置在一个单独的代码块中,因为在此实现中,传输完成后,资源将被释放并再次重新占用 - 再次提供针对内存泄漏的保护。
private void incialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
总结:
我们刚刚在一个简单的数据传输协议上编写了我们自己的变体,并弄清楚了它应该如何工作。 原则上,我在这里并没有发现美国,也没有写太多新东西,但是关于 Habré 没有类似的文章,并且作为撰写有关 cmd 实用程序的一系列文章的一部分,不可能不触及它。
参考文献: