编写具有客户端服务器实用程序Windows功能的软件,第02部分

继续关于Windows中控制台实用程序的自定义实现的系列文章,TFTP(临时文件传输协议)是一种简单的文件传输协议。

与上次一样,我们简要介绍一下该理论,查看实现与所需功能相似的功能的代码,然后进行分析。 阅读更多-削减

我不会复制粘贴参考信息,通常会在文章末尾找到这些参考信息的链接,我只是说TFTP本质上是FTP协议的简化版本,其中删除了访问控制设置,实际上除了接收和传输文件的命令外,这里没有任何其他内容。 但是,为了使我们的实现更加优雅并适应当前编写代码的原则,语法略有更改-不会更改工作原理,但是IMHO接口变得更具逻辑性并结合了FTP和TFTP的积极方面。

特别是,在启动时,客户端会询问服务器的ip地址和打开自定义TFTP的端口(由于与标准协议不兼容,我认为应该保留将端口选择给用户的选项),然后建立连接,因此客户端可以发送以下命令之一-获取或放置,以接收或发送文件到服务器。 所有文件都以二进制模式发送-为了简化逻辑。

为了实现协议,我传统上使用了4个类:

  • TFTP客户端
  • TFTP服务器
  • TFTPClientTester
  • TFTPServerTester

由于测试类仅用于调试主要类,因此我将不对其进行分析,但是代码将在存储库中,可以在本文结尾处找到指向该类的链接。 现在,我将了解主要课程。

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("\nDone\n"); } 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("\nDONE\n"); } 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 ++中的函数指针,或者至少是可怕而可怕的goto,让您实现美丽。 如果您知道如何使代码更加优雅,我正在等待评论中的批评。 在我看来,这里需要一个String-delegate字典,但是没有委托...

  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("\nDone\n"); }; 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实用工具的一系列文章的一部分,不可能不抚摸他。

参考文献:

源代码库
简要介绍TFTP
同样的事情,但俄语

Source: https://habr.com/ru/post/zh-CN461083/


All Articles