Продовжуючи розпочатий цикл статей, присвячений кастомним реалізаціям консольних утиліт Windows не можна торкнутися TFTP (Trivial File Transfer Protocol) — простий протокол передачі файлів.
Як і минулого разу, коротко пробіжимося по теорії, побачимо код, що реалізує функціонал, аналогічний до необхідного, і проаналізуємо його. Детальніше - під катом
Не копіпаститиму довідкову інформацію, посилання на яку традиційно можна знайти в кінці статті, скажу лише, що за своєю суттю TFTP — спрощена варіація протоколу FTP, в якій прибрано налаштування контролю доступу, та й по суті тут немає нічого крім команд отримання та передачі файлу . Однак, щоб зробити нашу реалізацію трохи витонченішою і адаптованішою до нинішніх принципів написання коду, синтаксис трохи змінений — принципів роботи це не змінює, але інтерфейс, ІМХО, стає трохи більш логічним і поєднує позитивні сторони FTP і TFTP.
Зокрема, при запуску клієнт запитує ip адресу сервера і порт, на якому відкрито кастомний TFTP (через несумісність зі стандартним протоколом я вважав доречним залишити можливість вибору порту користувачу), після чого відбувається з'єднання, в результаті якого клієнт може виправити одну з команд. get або put, щоб отримати чи надіслати файл на сервер. Усі файли відправляються в бінарному режимі – з метою спрощення логіки.
Для реалізації протоолу мною було використано традиційно 4 класи:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
У силу того, що тестуючі класи існують тільки для налагодження основних, я їх не розбиратиму, але код буде знаходитися в репозиторії, з посиланням на нього можна ознайомитися в кінці статті. А основні класи зараз розберу.
TFTPClient
Завдання цього класу - підключитися до віддаленого сервера за його 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 байти, які згодом конвертуються в одне число. Це не дуже Джавний підхід, таке швидше подібне до СІ, але своє завдання воно вирішує.
Далі все тривіально – ми отримуємо відоме число байтів із сокету та записуємо їх у файл, після чого виводимо повідомлення про успіх.
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());
}
}
Селектор — функція, яка визначає дії програми, залежно від введеного рядка. Тут все не дуже красиво і використовується не найкращий прийом з примусовим виходом за межі блоку коду, але основною причиною цього є відсутність у Джаві деяких речей, як делегати в С#, покажчики на функцію з 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);
}
}
TFTPServer
Функціонал сервера відрізняється від функціонала клієнта за великим рахунком лише тим, що команди на нього приходять не з клавіатури, а із сокету. Частина методів взагалі збігається, тому наводити їх я не буду, торкнуся лише відмінності.
Для запуску тут використовується метод 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 збігається з аналогічним клієнтом, з тією лише різницею, що він зчитує дані з сокету, а не з клавіатури. Код у репозиторії, як і selector.
У разі ініціалізація винесено на окремий блок коду, т.к. у межах цієї реалізації після закінчення передачі ресурси звільняються і знову займаються заново — з метою забезпечення захисту від витоку пам'яті.
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());
}
}
Резюмуючи:
Щойно ми написали свою варіацію на тему простого протоколу передачі і розібралися у тому, як він має працювати. В принципі, Америки я тут не відкрив і сильно нового не написав, але аналогічних статей на Хабрі не було, а в рамках написання циклу статей про утиліти cmd не можна було його не торкнутися.
Посилання:
Джерело: habr.com