Працягваючы пачаты цыкл артыкулаў, прысвечаны кастамным рэалізацыям кансольных утыліт 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