繼續致力於 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 實用程序的一系列文章的一部分,不可能不觸及它。
引用: